Fetch XEP-0231 inline images from trusted contacts

Stephen Paul Weber created

If a contact is trusted or already likely to know our presence (due to having
sent them a message) then auto-fetch XHTML-IM inline images via XEP-0231 from them.

Change summary

src/cheogram/java/com/cheogram/android/BobTransfer.java             | 72 
src/main/java/eu/siacs/conversations/entities/Contact.java          |  4 
src/main/java/eu/siacs/conversations/entities/Conversation.java     |  6 
src/main/java/eu/siacs/conversations/entities/Conversational.java   |  2 
src/main/java/eu/siacs/conversations/entities/StubConversation.java |  6 
src/main/java/eu/siacs/conversations/parser/MessageParser.java      |  2 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java   | 19 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java   |  2 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java | 13 
src/main/java/eu/siacs/conversations/utils/CryptoHelper.java        | 13 
10 files changed, 107 insertions(+), 32 deletions(-)

Detailed changes

src/cheogram/java/com/cheogram/android/BobTransfer.java 🔗

@@ -15,6 +15,7 @@ import io.ipfs.cid.Cid;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Transferable;
@@ -24,12 +25,14 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class BobTransfer implements Transferable {
 	protected int status = Transferable.STATUS_OFFER;
-	protected Message message;
 	protected URI uri;
+	protected Account account;
+	protected Jid to;
 	protected XmppConnectionService xmppConnectionService;
 
 	public static Cid cid(URI uri) {
@@ -44,10 +47,15 @@ public class BobTransfer implements Transferable {
 		}
 	}
 
-	public BobTransfer(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
-		this.message = message;
+	public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
+		return new URI("cid", CryptoHelper.multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+	}
+
+	public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {
 		this.xmppConnectionService = xmppConnectionService;
-		this.uri = new URI(message.getFileParams().url);
+		this.uri = uri;
+		this.to = to;
+		this.account = account;
 	}
 
 	@Override
@@ -56,10 +64,7 @@ public class BobTransfer implements Transferable {
 		File f = xmppConnectionService.getFileForCid(cid(uri));
 
 		if (f != null && f.canRead()) {
-			message.setRelativeFilePath(f.getAbsolutePath());
-			finish();
-			message.setTransferable(null);
-			xmppConnectionService.updateConversationUi();
+			finish(f);
 			return true;
 		}
 
@@ -67,13 +72,14 @@ public class BobTransfer implements Transferable {
 			changeStatus(Transferable.STATUS_DOWNLOADING);
 
 			IqPacket request = new IqPacket(IqPacket.TYPE.GET);
-			request.setTo(message.getCounterpart());
+			request.setTo(to);
 			final Element dataq = request.addChild("data", "urn:xmpp:bob");
 			dataq.setAttribute("cid", uri.getSchemeSpecificPart());
-			xmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (acct, packet) -> {
+			xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> {
 				final Element data = packet.findChild("data", "urn:xmpp:bob");
 				if (packet.getType() == IqPacket.TYPE.ERROR || data == null) {
 					Log.d(Config.LOGTAG, "BobTransfer failed: " + packet);
+					finish(null);
 					xmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
 				} else {
 					final String contentType = data.getAttribute("type");
@@ -85,25 +91,23 @@ public class BobTransfer implements Transferable {
 					try {
 						final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT);
 
-						xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new ByteArrayInputStream(bytes), fileExtension);
-						DownloadableFile file = xmppConnectionService.getFileBackend().getFile(message);
+						File file = xmppConnectionService.getFileBackend().getStorageLocation(new ByteArrayInputStream(bytes), fileExtension);
 						file.getParentFile().mkdirs();
 						if (!file.exists() && !file.createNewFile()) {
 							throw new IOException(file.getAbsolutePath());
 						}
 
-						final OutputStream outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
+						final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false);
 						outputStream.write(bytes);
 						outputStream.flush();
 						outputStream.close();
 
-						finish();
+						finish(file);
 					} catch (IOException e) {
+						finish(null);
 						xmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
 					}
 				}
-				message.setTransferable(null);
-				xmppConnectionService.updateConversationUi();
 			});
 			return true;
 		} else {
@@ -130,7 +134,6 @@ public class BobTransfer implements Transferable {
 	public void cancel() {
 		// No real way to cancel an iq in process...
 		changeStatus(Transferable.STATUS_CANCELLED);
-		message.setTransferable(null);
 	}
 
 	protected void changeStatus(int newStatus) {
@@ -138,10 +141,35 @@ public class BobTransfer implements Transferable {
 		xmppConnectionService.updateConversationUi();
 	}
 
-	protected void finish() {
-		final boolean privateMessage = message.isPrivateMessage();
-		message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
-		xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
-		xmppConnectionService.updateMessage(message);
+	protected void finish(File f) {
+		if (f != null) xmppConnectionService.updateConversationUi();
+	}
+
+	public static class ForMessage extends BobTransfer {
+		protected Message message;
+
+		public ForMessage(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
+			super(new URI(message.getFileParams().url), message.getConversation().getAccount(), message.getCounterpart(), xmppConnectionService);
+			this.message = message;
+		}
+
+		@Override
+		public void cancel() {
+			super.cancel();
+			message.setTransferable(null);
+		}
+
+		@Override
+		protected void finish(File f) {
+			if (f != null) {
+				message.setRelativeFilePath(f.getAbsolutePath());
+				final boolean privateMessage = message.isPrivateMessage();
+				message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
+				xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
+				xmppConnectionService.updateMessage(message);
+			}
+			message.setTransferable(null);
+			super.finish(f);
+		}
 	}
 }

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -414,6 +414,10 @@ public class Contact implements ListItem, Blockable {
         return ((this.subscription & (1 << option)) != 0);
     }
 
+    public boolean canInferPresence() {
+        return showInContactList() || isSelf();
+    }
+
     public boolean showInRoster() {
         return (this.getOption(Contact.Options.IN_ROSTER) && (!this
                 .getOption(Contact.Options.DIRTY_DELETE)))

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -1130,6 +1130,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return count;
     }
 
+    public boolean canInferPresence() {
+        final Contact contact = getContact();
+        if (contact != null && contact.canInferPresence()) return true;
+        return sentMessagesCount() > 0;
+    }
+
     public boolean isWithStranger() {
         final Contact contact = getContact();
         return mode == MODE_SINGLE

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -765,7 +765,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
                 if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) {
                     try {
-                        BobTransfer transfer = new BobTransfer(message, mXmppConnectionService);
+                        BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService);
                         message.setTransferable(transfer);
                         transfer.start();
                     } catch (URISyntaxException e) {

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -887,13 +887,7 @@ public class FileBackend {
     }
 
     public void setupRelativeFilePath(final Message message, final InputStream is, final String extension) throws IOException {
-        Cid[] cids = calculateCids(is);
-
-        setupRelativeFilePath(message, String.format("%s.%s", cids[0], extension));
-        File file = getFile(message);
-        for (int i = 0; i < cids.length; i++) {
-            mXmppConnectionService.saveCid(cids[i], file);
-        }
+        message.setRelativeFilePath(getStorageLocation(is, extension).getAbsolutePath());
     }
 
     public void setupRelativeFilePath(final Message message, final String filename) {
@@ -902,6 +896,17 @@ public class FileBackend {
         setupRelativeFilePath(message, filename, mime);
     }
 
+    public File getStorageLocation(final InputStream is, final String extension) throws IOException {
+        final String mime = MimeUtils.guessMimeTypeFromExtension(extension);
+        Cid[] cids = calculateCids(is);
+
+        File file = getStorageLocation(String.format("%s.%s", cids[0], extension), mime);
+        for (int i = 0; i < cids.length; i++) {
+            mXmppConnectionService.saveCid(cids[i], file);
+        }
+        return file;
+    }
+
     public File getStorageLocation(final String filename, final String mime) {
         final File parentDirectory;
         if (Strings.isNullOrEmpty(mime)) {

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1946,7 +1946,7 @@ public class ConversationFragment extends XmppFragment
         }
         if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) {
             try {
-                BobTransfer transfer = new BobTransfer(message, activity.xmppConnectionService);
+                BobTransfer transfer = new BobTransfer.ForMessage(message, activity.xmppConnectionService);
                 message.setTransferable(transfer);
                 transfer.start();
             } catch (URISyntaxException e) {

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -36,10 +36,14 @@ import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
 
+import com.cheogram.android.BobTransfer;
+
 import com.google.common.base.Strings;
 
 import java.io.IOException;
 import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -450,7 +454,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             SpannableStringBuilder body = message.getMergedBody((cid) -> {
                 try {
                     DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
-                    if (f == null) return null;
+                    if (f == null || !f.canRead()) {
+                        if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
+
+                        try {
+                            new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
+                        } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
+                        return null;
+                    }
 
                     Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
                     if (d == null) {

src/main/java/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -288,6 +288,19 @@ public final class CryptoHelper {
         return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
     }
 
+    public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
+        switch(type) {
+        case sha1:
+            return "sha1";
+        case sha2_256:
+            return "sha-256";
+        case sha2_512:
+            return "sha-512";
+        default:
+            throw new NoSuchAlgorithmException("" + type);
+        }
+    }
+
     public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
         if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
             return Multihash.Type.sha1;