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.jingle.JingleConnectionManager;
 23import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 24import eu.siacs.conversations.xmpp.jingle.Media;
 25import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 26import im.conversations.android.xmpp.model.correction.Replace;
 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) {
 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.addExtension(new Replace(message.getEditedIdWireFormat()));
 68        }
 69        return packet;
 70    }
 71
 72    public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
 73        final SimpleDateFormat mDateFormat = new SimpleDateFormat(
 74                "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 75        mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 76        Element delay = packet.addChild("delay", "urn:xmpp:delay");
 77        Date date = new Date(timestamp);
 78        delay.setAttribute("stamp", mDateFormat.format(date));
 79    }
 80
 81    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
 82        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
 83        if (axolotlMessage == null) {
 84            return null;
 85        }
 86        packet.setAxolotlMessage(axolotlMessage.toElement());
 87        packet.setBody(OMEMO_FALLBACK_MESSAGE);
 88        packet.addChild("store", "urn:xmpp:hints");
 89        packet.addChild("encryption", "urn:xmpp:eme:0")
 90                .setAttribute("name", "OMEMO")
 91                .setAttribute("namespace", AxolotlService.PEP_PREFIX);
 92        return packet;
 93    }
 94
 95    public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
 96        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
 97        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
 98        packet.setTo(to);
 99        packet.setAxolotlMessage(axolotlMessage.toElement());
100        packet.addChild("store", "urn:xmpp:hints");
101        return packet;
102    }
103
104    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
105        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
106        String content;
107        if (message.hasFileOnRemoteHost()) {
108            final Message.FileParams fileParams = message.getFileParams();
109            content = fileParams.url;
110            packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
111        } else {
112            content = message.getBody();
113        }
114        packet.setBody(content);
115        return packet;
116    }
117
118    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
119        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
120        if (message.hasFileOnRemoteHost()) {
121            Message.FileParams fileParams = message.getFileParams();
122            final String url = fileParams.url;
123            packet.setBody(url);
124            packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
125        } else {
126            if (Config.supportUnencrypted()) {
127                packet.setBody(PGP_FALLBACK_MESSAGE);
128            }
129            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
130                packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
131            } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
132                packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
133            }
134            packet.addChild("encryption", "urn:xmpp:eme:0")
135                    .setAttribute("namespace", "jabber:x:encrypted");
136        }
137        return packet;
138    }
139
140    public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) {
141        final Account account = conversation.getAccount();
142        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
143        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);
144        packet.setTo(conversation.getJid().asBareJid());
145        packet.setFrom(account.getJid());
146        packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
147        packet.addChild("no-store", "urn:xmpp:hints");
148        packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
149        return packet;
150    }
151
152    public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) {
153        final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
154        final Jid to = message.getCounterpart();
155        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
156        packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
157        packet.setTo(groupChat ? to.asBareJid() : to);
158        final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
159        if (groupChat) {
160            final String stanzaId = message.getServerMsgId();
161            if (stanzaId != null) {
162                displayed.setAttribute("id", stanzaId);
163            } else {
164                displayed.setAttribute("sender", to.toString());
165                displayed.setAttribute("id", message.getRemoteMsgId());
166            }
167        } else {
168            displayed.setAttribute("id", message.getRemoteMsgId());
169        }
170        packet.addChild("store", "urn:xmpp:hints");
171        return packet;
172    }
173
174    public im.conversations.android.xmpp.model.stanza.Message reaction(final Conversational conversation, final String reactingTo, final Collection<String> ourReactions) {
175        final boolean groupChat = conversation.getMode() == Conversational.MODE_MULTI;
176        final Jid to = conversation.getJid().asBareJid();
177        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
178        packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
179        packet.setTo(to);
180        final var reactions = packet.addExtension(new Reactions());
181        reactions.setId(reactingTo);
182        for(final String ourReaction : ourReactions) {
183            reactions.addExtension(new Reaction(ourReaction));
184        }
185        packet.addChild("store", "urn:xmpp:hints");
186        return packet;
187    }
188
189    public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) {
190        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
191        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
192        packet.setTo(conversation.getJid().asBareJid());
193        packet.addChild("subject").setContent(subject);
194        packet.setFrom(conversation.getAccount().getJid().asBareJid());
195        return packet;
196    }
197
198    public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) {
199        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
200        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
201        packet.setTo(contact);
202        packet.setFrom(conversation.getAccount().getJid());
203        Element x = packet.addChild("x", "jabber:x:conference");
204        x.setAttribute("jid", conversation.getJid().asBareJid());
205        String password = conversation.getMucOptions().getPassword();
206        if (password != null) {
207            x.setAttribute("password", password);
208        }
209        if (contact.isFullJid()) {
210            packet.addChild("no-store", "urn:xmpp:hints");
211            packet.addChild("no-copy", "urn:xmpp:hints");
212        }
213        return packet;
214    }
215
216    public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) {
217        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
218        packet.setTo(conversation.getJid().asBareJid());
219        packet.setFrom(conversation.getAccount().getJid());
220        Element x = new Element("x");
221        x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
222        Element invite = new Element("invite");
223        invite.setAttribute("to", contact.asBareJid());
224        x.addChild(invite);
225        packet.addChild(x);
226        return packet;
227    }
228
229    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) {
230        final var receivedPacket =
231                new im.conversations.android.xmpp.model.stanza.Message();
232        receivedPacket.setType(type);
233        receivedPacket.setTo(from);
234        receivedPacket.setFrom(account.getJid());
235        for (final String namespace : namespaces) {
236            receivedPacket.addChild("received", namespace).setAttribute("id", id);
237        }
238        receivedPacket.addChild("store", "urn:xmpp:hints");
239        return receivedPacket;
240    }
241
242    public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) {
243        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
244        packet.setFrom(account.getJid());
245        packet.setTo(to);
246        packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
247        packet.addChild("store", "urn:xmpp:hints");
248        return packet;
249    }
250
251    public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
252            final Jid with, final String sessionId, final Reason reason) {
253        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
254        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
255        packet.setTo(with);
256        final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
257        finish.setAttribute("id", sessionId);
258        final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
259        reasonElement.addChild(reason.toString());
260        packet.addChild("store", "urn:xmpp:hints");
261        return packet;
262    }
263
264    public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
265        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
266        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
267        packet.setTo(proposal.with);
268        packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
269        final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
270        propose.setAttribute("id", proposal.sessionId);
271        for (final Media media : proposal.media) {
272            propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString());
273        }
274        packet.addChild("request", "urn:xmpp:receipts");
275        packet.addChild("store", "urn:xmpp:hints");
276        return packet;
277    }
278
279    public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
280        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
281        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
282        packet.setTo(proposal.with);
283        final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
284        propose.setAttribute("id", proposal.sessionId);
285        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
286        packet.addChild("store", "urn:xmpp:hints");
287        return packet;
288    }
289
290    public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) {
291        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
292        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
293        packet.setTo(with);
294        final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
295        propose.setAttribute("id", sessionId);
296        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
297        packet.addChild("store", "urn:xmpp:hints");
298        return packet;
299    }
300}