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