MessageGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import eu.siacs.conversations.Config;
  4import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  5import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  6import eu.siacs.conversations.entities.Account;
  7import eu.siacs.conversations.entities.Conversation;
  8import eu.siacs.conversations.entities.Conversational;
  9import eu.siacs.conversations.entities.Message;
 10import eu.siacs.conversations.services.XmppConnectionService;
 11import eu.siacs.conversations.xml.Element;
 12import eu.siacs.conversations.xml.Namespace;
 13import eu.siacs.conversations.xmpp.Jid;
 14import eu.siacs.conversations.xmpp.chatstate.ChatState;
 15import eu.siacs.conversations.xmpp.forms.Data;
 16import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 17import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 18import eu.siacs.conversations.xmpp.jingle.Media;
 19import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 20import im.conversations.android.xmpp.model.correction.Replace;
 21import im.conversations.android.xmpp.model.hints.NoStore;
 22import im.conversations.android.xmpp.model.hints.Store;
 23import im.conversations.android.xmpp.model.reactions.Reaction;
 24import im.conversations.android.xmpp.model.reactions.Reactions;
 25import im.conversations.android.xmpp.model.receipts.Received;
 26import im.conversations.android.xmpp.model.unique.OriginId;
 27import java.text.SimpleDateFormat;
 28import java.util.Collection;
 29import java.util.Date;
 30import java.util.Locale;
 31import java.util.TimeZone;
 32
 33public class MessageGenerator extends AbstractGenerator {
 34    private static final String OMEMO_FALLBACK_MESSAGE =
 35            "I sent you an OMEMO encrypted message but your client doesn’t seem to support that."
 36                    + " Find more information on https://conversations.im/omemo";
 37    private static final String PGP_FALLBACK_MESSAGE =
 38            "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
 39
 40    public MessageGenerator(XmppConnectionService service) {
 41        super(service);
 42    }
 43
 44    private im.conversations.android.xmpp.model.stanza.Message preparePacket(
 45            final Message message, final boolean legacyEncryption) {
 46        Conversation conversation = (Conversation) message.getConversation();
 47        Account account = conversation.getAccount();
 48        im.conversations.android.xmpp.model.stanza.Message packet =
 49                new im.conversations.android.xmpp.model.stanza.Message();
 50        final boolean isWithSelf = conversation.getContact().isSelf();
 51        if (conversation.getMode() == Conversation.MODE_SINGLE) {
 52            packet.setTo(message.getCounterpart());
 53            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
 54            if (!isWithSelf) {
 55                packet.addChild("request", "urn:xmpp:receipts");
 56            }
 57        } else if (message.isPrivateMessage()) {
 58            packet.setTo(message.getCounterpart());
 59            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
 60            packet.addChild("x", "http://jabber.org/protocol/muc#user");
 61            packet.addChild("request", "urn:xmpp:receipts");
 62        } else {
 63            packet.setTo(message.getCounterpart().asBareJid());
 64            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
 65        }
 66        if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) {
 67            packet.addChild("markable", "urn:xmpp:chat-markers:0");
 68        }
 69        packet.setFrom(account.getJid());
 70        packet.setId(message.getUuid());
 71        if (conversation.getMode() == Conversational.MODE_MULTI
 72                && !message.isPrivateMessage()
 73                && !conversation.getMucOptions().stableId()) {
 74            packet.addExtension(new OriginId(message.getUuid()));
 75        }
 76        if (message.edited()) {
 77            packet.addExtension(new Replace(message.getEditedIdWireFormat()));
 78        }
 79        if (!legacyEncryption) {
 80            if (message.getSubject() != null && message.getSubject().length() > 0) packet.addChild("subject").setContent(message.getSubject());
 81            // Legacy encryption can't handle advanced payloads
 82            for (Element el : message.getPayloads()) {
 83                packet.addChild(el);
 84            }
 85        } else {
 86            for (Element el : message.getPayloads()) {
 87                if ("thread".equals(el.getName())) packet.addChild(el);
 88            }
 89        }
 90        return packet;
 91    }
 92
 93    public void addDelay(
 94            im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
 95        final SimpleDateFormat mDateFormat =
 96                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 97        mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 98        Element delay = packet.addChild("delay", "urn:xmpp:delay");
 99        Date date = new Date(timestamp);
100        delay.setAttribute("stamp", mDateFormat.format(date));
101    }
102
103    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(
104            Message message, XmppAxolotlMessage axolotlMessage) {
105        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
106        if (axolotlMessage == null) {
107            return null;
108        }
109        packet.setAxolotlMessage(axolotlMessage.toElement());
110        packet.setBody(OMEMO_FALLBACK_MESSAGE);
111        packet.addExtension(new Store());
112        packet.addChild("encryption", "urn:xmpp:eme:0")
113                .setAttribute("name", "OMEMO")
114                .setAttribute("namespace", AxolotlService.PEP_PREFIX);
115        return packet;
116    }
117
118    public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(
119            Jid to, XmppAxolotlMessage axolotlMessage) {
120        im.conversations.android.xmpp.model.stanza.Message packet =
121                new im.conversations.android.xmpp.model.stanza.Message();
122        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
123        packet.setTo(to);
124        packet.setAxolotlMessage(axolotlMessage.toElement());
125        packet.addChild(new Store());
126        return packet;
127    }
128
129    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
130        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false);
131        String content;
132        if (message.hasFileOnRemoteHost()) {
133            final Message.FileParams fileParams = message.getFileParams();
134
135            if (message.getFallbacks(Namespace.OOB).isEmpty()) {
136                if (message.getBody().equals("")) {
137                    message.setBody(fileParams.url);
138                    final var fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
139                    fallback.addChild("body", "urn:xmpp:fallback:0");
140                    message.addPayload(fallback);
141                } else {
142                    long start = message.getRawBody().codePointCount(0, message.getRawBody().length());
143                    message.appendBody(fileParams.url);
144                    final var fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
145                    fallback.addChild("body", "urn:xmpp:fallback:0")
146                        .setAttribute("start", String.valueOf(start))
147                        .setAttribute("end", String.valueOf(start + fileParams.url.length()));
148                    message.addPayload(fallback);
149                }
150            }
151
152            packet = preparePacket(message, false);
153            packet.addChild("x", Namespace.OOB).addChild("url").setContent(fileParams.url);
154        }
155        if (message.getRawBody() != null) packet.setBody(message.getRawBody());
156        return packet;
157    }
158
159    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
160        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
161        if (message.hasFileOnRemoteHost()) {
162            Message.FileParams fileParams = message.getFileParams();
163            final String url = fileParams.url;
164            packet.setBody(url);
165            packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
166            packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
167                  .addChild("body", "urn:xmpp:fallback:0");
168        } else {
169            if (Config.supportUnencrypted()) {
170                packet.setBody(PGP_FALLBACK_MESSAGE);
171            }
172            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
173                packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
174            } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
175                packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
176            }
177            packet.addChild("encryption", "urn:xmpp:eme:0")
178                    .setAttribute("namespace", "jabber:x:encrypted");
179        }
180        return packet;
181    }
182
183    public im.conversations.android.xmpp.model.stanza.Message generateChatState(
184            Conversation conversation) {
185        final Account account = conversation.getAccount();
186        final im.conversations.android.xmpp.model.stanza.Message packet =
187                new im.conversations.android.xmpp.model.stanza.Message();
188        packet.setType(
189                conversation.getMode() == Conversation.MODE_MULTI
190                        ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
191                        : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
192        packet.setTo(conversation.getJid().asBareJid());
193        packet.setFrom(account.getJid());
194        packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
195        packet.addExtension(new NoStore());
196        return packet;
197    }
198
199    public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) {
200        final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
201        final Jid to = message.getCounterpart();
202        final im.conversations.android.xmpp.model.stanza.Message packet =
203                new im.conversations.android.xmpp.model.stanza.Message();
204        packet.setType(
205                groupChat
206                        ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
207                        : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
208        packet.setTo(groupChat ? to.asBareJid() : to);
209        final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
210        if (groupChat) {
211            final String stanzaId = message.getServerMsgId();
212            if (stanzaId != null) {
213                displayed.setAttribute("id", stanzaId);
214            } else {
215                displayed.setAttribute("sender", to.toString());
216                displayed.setAttribute("id", message.getRemoteMsgId());
217            }
218        } else {
219            displayed.setAttribute("id", message.getRemoteMsgId());
220        }
221        packet.addExtension(new Store());
222        return packet;
223    }
224
225    public im.conversations.android.xmpp.model.stanza.Message reaction(
226            final Jid to,
227            final boolean groupChat,
228            final Message inReplyTo,
229            final String reactingTo,
230            final Collection<String> ourReactions) {
231        final im.conversations.android.xmpp.model.stanza.Message packet =
232                new im.conversations.android.xmpp.model.stanza.Message();
233        packet.setType(
234                groupChat
235                        ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
236                        : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
237        packet.setTo(to);
238        final var reactions = packet.addExtension(new Reactions());
239        reactions.setId(reactingTo);
240        for (final String ourReaction : ourReactions) {
241            reactions.addExtension(new Reaction(ourReaction));
242        }
243
244        final var thread = inReplyTo.getThread();
245        if (thread != null) packet.addChild(thread);
246
247        packet.addExtension(new Store());
248        return packet;
249    }
250
251    public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) {
252        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
253        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
254        packet.setTo(jid.asBareJid());
255        final var form = new Data();
256        form.setFormType("http://jabber.org/protocol/muc#request");
257        form.put("muc#role", "participant");
258        form.submit();
259        packet.addChild(form);
260        return packet;
261    }
262
263    public im.conversations.android.xmpp.model.stanza.Message received(
264            final Jid from,
265            final String id,
266            final im.conversations.android.xmpp.model.stanza.Message.Type type) {
267        final var receivedPacket = new im.conversations.android.xmpp.model.stanza.Message();
268        receivedPacket.setType(type);
269        receivedPacket.setTo(from);
270        receivedPacket.addExtension(new Received(id));
271        receivedPacket.addExtension(new Store());
272        return receivedPacket;
273    }
274
275    public im.conversations.android.xmpp.model.stanza.Message received(
276            Account account, Jid to, String id) {
277        im.conversations.android.xmpp.model.stanza.Message packet =
278                new im.conversations.android.xmpp.model.stanza.Message();
279        packet.setFrom(account.getJid());
280        packet.setTo(to);
281        packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
282        packet.addExtension(new Store());
283        return packet;
284    }
285
286    public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
287            final Jid with, final String sessionId, final Reason reason) {
288        final im.conversations.android.xmpp.model.stanza.Message packet =
289                new im.conversations.android.xmpp.model.stanza.Message();
290        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
291        packet.setTo(with);
292        final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
293        finish.setAttribute("id", sessionId);
294        final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
295        reasonElement.addChild(reason.toString());
296        packet.addExtension(new Store());
297        return packet;
298    }
299
300    public im.conversations.android.xmpp.model.stanza.Message sessionProposal(
301            final JingleConnectionManager.RtpSessionProposal proposal) {
302        final im.conversations.android.xmpp.model.stanza.Message packet =
303                new im.conversations.android.xmpp.model.stanza.Message();
304        packet.setType(
305                im.conversations.android.xmpp.model.stanza.Message.Type
306                        .CHAT); // we want to carbon copy those
307        packet.setTo(proposal.with);
308        packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
309        final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
310        propose.setAttribute("id", proposal.sessionId);
311        for (final Media media : proposal.media) {
312            propose.addChild("description", Namespace.JINGLE_APPS_RTP)
313                    .setAttribute("media", media.toString());
314        }
315        packet.addChild("request", "urn:xmpp:receipts");
316        packet.addExtension(new Store());
317        return packet;
318    }
319
320    public im.conversations.android.xmpp.model.stanza.Message sessionRetract(
321            final JingleConnectionManager.RtpSessionProposal proposal) {
322        final im.conversations.android.xmpp.model.stanza.Message packet =
323                new im.conversations.android.xmpp.model.stanza.Message();
324        packet.setType(
325                im.conversations.android.xmpp.model.stanza.Message.Type
326                        .CHAT); // we want to carbon copy those
327        packet.setTo(proposal.with);
328        final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
329        propose.setAttribute("id", proposal.sessionId);
330        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
331        packet.addExtension(new Store());
332        return packet;
333    }
334
335    public im.conversations.android.xmpp.model.stanza.Message sessionReject(
336            final Jid with, final String sessionId) {
337        final im.conversations.android.xmpp.model.stanza.Message packet =
338                new im.conversations.android.xmpp.model.stanza.Message();
339        packet.setType(
340                im.conversations.android.xmpp.model.stanza.Message.Type
341                        .CHAT); // we want to carbon copy those
342        packet.setTo(with);
343        final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
344        propose.setAttribute("id", sessionId);
345        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
346        packet.addExtension(new Store());
347        return packet;
348    }
349}