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","jabber:x: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 (Config.supportUnencrypted()) {
143			packet.setBody(PGP_FALLBACK_MESSAGE);
144		}
145		if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
146			packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
147		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
148			packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
149		}
150		packet.addChild("encryption","urn:xmpp:eme:0")
151				.setAttribute("namespace","jabber:x:encrypted");
152		return packet;
153	}
154
155	public MessagePacket generateChatState(Conversation conversation) {
156		final Account account = conversation.getAccount();
157		MessagePacket packet = new MessagePacket();
158		packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
159		packet.setTo(conversation.getJid().toBareJid());
160		packet.setFrom(account.getJid());
161		packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
162		packet.addChild("no-store", "urn:xmpp:hints");
163		packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
164		return packet;
165	}
166
167	public MessagePacket confirm(final Account account, final Jid to, final String id) {
168		MessagePacket packet = new MessagePacket();
169		packet.setType(MessagePacket.TYPE_CHAT);
170		packet.setTo(to);
171		packet.setFrom(account.getJid());
172		Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
173		received.setAttribute("id", id);
174		packet.addChild("store", "urn:xmpp:hints");
175		return packet;
176	}
177
178	public MessagePacket conferenceSubject(Conversation conversation,String subject) {
179		MessagePacket packet = new MessagePacket();
180		packet.setType(MessagePacket.TYPE_GROUPCHAT);
181		packet.setTo(conversation.getJid().toBareJid());
182		Element subjectChild = new Element("subject");
183		subjectChild.setContent(subject);
184		packet.addChild(subjectChild);
185		packet.setFrom(conversation.getAccount().getJid().toBareJid());
186		return packet;
187	}
188
189	public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
190		MessagePacket packet = new MessagePacket();
191		packet.setType(MessagePacket.TYPE_NORMAL);
192		packet.setTo(contact);
193		packet.setFrom(conversation.getAccount().getJid());
194		Element x = packet.addChild("x", "jabber:x:conference");
195		x.setAttribute("jid", conversation.getJid().toBareJid().toString());
196		String password = conversation.getMucOptions().getPassword();
197		if (password != null) {
198			x.setAttribute("password",password);
199		}
200		return packet;
201	}
202
203	public MessagePacket invite(Conversation conversation, Jid contact) {
204		MessagePacket packet = new MessagePacket();
205		packet.setTo(conversation.getJid().toBareJid());
206		packet.setFrom(conversation.getAccount().getJid());
207		Element x = new Element("x");
208		x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
209		Element invite = new Element("invite");
210		invite.setAttribute("to", contact.toBareJid().toString());
211		x.addChild(invite);
212		packet.addChild(x);
213		return packet;
214	}
215
216	public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList<String> namespaces, int type) {
217		MessagePacket receivedPacket = new MessagePacket();
218		receivedPacket.setType(type);
219		receivedPacket.setTo(originalMessage.getFrom());
220		receivedPacket.setFrom(account.getJid());
221		for(String namespace : namespaces) {
222			receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId());
223		}
224		return receivedPacket;
225	}
226
227	public MessagePacket generateOtrError(Jid to, String id, String errorText) {
228		MessagePacket packet = new MessagePacket();
229		packet.setType(MessagePacket.TYPE_ERROR);
230		packet.setAttribute("id",id);
231		packet.setTo(to);
232		Element error = packet.addChild("error");
233		error.setAttribute("code","406");
234		error.setAttribute("type","modify");
235		error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas");
236		error.addChild("text").setContent("?OTR Error:" + errorText);
237		return packet;
238	}
239}