1package eu.siacs.conversations.parser;
2
3import android.util.Log;
4
5import eu.siacs.conversations.Config;
6import eu.siacs.conversations.crypto.PgpEngine;
7import eu.siacs.conversations.crypto.axolotl.AxolotlService;
8import eu.siacs.conversations.entities.Account;
9import eu.siacs.conversations.entities.Contact;
10import eu.siacs.conversations.entities.Conversation;
11import eu.siacs.conversations.entities.Message;
12import eu.siacs.conversations.entities.MucOptions;
13import eu.siacs.conversations.entities.Presence;
14import eu.siacs.conversations.generator.IqGenerator;
15import eu.siacs.conversations.generator.PresenceGenerator;
16import eu.siacs.conversations.services.XmppConnectionService;
17import eu.siacs.conversations.utils.XmppUri;
18import eu.siacs.conversations.xml.Element;
19import eu.siacs.conversations.xml.Namespace;
20import eu.siacs.conversations.xmpp.InvalidJid;
21import eu.siacs.conversations.xmpp.Jid;
22import eu.siacs.conversations.xmpp.pep.Avatar;
23
24import org.openintents.openpgp.util.OpenPgpUtils;
25
26import java.util.ArrayList;
27import java.util.List;
28import java.util.function.Consumer;
29
30public class PresenceParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Presence> {
31
32 public PresenceParser(final XmppConnectionService service, final Account account) {
33 super(service, account);
34 }
35
36 public void parseConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) {
37 final Conversation conversation =
38 packet.getFrom() == null
39 ? null
40 : mXmppConnectionService.find(account, packet.getFrom().asBareJid());
41 if (conversation != null) {
42 final MucOptions mucOptions = conversation.getMucOptions();
43 boolean before = mucOptions.online();
44 int count = mucOptions.getUserCount();
45 final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
46 processConferencePresence(packet, conversation);
47 final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
48 if (!tileUserAfter.equals(tileUserBefore)) {
49 mXmppConnectionService.getAvatarService().clear(mucOptions);
50 }
51 if (before != mucOptions.online()
52 || (mucOptions.online() && count != mucOptions.getUserCount())) {
53 mXmppConnectionService.updateConversationUi();
54 } else if (mucOptions.online()) {
55 mXmppConnectionService.updateMucRosterUi();
56 }
57 }
58 }
59
60 private void processConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Conversation conversation) {
61 final Account account = conversation.getAccount();
62 final MucOptions mucOptions = conversation.getMucOptions();
63 final Jid jid = conversation.getAccount().getJid();
64 final Jid from = packet.getFrom();
65 if (!from.isBareJid()) {
66 final String type = packet.getAttribute("type");
67 final Element x = packet.findChild("x", Namespace.MUC_USER);
68 Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
69 final List<String> codes = getStatusCodes(x);
70 if (type == null) {
71 if (x != null) {
72 Element item = x.findChild("item");
73 if (item != null && !from.isBareJid()) {
74 mucOptions.setError(MucOptions.Error.NONE);
75 MucOptions.User user = parseItem(conversation, item, from);
76 if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)
77 || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
78 && jid.equals(
79 InvalidJid.getNullForInvalid(
80 item.getAttributeAsJid("jid"))))) {
81 if (mucOptions.setOnline()) {
82 mXmppConnectionService.getAvatarService().clear(mucOptions);
83 }
84 if (mucOptions.setSelf(user)) {
85 Log.d(Config.LOGTAG, "role or affiliation changed");
86 mXmppConnectionService.databaseBackend.updateConversation(
87 conversation);
88 }
89
90 mXmppConnectionService.persistSelfNick(user);
91 invokeRenameListener(mucOptions, true);
92 }
93 boolean isNew = mucOptions.updateUser(user);
94 final AxolotlService axolotlService =
95 conversation.getAccount().getAxolotlService();
96 Contact contact = user.getContact();
97 if (isNew
98 && user.getRealJid() != null
99 && mucOptions.isPrivateAndNonAnonymous()
100 && (contact == null || !contact.mutualPresenceSubscription())
101 && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
102 axolotlService.fetchDeviceIds(user.getRealJid());
103 }
104 if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
105 && mucOptions.autoPushConfiguration()) {
106 Log.d(
107 Config.LOGTAG,
108 account.getJid().asBareJid()
109 + ": room '"
110 + mucOptions.getConversation().getJid().asBareJid()
111 + "' created. pushing default configuration");
112 mXmppConnectionService.pushConferenceConfiguration(
113 mucOptions.getConversation(),
114 IqGenerator.defaultChannelConfiguration(),
115 null);
116 }
117 if (mXmppConnectionService.getPgpEngine() != null) {
118 Element signed = packet.findChild("x", "jabber:x:signed");
119 if (signed != null) {
120 Element status = packet.findChild("status");
121 String msg = status == null ? "" : status.getContent();
122 long keyId =
123 mXmppConnectionService
124 .getPgpEngine()
125 .fetchKeyId(
126 mucOptions.getAccount(),
127 msg,
128 signed.getContent());
129 if (keyId != 0) {
130 user.setPgpKeyId(keyId);
131 }
132 }
133 }
134 if (avatar != null) {
135 avatar.owner = from;
136 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
137 if (user.setAvatar(avatar)) {
138 mXmppConnectionService.getAvatarService().clear(user);
139 }
140 if (user.getRealJid() != null) {
141 final Contact c =
142 conversation
143 .getAccount()
144 .getRoster()
145 .getContact(user.getRealJid());
146 c.setAvatar(avatar);
147 mXmppConnectionService.syncRoster(conversation.getAccount());
148 mXmppConnectionService.getAvatarService().clear(c);
149 mXmppConnectionService.updateRosterUi();
150 }
151 } else if (mXmppConnectionService.isDataSaverDisabled()) {
152 mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
153 }
154 }
155 }
156 }
157 } else if (type.equals("unavailable")) {
158 final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid());
159 if (x.hasChild("destroy") && fullJidMatches) {
160 Element destroy = x.findChild("destroy");
161 final Jid alternate =
162 destroy == null
163 ? null
164 : InvalidJid.getNullForInvalid(
165 destroy.getAttributeAsJid("jid"));
166 mucOptions.setError(MucOptions.Error.DESTROYED);
167 if (alternate != null) {
168 Log.d(
169 Config.LOGTAG,
170 account.getJid().asBareJid()
171 + ": muc destroyed. alternate location "
172 + alternate);
173 }
174 } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) {
175 mucOptions.setError(MucOptions.Error.SHUTDOWN);
176 } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) {
177 if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) {
178 final boolean wasOnline = mucOptions.online();
179 mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS);
180 Log.d(
181 Config.LOGTAG,
182 account.getJid().asBareJid()
183 + ": received status code 333 in room "
184 + mucOptions.getConversation().getJid().asBareJid()
185 + " online="
186 + wasOnline);
187 if (wasOnline) {
188 mXmppConnectionService.mucSelfPingAndRejoin(conversation);
189 }
190 } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
191 mucOptions.setError(MucOptions.Error.KICKED);
192 } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
193 mucOptions.setError(MucOptions.Error.BANNED);
194 } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
195 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
196 } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
197 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
198 } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
199 mucOptions.setError(MucOptions.Error.SHUTDOWN);
200 } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
201 mucOptions.setError(MucOptions.Error.UNKNOWN);
202 Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
203 }
204 } else if (!from.isBareJid()) {
205 Element item = x.findChild("item");
206 if (item != null) {
207 mucOptions.updateUser(parseItem(conversation, item, from));
208 }
209 MucOptions.User user = mucOptions.deleteUser(from);
210 if (user != null) {
211 mXmppConnectionService.getAvatarService().clear(user);
212 }
213 }
214 } else if (type.equals("error")) {
215 final Element error = packet.findChild("error");
216 if (error == null) {
217 return;
218 }
219 if (error.hasChild("conflict")) {
220 if (mucOptions.online()) {
221 invokeRenameListener(mucOptions, false);
222 } else {
223 mucOptions.setError(MucOptions.Error.NICK_IN_USE);
224 }
225 } else if (error.hasChild("not-authorized")) {
226 mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
227 } else if (error.hasChild("forbidden")) {
228 mucOptions.setError(MucOptions.Error.BANNED);
229 } else if (error.hasChild("registration-required")) {
230 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
231 } else if (error.hasChild("resource-constraint")) {
232 mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT);
233 } else if (error.hasChild("remote-server-timeout")) {
234 mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT);
235 } else if (error.hasChild("gone")) {
236 final String gone = error.findChildContent("gone");
237 final Jid alternate;
238 if (gone != null) {
239 final XmppUri xmppUri = new XmppUri(gone);
240 if (xmppUri.isValidJid()) {
241 alternate = xmppUri.getJid();
242 } else {
243 alternate = null;
244 }
245 } else {
246 alternate = null;
247 }
248 mucOptions.setError(MucOptions.Error.DESTROYED);
249 if (alternate != null) {
250 Log.d(
251 Config.LOGTAG,
252 conversation.getAccount().getJid().asBareJid()
253 + ": muc destroyed. alternate location "
254 + alternate);
255 }
256 } else {
257 final String text = error.findChildContent("text");
258 if (text != null && text.contains("attribute 'to'")) {
259 if (mucOptions.online()) {
260 invokeRenameListener(mucOptions, false);
261 } else {
262 mucOptions.setError(MucOptions.Error.INVALID_NICK);
263 }
264 } else {
265 mucOptions.setError(MucOptions.Error.UNKNOWN);
266 Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
267 }
268 }
269 }
270 }
271 }
272
273 private static void invokeRenameListener(final MucOptions options, boolean success) {
274 if (options.onRenameListener != null) {
275 if (success) {
276 options.onRenameListener.onSuccess();
277 } else {
278 options.onRenameListener.onFailure();
279 }
280 options.onRenameListener = null;
281 }
282 }
283
284 private static List<String> getStatusCodes(Element x) {
285 List<String> codes = new ArrayList<>();
286 if (x != null) {
287 for (Element child : x.getChildren()) {
288 if (child.getName().equals("status")) {
289 String code = child.getAttribute("code");
290 if (code != null) {
291 codes.add(code);
292 }
293 }
294 }
295 }
296 return codes;
297 }
298
299 private void parseContactPresence(final im.conversations.android.xmpp.model.stanza.Presence packet, final Account account) {
300 final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
301 final Jid from = packet.getFrom();
302 if (from == null || from.equals(account.getJid())) {
303 return;
304 }
305 final String type = packet.getAttribute("type");
306 final Contact contact = account.getRoster().getContact(from);
307 if (type == null) {
308 final String resource = from.isBareJid() ? "" : from.getResource();
309 final Avatar avatar =
310 Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
311 if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
312 avatar.owner = from.asBareJid();
313 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
314 if (avatar.owner.equals(account.getJid().asBareJid())) {
315 account.setAvatar(avatar.getFilename());
316 mXmppConnectionService.databaseBackend.updateAccount(account);
317 mXmppConnectionService.getAvatarService().clear(account);
318 mXmppConnectionService.updateConversationUi();
319 mXmppConnectionService.updateAccountUi();
320 } else {
321 contact.setAvatar(avatar);
322 mXmppConnectionService.syncRoster(account);
323 mXmppConnectionService.getAvatarService().clear(contact);
324 mXmppConnectionService.updateConversationUi();
325 mXmppConnectionService.updateRosterUi();
326 }
327 } else if (mXmppConnectionService.isDataSaverDisabled()) {
328 mXmppConnectionService.fetchAvatar(account, avatar);
329 }
330 }
331
332 if (mXmppConnectionService.isMuc(account, from)) {
333 return;
334 }
335
336 final int sizeBefore = contact.getPresences().size();
337
338 final String show = packet.findChildContent("show");
339 final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
340 final String message = packet.findChildContent("status");
341 final Presence presence = Presence.parse(show, caps, message);
342 contact.updatePresence(resource, presence);
343 if (presence.hasCaps()) {
344 mXmppConnectionService.fetchCaps(account, from, presence);
345 }
346
347 final Element idle = packet.findChild("idle", Namespace.IDLE);
348 if (idle != null) {
349 try {
350 final String since = idle.getAttribute("since");
351 contact.setLastseen(AbstractParser.parseTimestamp(since));
352 contact.flagInactive();
353 } catch (Throwable throwable) {
354 if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
355 contact.flagActive();
356 }
357 }
358 } else {
359 if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
360 contact.flagActive();
361 }
362 }
363
364 final PgpEngine pgp = mXmppConnectionService.getPgpEngine();
365 final Element x = packet.findChild("x", "jabber:x:signed");
366 if (pgp != null && x != null) {
367 final String status = packet.findChildContent("status");
368 final long keyId = pgp.fetchKeyId(account, status, x.getContent());
369 if (keyId != 0 && contact.setPgpKeyId(keyId)) {
370 Log.d(
371 Config.LOGTAG,
372 account.getJid().asBareJid()
373 + ": found OpenPGP key id for "
374 + contact.getJid()
375 + " "
376 + OpenPgpUtils.convertKeyIdToHex(keyId));
377 mXmppConnectionService.syncRoster(account);
378 }
379 }
380 boolean online = sizeBefore < contact.getPresences().size();
381 mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
382 } else if (type.equals("unavailable")) {
383 if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) {
384 contact.flagInactive();
385 }
386 if (from.isBareJid()) {
387 contact.clearPresences();
388 } else {
389 contact.removePresence(from.getResource());
390 }
391 if (contact.getShownStatus() == Presence.Status.OFFLINE) {
392 contact.flagInactive();
393 }
394 mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
395 } else if (type.equals("subscribe")) {
396 if (contact.isBlocked()) {
397 Log.d(
398 Config.LOGTAG,
399 account.getJid().asBareJid()
400 + ": ignoring 'subscribe' presence from blocked "
401 + from);
402 return;
403 }
404 if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
405 mXmppConnectionService.syncRoster(account);
406 mXmppConnectionService.getAvatarService().clear(contact);
407 }
408 if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
409 mXmppConnectionService.sendPresencePacket(
410 account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
411 } else {
412 contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
413 final Conversation conversation =
414 mXmppConnectionService.findOrCreateConversation(
415 account, contact.getJid().asBareJid(), false, false);
416 final String statusMessage = packet.findChildContent("status");
417 if (statusMessage != null
418 && !statusMessage.isEmpty()
419 && conversation.countMessages() == 0) {
420 conversation.add(
421 new Message(
422 conversation,
423 statusMessage,
424 Message.ENCRYPTION_NONE,
425 Message.STATUS_RECEIVED));
426 }
427 }
428 }
429 mXmppConnectionService.updateRosterUi();
430 }
431
432 @Override
433 public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
434 if (packet.hasChild("x", Namespace.MUC_USER)) {
435 this.parseConferencePresence(packet, account);
436 } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
437 this.parseConferencePresence(packet, account);
438 } else if ("error".equals(packet.getAttribute("type"))
439 && mXmppConnectionService.isMuc(account, packet.getFrom())) {
440 this.parseConferencePresence(packet, account);
441 } else {
442 this.parseContactPresence(packet, account);
443 }
444 }
445}