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