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