MessageGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import net.java.otr4j.OtrException;
  4import net.java.otr4j.session.Session;
  5
  6import java.text.SimpleDateFormat;
  7import java.util.ArrayList;
  8import java.util.Date;
  9import java.util.Locale;
 10import java.util.TimeZone;
 11
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 14import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 15import eu.siacs.conversations.entities.Account;
 16import eu.siacs.conversations.entities.Contact;
 17import eu.siacs.conversations.entities.Conversation;
 18import eu.siacs.conversations.entities.Message;
 19import eu.siacs.conversations.services.XmppConnectionService;
 20import eu.siacs.conversations.xml.Element;
 21import eu.siacs.conversations.xml.Namespace;
 22import eu.siacs.conversations.xmpp.chatstate.ChatState;
 23import eu.siacs.conversations.xmpp.jid.Jid;
 24import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 25
 26public class MessageGenerator extends AbstractGenerator {
 27	public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
 28	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";
 29	private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
 30
 31	public MessageGenerator(XmppConnectionService service) {
 32		super(service);
 33	}
 34
 35	private MessagePacket preparePacket(Message message) {
 36		Conversation conversation = message.getConversation();
 37		Account account = conversation.getAccount();
 38		MessagePacket packet = new MessagePacket();
 39		if (conversation.getMode() == Conversation.MODE_SINGLE) {
 40			packet.setTo(message.getCounterpart());
 41			packet.setType(MessagePacket.TYPE_CHAT);
 42			if (this.mXmppConnectionService.indicateReceived()) {
 43				packet.addChild("request", "urn:xmpp:receipts");
 44			}
 45		} else if (message.getType() == Message.TYPE_PRIVATE) {
 46			packet.setTo(message.getCounterpart());
 47			packet.setType(MessagePacket.TYPE_CHAT);
 48			packet.addChild("x","http://jabber.org/protocol/muc#user");
 49			if (this.mXmppConnectionService.indicateReceived()) {
 50				packet.addChild("request", "urn:xmpp:receipts");
 51			}
 52		} else {
 53			packet.setTo(message.getCounterpart().toBareJid());
 54			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 55		}
 56		if (conversation.isSingleOrPrivateAndNonAnonymous() && message.getType() != Message.TYPE_PRIVATE) {
 57			packet.addChild("markable", "urn:xmpp:chat-markers:0");
 58		}
 59		packet.setFrom(account.getJid());
 60		packet.setId(message.getUuid());
 61		packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid());
 62		if (message.edited()) {
 63			packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
 64		}
 65		return packet;
 66	}
 67
 68	public void addDelay(MessagePacket packet, long timestamp) {
 69		final SimpleDateFormat mDateFormat = new SimpleDateFormat(
 70				"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 71		mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 72		Element delay = packet.addChild("delay", "urn:xmpp:delay");
 73		Date date = new Date(timestamp);
 74		delay.setAttribute("stamp", mDateFormat.format(date));
 75	}
 76
 77	public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
 78		MessagePacket packet = preparePacket(message);
 79		if (axolotlMessage == null) {
 80			return null;
 81		}
 82		packet.setAxolotlMessage(axolotlMessage.toElement());
 83		if (Config.supportUnencrypted() && !recipientSupportsOmemo(message)) {
 84			packet.setBody(OMEMO_FALLBACK_MESSAGE);
 85		}
 86		packet.addChild("store", "urn:xmpp:hints");
 87		packet.addChild("encryption","urn:xmpp:eme:0")
 88				.setAttribute("name","OMEMO")
 89				.setAttribute("namespace",AxolotlService.PEP_PREFIX);
 90		return packet;
 91	}
 92
 93	public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
 94		MessagePacket packet = new MessagePacket();
 95		packet.setType(MessagePacket.TYPE_CHAT);
 96		packet.setTo(to);
 97		packet.setAxolotlMessage(axolotlMessage.toElement());
 98		packet.addChild("store", "urn:xmpp:hints");
 99		return packet;
