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