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.Store;
 22import im.conversations.android.xmpp.model.reactions.Reaction;
 23import im.conversations.android.xmpp.model.reactions.Reactions;
 24import im.conversations.android.xmpp.model.receipts.Received;
 25import im.conversations.android.xmpp.model.unique.OriginId;
 26import java.text.SimpleDateFormat;
 27import java.util.Collection;
 28import java.util.Date;
 29import java.util.Locale;
 30import java.util.TimeZone;
 31
 32public class MessageGenerator extends AbstractGenerator {
 33    private static final String OMEMO_FALLBACK_MESSAGE =
 34            "I sent you an OMEMO encrypted message but your client doesn’t seem to support that."
 35                    + " Find more information on https://conversations.im/omemo";
 36    private static final String PGP_FALLBACK_MESSAGE =
 37            "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
 38
 39    public MessageGenerator(XmppConnectionService service) {
 40        super(service);
 41    }
 42
 43    private im.conversations.android.xmpp.model.stanza.Message preparePacket(
 44            final Message message, final boolean legacyEncryption) {
 45        Conversation conversation = (Conversation) message.getConversation();
 46        Account account = conversation.getAccount();
 47        im.conversations.android.xmpp.model.stanza.Message packet =
 48                new im.conversations.android.xmpp.model.stanza.Message();
 49        final boolean isWithSelf = conversation.getContact().isSelf();
 50        if (conversation.getMode() == Conversation.MODE_SINGLE) {
 51            packet.setTo(message.getCounterpart());
 52            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
 53            if (!isWithSelf) {
 54                packet.addChild("request", "urn:xmpp:receipts");
 55            }
 56        } else if (message.isPrivateMessage()) {
 57            packet.setTo(message.getCounterpart());
 58            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
 59            packet.addChild("x", "http://jabber.org/protocol/muc#user");
 60            packet.addChild("request", "urn:xmpp:receipts");
 61        } else {
 62            packet.setTo(message.getCounterpart().asBareJid());
 63            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
 64        }
 65        if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) {
 66            packet.addChild("markable", "urn:xmpp:chat-markers:0");
 67        }
 68        packet.setFrom(account.getJid());
 69        packet.setId(message.getUuid());
 70        if (conversation.getMode() == Conversational.MODE_MULTI
 71                && !message.isPrivateMessage()
 72                && !conversation.getMucOptions().stableId()) {
 73            packet.addExtension(new OriginId(message.getUuid()));
 74        }
 75        if (message.edited()) {
 76            packet.addExtension(new Replace(message.getEditedIdWireFormat()));
 77        }
 78        if (!legacyEncryption) {
 79            if (message.getSubject() != null && message.getSubject().length() > 0) packet.addChild("subject").setContent(message.getSubject());
 80            // Legacy encryption can't handle advanced payloads
 81            for (Element el : message.getPayloads()) {
 82                packet.addChild(el);
 83            }
 84        } else {
 85            for (Element el : message.getPayloads()) {
 86                if ("thread".equals(el.getName())) packet.addChild(el);
 87            }
 88        }
 89        return packet;
 90    }
 91
 92    public void addDelay(
 93            im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
 94        final SimpleDateFormat mDateFormat =
 95                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 96        mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 97        Element delay = packet.addChild("delay", "urn:xmpp:delay");
 98        Date date = new Date(timestamp);
 99        delay.setAttribute("stamp", mDateFormat.format(date));
100    }
101
102    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(
103            Message message, XmppAxolotlMessage axolotlMessage) {
104        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
105        if (axolotlMessage == null) {
106            return null;
107        }
108        packet.setAxolotlMessage(axolotlMessage.toElement());
109        packet.setBody(OMEMO_FALLBACK_MESSAGE);
110        packet.addChild("store", "urn:xmpp:hints");
111        packet.addChild("encryption", "urn:xmpp:eme:0")
112                .setAttribute("name", "OMEMO")
113                .setAttribute("namespace", AxolotlService.PEP_PREFIX);
114        return packet;
115    }
116
117    public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(
118            Jid to, XmppAxolotlMessage axolotlMessage) {
119        im.conversations.android.xmpp.model.stanza.Message packet =
120                new im.conversations.android.xmpp.model.stanza.Message();
121        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
122        packet.setTo(to);
123        packet.setAxolotlMessage(axolotlMessage.toElement());
124        packet.addChild("store", "urn:xmpp:hints");
125        return packet;
126    }
127
128    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
129        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false);
130        String content;
131        if (message.hasFileOnRemoteHost()) {
132            final Message.FileParams fileParams = message.getFileParams();
133
134            if (message.getFallbacks(Namespace.OOB).isEmpty()) {
135                if (message.getBody().equals("")) {
136                    message.setBody(fileParams.url);
137                    final var fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
138                    fallback.addChild("body", "urn:xmpp:fallback:0");
139                    message.addPayload(fallback);
140                } else {
141                    long start = message.getRawBody().codePointCount(0, message.getRawBody().length());
142                    message.appendBody(fileParams.url);
143                    final var fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
144                    fallback.addChild("body", "urn:xmpp:fallback:0")
145                        .setAttribute("start", String.valueOf(start))
146                        .setAttribute("end", String.valueOf(start + fileParams.url.length()));
147                    message.addPayload(fallback);
148                }
149            }
150
151            packet = preparePacket(message, false);
152            packet.addChild("x", Namespace.OOB).addChild("url").setContent(fileParams.url);
153        }
154        if (message.getRawBody() != null) packet.setBody(message.getRawBody());
155        return packet;
156    }
157
158    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
159        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
160        if (message.hasFileOnRemoteHost()) {
161            Message.FileParams fileParams = message.getFileParams();
162            final String url = fileParams.url;
163            packet.setBody(url);
164            packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
165            packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
166                  .addChild("body", "urn:xmpp:fallback:0");
167        } else {
168            if (Config.supportUnencrypted()) {
169                packet.setBody(PGP_FALLBACK_MESSAGE);
170            }
171            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
172                packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
173            } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
174                packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
175            }
176            packet.addChild("encryption", "urn:xmpp:eme:0")
177                    .setAttribute("namespace", "jabber:x:encrypted");
178        }
179        return packet;
180    }
181
182    public im.conversations.android.xmpp.model.stanza.Message generateChatState(
183            Conversation conversation) {
184        final Account account = conversation.getAccount();
185        final im.conversations.android.xmpp.model.stanza.Message packet =
186                new im.conversations.android.xmpp.model.stanza.Message();
187        packet.setType(
188                conversation.getMode() == Conversation.MODE_MULTI
189                        ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
190                        : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
191        packet.setTo(conversation.getJid().asBareJid());
192        packet.setFrom(account.getJid());
193        packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
194        packet.addChild("no-store", "urn:xmpp:hints");
195        packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store*
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.addChild("store", "urn:xmpp:hints");
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.addChild("store", "urn:xmpp:hints");
248        return packet;
249    }
250
251    public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(
252            Conversation conversation, String subject) {
253        im.conversations.android.xmpp.model.stanza.Message packet =
254                new im.conversations.android.xmpp.model.stanza.Message();
255        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
256        packet.setTo(conversation.getJid().asBareJid());
257        packet.addChild("subject").setContent(subject);
258        packet.setFrom(conversation.getAccount().getJid().asBareJid());
259        return packet;
260    }
261
262    public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) {
263        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
264        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
265        packet.setTo(jid.asBareJid());
266        final var form = new Data();
267        form.setFormType("http://jabber.org/protocol/muc#request");
268        form.put("muc#role", "participant");
269        form.submit();
270        packet.addChild(form);
271        return packet;
272    }
273
274    public im.conversations.android.xmpp.model.stanza.Message directInvite(
275            final Conversation conversation, final Jid contact) {
276        im.conversations.android.xmpp.model.stanza.Message packet =
277                new im.conversations.android.xmpp.model.stanza.Message();
278        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
279        packet.setTo(contact);
280        packet.setFrom(conversation.getAccount().getJid());
281        Element x = packet.addChild("x", "jabber:x:conference");
282        x.setAttribute("jid", conversation.getJid().asBareJid());
283        String password = conversation.getMucOptions().getPassword();
284        if (password != null) {
285            x.setAttribute("password", password);
286        }
287        if (contact.isFullJid()) {
288            packet.addChild("no-store", "urn:xmpp:hints");
289            packet.addChild("no-copy", "urn:xmpp:hints");
290        }
291        return packet;
292    }
293
294    public im.conversations.android.xmpp.model.stanza.Message invite(
295            final Conversation conversation, final Jid contact) {
296        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
297        packet.setTo(conversation.getJid().asBareJid());
298        packet.setFrom(conversation.getAccount().getJid());
299        Element x = new Element("x");
300        x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
301        Element invite = new Element("invite");
302        invite.setAttribute("to", contact.asBareJid());
303        x.addChild(invite);
304        packet.addChild(x);
305        return packet;
306    }
307
308    public im.conversations.android.xmpp.model.stanza.Message received(
309            final Jid from,
310            final String id,
311            final im.conversations.android.xmpp.model.stanza.Message.Type type) {
312        final var receivedPacket = new im.conversations.android.xmpp.model.stanza.Message();
313        receivedPacket.setType(type);
314        receivedPacket.setTo(from);
315        receivedPacket.addExtension(new Received(id));
316        receivedPacket.addExtension(new Store());
317        return receivedPacket;
318    }
319
320    public im.conversations.android.xmpp.model.stanza.Message received(
321            Account account, Jid to, String id) {
322        im.conversations.android.xmpp.model.stanza.Message packet =
323                new im.conversations.android.xmpp.model.stanza.Message();
324        packet.setFrom(account.getJid());
325        packet.setTo(to);
326        packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
327        packet.addChild("store", "urn:xmpp:hints");
328        return packet;
329    }
330
331    public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
332            final Jid with, final String sessionId, final Reason reason) {
333        final im.conversations.android.xmpp.model.stanza.Message packet =
334                new im.conversations.android.xmpp.model.stanza.Message();
335        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
336        packet.setTo(with);
337        final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
338        finish.setAttribute("id", sessionId);
339        final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
340        reasonElement.addChild(reason.toString());
341        packet.addChild("store", "urn:xmpp:hints");
342        return packet;
343    }
344
345    public im.conversations.android.xmpp.model.stanza.Message sessionProposal(
346            final JingleConnectionManager.RtpSessionProposal proposal) {
347        final im.conversations.android.xmpp.model.stanza.Message packet =
348                new im.conversations.android.xmpp.model.stanza.Message();
349        packet.setType(
350                im.conversations.android.xmpp.model.stanza.Message.Type
351                        .CHAT); // we want to carbon copy those
352        packet.setTo(proposal.with);
353        packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
354        final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
355        propose.setAttribute("id", proposal.sessionId);
356        for (final Media media : proposal.media) {
357            propose.addChild("description", Namespace.JINGLE_APPS_RTP)
358                    .setAttribute("media", media.toString());
359        }
360        packet.addChild("request", "urn:xmpp:receipts");
361        packet.addChild("store", "urn:xmpp:hints");
362        return packet;
363    }
364
365    public im.conversations.android.xmpp.model.stanza.Message sessionRetract(
366            final JingleConnectionManager.RtpSessionProposal proposal) {
367        final im.conversations.android.xmpp.model.stanza.Message packet =
368                new im.conversations.android.xmpp.model.stanza.Message();
369        packet.setType(
370                im.conversations.android.xmpp.model.stanza.Message.Type
371                        .CHAT); // we want to carbon copy those
372        packet.setTo(proposal.with);
373        final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
374        propose.setAttribute("id", proposal.sessionId);
375        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
376        packet.addChild("store", "urn:xmpp:hints");
377        return packet;
378    }
379
380    public im.conversations.android.xmpp.model.stanza.Message sessionReject(
381            final Jid with, final String sessionId) {
382        final im.conversations.android.xmpp.model.stanza.Message packet =
383                new im.conversations.android.xmpp.model.stanza.Message();
384        packet.setType(
385                im.conversations.android.xmpp.model.stanza.Message.Type
386                        .CHAT); // we want to carbon copy those
387        packet.setTo(with);
388        final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
389        propose.setAttribute("id", sessionId);
390        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
391        packet.addChild("store", "urn:xmpp:hints");
392        return packet;
393    }
394}