100	}
101
102	private static boolean recipientSupportsOmemo(Message message) {
103		Contact c = message.getContact();
104		return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
105	}
106
107	public static void addMessageHints(MessagePacket packet) {
108		packet.addChild("private", "urn:xmpp:carbons:2");
109		packet.addChild("no-copy", "urn:xmpp:hints");
110		packet.addChild("no-permanent-store", "urn:xmpp:hints");
111		packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
112	}
113
114	public MessagePacket generateOtrChat(Message message) {
115		Session otrSession = message.getConversation().getOtrSession();
116		if (otrSession == null) {
117			return null;
118		}
119		MessagePacket packet = preparePacket(message);
120		addMessageHints(packet);
121		try {
122			String content;
123			if (message.hasFileOnRemoteHost()) {
124				content = message.getFileParams().url.toString();
125			} else {
126				content = message.getBody();
127			}
128			packet.setBody(otrSession.transformSending(content)[0]);
129			packet.addChild("encryption","urn:xmpp:eme:0")
130					.setAttribute("namespace","urn:xmpp:otr:0");
131			return packet;
132		} catch (OtrException e) {
133			return null;
134		}
135	}
136
137	public MessagePacket generateChat(Message message) {
138		MessagePacket packet = preparePacket(message);
139		String content;
140		if (message.hasFileOnRemoteHost()) {
141			Message.FileParams fileParams = message.getFileParams();
142			content = fileParams.url.toString();
143			packet.addChild("x",Namespace.OOB).addChild("url").setContent(content);
144		} else {
145			content = message.getBody();
146		}
147		packet.setBody(content);
148		return packet;
149	}
150
151	public MessagePacket generatePgpChat(Message message) {
152		MessagePacket packet = preparePacket(message);
153		if (message.hasFileOnRemoteHost()) {
154			final String url = message.getFileParams().url.toString();
155			packet.setBody(url);
156			packet.addChild("x",Namespace.OOB).addChild("url").setContent(url);
157		} else {
158			if (Config.supportUnencrypted()) {
159				packet.setBody(PGP_FALLBACK_MESSAGE);
160			}
161			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
162				packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
163			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
164				packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
165			}
166			packet.addChild("encryption", "urn:xmpp:eme:0")
167					.setAttribute("namespace", "jabber:x:encrypted");
168		}
169		return packet;
170	}
171
172	public MessagePacket generateChatState(Conversation conversation) {
173		final Account account = conversation.getAccount();
174		MessagePacket packet = new MessagePacket();
175		packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
176		packet.setTo(conversation.getJid().toBareJid());
177		packet.setFrom(account.getJid());
178		packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
179		packet.addChild("no-store", "urn:xmpp:hints");
180		packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
181		return packet;
182	}
183
184	public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) {
185		MessagePacket packet = new MessagePacket();
186		packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
187		packet.setTo(groupChat ? to.toBareJid() : to);
188		packet.setFrom(account.getJid());
189		Element displayed = packet.addChild("displayed","urn:xmpp:chat-markers:0");
190		displayed.setAttribute("id", id);
191		if (groupChat && counterpart != null) {
192			displayed.setAttribute("sender",counterpart.toPreppedString());
193		}
194		packet.addChild("store", "urn:xmpp:hints");
195		return packet;
196	}
197
198	public MessagePacket conferenceSubject(Conversation conversation,String subject) {
199		MessagePacket packet = new MessagePacket();
200		packet.setType(MessagePacket.TYPE_GROUPCHAT);
201		packet.setTo(conversation.getJid().toBareJid());
202		Element subjectChild = new Element("subject");
203		subjectChild.setContent(subject);
204		packet.addChild(subjectChild);
205		packet.setFrom(conversation.getAccount().getJid().toBareJid());
206		return packet;
207	}
208
209	public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
210		MessagePacket packet = new MessagePacket();
211		packet.setType(MessagePacket.TYPE_NORMAL);
212		packet.setTo(contact);
213		packet.setFrom(conversation.getAccount().getJid());
214		Element x = packet.addChild("x", "jabber:x:conference");
215		x.setAttribute("jid", conversation.getJid().toBareJid().toString());
216		String password = conversation.getMucOptions().getPassword();
217		if (password != null) {
218			x.setAttribute("password",password);
219		}
220		return packet;
221	}
222
223	public MessagePacket invite(Conversation conversation, Jid contact) {
224		MessagePacket packet = new MessagePacket();
225		packet.setTo(conversation.getJid().toBareJid());
226		packet.setFrom(conversation.getAccount().getJid());
227		Element x = new Element("x");
228		x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
229		Element invite = new Element("invite");
230		invite.setAttribute("to", contact.toBareJid().toString());
231		x.addChild(invite);
232		packet.addChild(x);
233		return packet;
234	}
235
236	public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList<String> namespaces, int type) {
237		MessagePacket receivedPacket = new MessagePacket();
238		receivedPacket.setType(type);
239		receivedPacket.setTo(originalMessage.getFrom());
240		receivedPacket.setFrom(account.getJid());
241		for(String namespace : namespaces) {
242			receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId());
243		}
244		receivedPacket.addChild("store", "urn:xmpp:hints");
245		return receivedPacket;
246	}
247
248	public MessagePacket received(Account account, Jid to, String id) {
249		MessagePacket packet = new MessagePacket();
250		packet.setFrom(account.getJid());
251		packet.setTo(to);
252		packet.addChild("received","urn:xmpp:receipts").setAttribute("id",id);
253		packet.addChild("store", "urn:xmpp:hints");
254		return packet;
255	}
256
257	public MessagePacket generateOtrError(Jid to, String id, String errorText) {
258		MessagePacket packet = new MessagePacket();
259		packet.setType(MessagePacket.TYPE_ERROR);
260		packet.setAttribute("id",id);
261		packet.setTo(to);
262		Element error = packet.addChild("error");
263		error.setAttribute("code","406");
264		error.setAttribute("type","modify");
265		error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas");
266		error.addChild("text").setContent("?OTR Error:" + errorText);
267		return packet;
268	}
269}