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			packet.addChild("markable", "urn:xmpp:chat-markers:0");
 43			if (this.mXmppConnectionService.indicateReceived()) {
 44				packet.addChild("request", "urn:xmpp:receipts");
 45			}
 46		} else if (message.getType() == Message.TYPE_PRIVATE) {
 47			packet.setTo(message.getCounterpart());
 48			packet.setType(MessagePacket.TYPE_CHAT);
 49			packet.addChild("x","http://jabber.org/protocol/muc#user");
 50			if (this.mXmppConnectionService.indicateReceived()) {
 51				packet.addChild("request", "urn:xmpp:receipts");
 52			}
 53		} else {
 54			packet.setTo(message.getCounterpart().toBareJid());
 55			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 56		}
 57		packet.setFrom(account.getJid());
 58		packet.setId(message.getUuid());
 59		packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id",message.getUuid());
 60		if (message.edited()) {
 61			packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
 62		}
 63		return packet;
 64	}
 65
 66	public void addDelay(MessagePacket packet, long timestamp) {
 67		final SimpleDateFormat mDateFormat = new SimpleDateFormat(
 68				"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 69		mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 70		Element delay = packet.addChild("delay", "urn:xmpp:delay");
 71		Date date = new Date(timestamp);
 72		delay.setAttribute("stamp", mDateFormat.format(date));
 73	}
 74
 75	public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
 76		MessagePacket packet = preparePacket(message);
 77		if (axolotlMessage == null) {
 78			return null;
 79		}
 80		packet.setAxolotlMessage(axolotlMessage.toElement());
 81		if (Config.supportUnencrypted() && !recipientSupportsOmemo(message)) {
 82			packet.setBody(OMEMO_FALLBACK_MESSAGE);
 83		}
 84		packet.addChild("store", "urn:xmpp:hints");
 85		packet.addChild("encryption","urn:xmpp:eme:0")
 86				.setAttribute("name","OMEMO")
 87				.setAttribute("namespace",AxolotlService.PEP_PREFIX);
 88		return packet;
 89	}
 90
 91	private static boolean recipientSupportsOmemo(Message message) {
 92		Contact c = message.getContact();
 93		return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
 94	}
 95
 96	public static void addMessageHints(MessagePacket packet) {
 97		packet.addChild("private", "urn:xmpp:carbons:2");
 98		packet.addChild("no-copy", "urn:xmpp:hints");
 99		packet.addChild("no-permanent-store", "urn:xmpp:hints");
100		packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
101	}
102
103	public MessagePacket generateOtrChat(Message message) {
104		Session otrSession = message.getConversation().getOtrSession();
105		if (otrSession == null) {
106			return null;
107		}
108		MessagePacket packet = preparePacket(message);
109		addMessageHints(packet);
110		try {
111			String content;
112			if (message.hasFileOnRemoteHost()) {
113				content = message.getFileParams().url.toString();
114			} else {
115				content = message.getBody();
116			}
117			packet.setBody(otrSession.transformSending(content)[0]);
118			packet.addChild("encryption","urn:xmpp:eme:0")
119					.setAttribute("namespace","urn:xmpp:otr:0");
120			return packet;
121		} catch (OtrException e) {
122			return null;
123		}
124	}
125
126	public MessagePacket generateChat(Message message) {
127		MessagePacket packet = preparePacket(message);
128		String content;
129		if (message.hasFileOnRemoteHost()) {
130			Message.FileParams fileParams = message.getFileParams();
131			content = fileParams.url.toString();
132			packet.addChild("x",Namespace.OOB).addChild("url").setContent(content);
133		} else {
134			content = message.getBody();
135		}
136		packet.setBody(content);
137		return packet;
138	}
139
140	public MessagePacket generatePgpChat(Message message) {
141		MessagePacket packet = preparePacket(message);
142		if (message.hasFileOnRemoteHost()) {
143			final String url = message.getFileParams().url.toString();
144			packet.setBody(url);
145			packet.addChild("x",Namespace.OOB).addChild("url").setContent(url);
146		} else {
147			if (Config.supportUnencrypted()) {
148				packet.setBody(PGP_FALLBACK_MESSAGE);
149			}
150			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
151				packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
152			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
153				packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
154			}
155			packet.addChild("encryption", "urn:xmpp:eme:0")
156					.setAttribute("namespace", "jabber:x:encrypted");
157		}
158		return packet;
159	}
160
161	public MessagePacket generateChatState(Conversation conversation) {
162		final Account account = conversation.getAccount();
163		MessagePacket packet = new MessagePacket();
164		packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
165		packet.setTo(conversation.getJid().toBareJid());
166		packet.setFrom(account.getJid());
167		packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
168		packet.addChild("no-store", "urn:xmpp:hints");
169		packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
170		return packet;
171	}
172
173	public MessagePacket confirm(final Account account, final Jid to, final String id) {
174		MessagePacket packet = new MessagePacket();
175		packet.setType(MessagePacket.TYPE_CHAT);
176		packet.setTo(to);
177		packet.setFrom(account.getJid());
178		Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
179		received.setAttribute("id", id);
180		packet.addChild("store", "urn:xmpp:hints");
181		return packet;
182	}
183
184	public MessagePacket conferenceSubject(Conversation conversation,String subject) {
185		MessagePacket packet = new MessagePacket();
186		packet.setType(MessagePacket.TYPE_GROUPCHAT);
187		packet.setTo(conversation.getJid().toBareJid());
188		Element subjectChild = new Element("subject");
189		subjectChild.setContent(subject);
190		packet.addChild(subjectChild);
191		packet.setFrom(conversation.getAccount().getJid().toBareJid());
192		return packet;
193	}
194
195	public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
196		MessagePacket packet = new MessagePacket();
197		packet.setType(MessagePacket.TYPE_NORMAL);
198		packet.setTo(contact);
199		packet.setFrom(conversation.getAccount().getJid());
200		Element x = packet.addChild("x", "jabber:x:conference");
201		x.setAttribute("jid", conversation.getJid().toBareJid().toString());
202		String password = conversation.getMucOptions().getPassword();
203		if (password != null) {
204			x.setAttribute("password",password);
205		}
206		return packet;
207	}
208
209	public MessagePacket invite(Conversation conversation, Jid contact) {
210		MessagePacket packet = new MessagePacket();
211		packet.setTo(conversation.getJid().toBareJid());
212		packet.setFrom(conversation.getAccount().getJid());
213		Element x = new Element("x");
214		x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
215		Element invite = new Element("invite");
216		invite.setAttribute("to", contact.toBareJid().toString());
217		x.addChild(invite);
218		packet.addChild(x);
219		return packet;
220	}
221
222	public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList<String> namespaces, int type) {
223		MessagePacket receivedPacket = new MessagePacket();
224		receivedPacket.setType(type);
225		receivedPacket.setTo(originalMessage.getFrom());
226		receivedPacket.setFrom(account.getJid());
227		for(String namespace : namespaces) {
228			receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId());
229		}
230		return receivedPacket;
231	}
232
233	public MessagePacket generateOtrError(Jid to, String id, String errorText) {
234		MessagePacket packet = new MessagePacket();
235		packet.setType(MessagePacket.TYPE_ERROR);
236		packet.setAttribute("id",id);
237		packet.setTo(to);
238		Element error = packet.addChild("error");
239		error.setAttribute("code","406");
240		error.setAttribute("type","modify");
241		error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas");
242		error.addChild("text").setContent("?OTR Error:" + errorText);
243		return packet;
244	}
245}