MessageGenerator.java

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