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