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.NoStore;
 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) {
 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        return packet;
 79    }
 80
 81    public void addDelay(
 82            im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
 83        final SimpleDateFormat mDateFormat =
 84                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 85        mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 86        Element delay = packet.addChild("delay", "urn:xmpp:delay");
 87        Date date = new Date(timestamp);
 88        delay.setAttribute("stamp", mDateFormat.format(date));
 89    }
 90
 91    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(
 92            Message message, XmppAxolotlMessage axolotlMessage) {
 93        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
 94        if (axolotlMessage == null) {
 95            return null;
 96        }
 97        packet.setAxolotlMessage(axolotlMessage.toElement());
 98        packet.setBody(OMEMO_FALLBACK_MESSAGE);
 99        packet.addExtension(new Store());
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(
107            Jid to, XmppAxolotlMessage axolotlMessage) {
108        im.conversations.android.xmpp.model.stanza.Message packet =
109                new im.conversations.android.xmpp.model.stanza.Message();
110        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
111        packet.setTo(to);
112        packet.setAxolotlMessage(axolotlMessage.toElement());
113        packet.addChild(new Store());
114        return packet;
115    }
116
117    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
118        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
119        String content;
120        if (message.hasFileOnRemoteHost()) {
121            final Message.FileParams fileParams = message.getFileParams();
122            content = fileParams.url;
123            packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
124        } else {
125            content = message.getBody();
126        }
127        packet.setBody(content);
128        return packet;
129    }
130
131    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
132        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
133        if (message.hasFileOnRemoteHost()) {
134            Message.FileParams fileParams = message.getFileParams();
135            final String url = fileParams.url;
136            packet.setBody(url);
137            packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
138        } else {
139            if (Config.supportUnencrypted()) {
140                packet.setBody(PGP_FALLBACK_MESSAGE);
141            }
142            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
143                packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
144            } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
145                packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
146            }
147            packet.addChild("encryption", "urn:xmpp:eme:0")
148                    .setAttribute("namespace", "jabber:x:encrypted");
149        }
150        return packet;
151    }
152
153    public im.conversations.android.xmpp.model.stanza.Message generateChatState(
154            Conversation conversation) {
155        final Account account = conversation.getAccount();
156        final im.conversations.android.xmpp.model.stanza.Message packet =
157                new im.conversations.android.xmpp.model.stanza.Message();
158        packet.setType(
159                conversation.getMode() == Conversation.MODE_MULTI
160                        ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT
161                        : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
162        packet.setTo(conversation.getJid().asBareJid());
163        packet.setFrom(account.getJid());
164        packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
165        packet.addExtension(new NoStore());
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.addExtension(new Store());
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.addExtension(new Store());
213        return packet;
214    }
215
216    public im.conversations.android.xmpp.model.stanza.Message received(
217            final Jid from,
218            final String id,
219            final im.conversations.android.xmpp.model.stanza.Message.Type type) {
220        final var receivedPacket = new im.conversations.android.xmpp.model.stanza.Message();
221        receivedPacket.setType(type);
222        receivedPacket.setTo(from);
223        receivedPacket.addExtension(new Received(id));
224        receivedPacket.addExtension(new Store());
225        return receivedPacket;
226    }
227
228    public im.conversations.android.xmpp.model.stanza.Message received(
229            Account account, Jid to, String id) {
230        im.conversations.android.xmpp.model.stanza.Message packet =
231                new im.conversations.android.xmpp.model.stanza.Message();
232        packet.setFrom(account.getJid());
233        packet.setTo(to);
234        packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
235        packet.addExtension(new Store());
236        return packet;
237    }
238
239    public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
240            final Jid with, final String sessionId, final Reason reason) {
241        final im.conversations.android.xmpp.model.stanza.Message packet =
242                new im.conversations.android.xmpp.model.stanza.Message();
243        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
244        packet.setTo(with);
245        final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
246        finish.setAttribute("id", sessionId);
247        final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
248        reasonElement.addChild(reason.toString());
249        packet.addExtension(new Store());
250        return packet;
251    }
252
253    public im.conversations.android.xmpp.model.stanza.Message sessionProposal(
254            final JingleConnectionManager.RtpSessionProposal proposal) {
255        final im.conversations.android.xmpp.model.stanza.Message packet =
256                new im.conversations.android.xmpp.model.stanza.Message();
257        packet.setType(
258                im.conversations.android.xmpp.model.stanza.Message.Type
259                        .CHAT); // we want to carbon copy those
260        packet.setTo(proposal.with);
261        packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
262        final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
263        propose.setAttribute("id", proposal.sessionId);
264        for (final Media media : proposal.media) {
265            propose.addChild("description", Namespace.JINGLE_APPS_RTP)
266                    .setAttribute("media", media.toString());
267        }
268        packet.addChild("request", "urn:xmpp:receipts");
269        packet.addExtension(new Store());
270        return packet;
271    }
272
273    public im.conversations.android.xmpp.model.stanza.Message sessionRetract(
274            final JingleConnectionManager.RtpSessionProposal proposal) {
275        final im.conversations.android.xmpp.model.stanza.Message packet =
276                new im.conversations.android.xmpp.model.stanza.Message();
277        packet.setType(
278                im.conversations.android.xmpp.model.stanza.Message.Type
279                        .CHAT); // we want to carbon copy those
280        packet.setTo(proposal.with);
281        final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
282        propose.setAttribute("id", proposal.sessionId);
283        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
284        packet.addExtension(new Store());
285        return packet;
286    }
287
288    public im.conversations.android.xmpp.model.stanza.Message sessionReject(
289            final Jid with, final String sessionId) {
290        final im.conversations.android.xmpp.model.stanza.Message packet =
291                new im.conversations.android.xmpp.model.stanza.Message();
292        packet.setType(
293                im.conversations.android.xmpp.model.stanza.Message.Type
294                        .CHAT); // we want to carbon copy those
295        packet.setTo(with);
296        final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
297        propose.setAttribute("id", sessionId);
298        propose.addChild("description", Namespace.JINGLE_APPS_RTP);
299        packet.addExtension(new Store());
300        return packet;
301    }
302}