From a8e57ce1c9a9603dbb0f22b8dd6d924016364609 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 3 Oct 2022 15:21:48 -0500 Subject: [PATCH] Fetch XEP-0231 inline images from trusted contacts 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. --- .../com/cheogram/android/BobTransfer.java | 72 +++++++++++++------ .../siacs/conversations/entities/Contact.java | 4 ++ .../conversations/entities/Conversation.java | 6 ++ .../entities/Conversational.java | 2 + .../entities/StubConversation.java | 6 ++ .../conversations/parser/MessageParser.java | 2 +- .../persistance/FileBackend.java | 19 +++-- .../ui/ConversationFragment.java | 2 +- .../ui/adapter/MessageAdapter.java | 13 +++- .../conversations/utils/CryptoHelper.java | 13 ++++ 10 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index 680c2bdfed1b0beab3c0fb4fd886bbd62af2f2a1..cbe30a302674891129c61620f970d0cb215d2625 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/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); + } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 42232c560f784d1b3cc13c9c325586eee82613ae..6da9283f982613af3a9673cc3a8ab76d9c4fdbb3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/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))) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8367f7cba0da69838484bd194b9f5ba20f7417a3..05537efd27ac3855b425d2130ad6961558855288 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/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 diff --git a/src/main/java/eu/siacs/conversations/entities/Conversational.java b/src/main/java/eu/siacs/conversations/entities/Conversational.java index 58af42213c570a95c54494a721cc64e582fc25dd..9d5aa8e2b91d4103f357a7fb769720c9157d5290 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversational.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversational.java @@ -45,4 +45,6 @@ public interface Conversational { int getMode(); String getUuid(); + + boolean canInferPresence(); } diff --git a/src/main/java/eu/siacs/conversations/entities/StubConversation.java b/src/main/java/eu/siacs/conversations/entities/StubConversation.java index 79f4e80b4a65a60b7dc6f8881169d7955435036c..42f98e740233cc1be7f28e0a1dba44c6e4c22cc0 100644 --- a/src/main/java/eu/siacs/conversations/entities/StubConversation.java +++ b/src/main/java/eu/siacs/conversations/entities/StubConversation.java @@ -70,4 +70,10 @@ public class StubConversation implements Conversational { public String getUuid() { return uuid; } + + @Override + public boolean canInferPresence() { + final Contact contact = getContact(); + return contact != null && contact.canInferPresence(); + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 1613f037283e8a4d21a15fb2dbfc5edcdaa83b88..d6bd5ef3e91ae550218fa0e32d192fb10241bcb4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 13118252c08e574289c96c414e285bb18c0ccefd..4a5f4d3aed5bf9339aba259c85802bd8fa6c2c12 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/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)) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 6359e363c5b7300bcf4180210e9b834a7bec2a97..73bf6a556930429e866181d475fda3f68aad6483 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index efb5e9b1373d6854036030c4ae172221e8e4753e..7b57c0dc27c529f72c096e79d41bf9f3d86e840c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/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 { 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) { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index c2d47ebe3c8ddbbe0092eaf285b56fa8676e3bdd..410a43838e80781838691618a332a5e06214e8d1 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/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;