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 List<String> codes = getStatusCodes(x);
88 if (type == null) {
89 if (x != null) {
90 final var item = x.getItem();
91 if (item != null && !from.isBareJid()) {
92 mucOptions.setError(MucOptions.Error.NONE);
93 final MucOptions.User user =
94 MultiUserChatManager.itemToUser(conversation, item, from);
95 final var occupant = packet.getOnlyExtension(OccupantId.class);
96 final String occupantId =
97 mucOptions.occupantId() && occupant != null
98 ? occupant.getId()
99 : null;
100 user.setOccupantId(occupantId);
101 if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)
102 || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
103 && jid.equals(
104 Jid.Invalid.getNullForInvalid(
105 item.getAttributeAsJid("jid"))))) {
106 Log.d(
107 Config.LOGTAG,
108 account.getJid().asBareJid()
109 + ": got self-presence from "
110 + user.getFullJid()
111 + ". occupant-id="
112 + occupantId);
113 if (mucOptions.setOnline()) {
114 mXmppConnectionService.getAvatarService().clear(mucOptions);
115 }
116 final var current = mucOptions.getSelf().getFullJid();
117 if (mucOptions.setSelf(user)) {
118 Log.d(Config.LOGTAG, "role or affiliation changed");
119 mXmppConnectionService.databaseBackend.updateConversation(
120 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 final var address = mucOptions.getConversation().getJid().asBareJid();
141 Log.d(
142 Config.LOGTAG,
143 account.getJid().asBareJid()
144 + ": room '"
145 + address
146 + "' created. pushing default configuration");
147 getManager(MultiUserChatManager.class)
148 .pushConfiguration(
149 conversation,
150 MultiUserChatManager.defaultChannelConfiguration());
151 }
152 if (mXmppConnectionService.getPgpEngine() != null) {
153 Element signed = packet.findChild("x", "jabber:x:signed");
154 if (signed != null) {
155 Element status = packet.findChild("status");
156 String msg = status == null ? "" : status.getContent();
157 long keyId =
158 mXmppConnectionService
159 .getPgpEngine()
160 .fetchKeyId(
161 mucOptions.getAccount(),
162 msg,
163 signed.getContent());
164 if (keyId != 0) {
165 user.setPgpKeyId(keyId);
166 }
167 }
168 }
169 if (vCardUpdate != null) {
170 getManager(AvatarManager.class).handleVCardUpdate(from, vCardUpdate);
171 }
172 }
173 }
174 } else if (type.equals("unavailable")) {
175 final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid());
176 if (x.hasChild("destroy") && fullJidMatches) {
177 Element destroy = x.findChild("destroy");
178 final Jid alternate =
179 destroy == null
180 ? null
181 : Jid.Invalid.getNullForInvalid(
182 destroy.getAttributeAsJid("jid"));
183 mucOptions.setError(MucOptions.Error.DESTROYED);
184 if (alternate != null) {
185 Log.d(
186 Config.LOGTAG,
187 account.getJid().asBareJid()
188 + ": muc destroyed. alternate location "
189 + alternate);
190 }
191 } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) {
192 mucOptions.setError(MucOptions.Error.SHUTDOWN);
193 } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) {
194 if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) {
195 final boolean wasOnline = mucOptions.online();
196 mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS);
197 Log.d(
198 Config.LOGTAG,
199 account.getJid().asBareJid()
200 + ": received status code 333 in room "
201 + mucOptions.getConversation().getJid().asBareJid()
202 + " online="
203 + wasOnline);
204 if (wasOnline) {
205 getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
206 }
207 } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
208 mucOptions.setError(MucOptions.Error.KICKED);
209 } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
210 mucOptions.setError(MucOptions.Error.BANNED);
211 } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
212 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
213 } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
214 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
215 } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
216 mucOptions.setError(MucOptions.Error.SHUTDOWN);
217 } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
218 mucOptions.setError(MucOptions.Error.UNKNOWN);
219 Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
220 }
221 } else if (!from.isBareJid()) {
222 final var item = x.getItem();
223 if (item != null) {
224 mucOptions.updateUser(
225 MultiUserChatManager.itemToUser(conversation, item, from));
226 }
227 MucOptions.User user = mucOptions.deleteUser(from);
228 if (user != null) {
229 mXmppConnectionService.getAvatarService().clear(user);
230 }
231 }
232 } else if (type.equals("error")) {
233 final Element error = packet.findChild("error");
234 if (error == null) {
235 return;
236 }
237 if (error.hasChild("conflict")) {
238 if (mucOptions.online()) {
239 invokeRenameListener(mucOptions, false);
240 } else {
241 mucOptions.setError(MucOptions.Error.NICK_IN_USE);
242 }
243 } else if (error.hasChild("not-authorized")) {
244 mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
245 } else if (error.hasChild("forbidden")) {
246 mucOptions.setError(MucOptions.Error.BANNED);
247 } else if (error.hasChild("registration-required")) {
248 mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
249 } else if (error.hasChild("resource-constraint")) {
250 mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT);
251 } else if (error.hasChild("remote-server-timeout")) {
252 mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT);
253 } else if (error.hasChild("gone")) {
254 final String gone = error.findChildContent("gone");
255 final Jid alternate;
256 if (gone != null) {
257 final XmppUri xmppUri = new XmppUri(gone);
258 if (xmppUri.isValidJid()) {
259 alternate = xmppUri.getJid();
260 } else {
261 alternate = null;
262 }
263 } else {
264 alternate = null;
265 }
266 mucOptions.setError(MucOptions.Error.DESTROYED);
267 if (alternate != null) {
268 Log.d(
269 Config.LOGTAG,
270 conversation.getAccount().getJid().asBareJid()
271 + ": muc destroyed. alternate location "
272 + alternate);
273 }
274 } else {
275 final String text = error.findChildContent("text");
276 if (text != null && text.contains("attribute 'to'")) {
277 if (mucOptions.online()) {
278 invokeRenameListener(mucOptions, false);
279 } else {
280 mucOptions.setError(MucOptions.Error.INVALID_NICK);
281 }
282 } else {
283 mucOptions.setError(MucOptions.Error.UNKNOWN);
284 Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
285 }
286 }
287 }
288 }
289 }
290
291 private static void invokeRenameListener(final MucOptions options, boolean success) {
292 if (options.onRenameListener != null) {
293 if (success) {
294 options.onRenameListener.onSuccess();
295 } else {
296 options.onRenameListener.onFailure();
297 }
298 options.onRenameListener = null;
299 }
300 }
301
302 private static List<String> getStatusCodes(Element x) {
303 List<String> codes = new ArrayList<>();
304 if (x != null) {
305 for (Element child : x.getChildren()) {
306 if (child.getName().equals("status")) {
307 String code = child.getAttribute("code");
308 if (code != null) {
309 codes.add(code);
310 }
311 }
312 }
313 }
314 return codes;
315 }
316
317 private void parseContactPresence(
318 final im.conversations.android.xmpp.model.stanza.Presence packet) {
319 final var account = getAccount();
320 final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
321 final Jid from = packet.getFrom();
322 if (from == null || from.equals(account.getJid())) {
323 return;
324 }
325 final String type = packet.getAttribute("type");
326 final Contact contact = account.getRoster().getContact(from);
327 if (type == null) {
328 final String resource = from.isBareJid() ? "" : from.getResource();
329
330 if (mXmppConnectionService.isMuc(account, from)) {
331 return;
332 }
333
334 final int sizeBefore = contact.getPresences().size();
335
336 contact.updatePresence(resource, packet);
337
338 final var nodeHash = packet.getCapabilities();
339 if (nodeHash != null) {
340 final var discoFuture =
341 this.getManager(DiscoManager.class)
342 .infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash);
343
344 logDiscoFailure(from, discoFuture);
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 this.connection.getManager(RosterManager.class).writeToDatabaseAsync();
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 getManager(DiscoManager.class).clear(from);
387 if (from.isBareJid()) {
388 contact.clearPresences();
389 } else {
390 contact.removePresence(from.getResource());
391 }
392 if (contact.getShownStatus()
393 == im.conversations.android.xmpp.model.stanza.Presence.Availability.OFFLINE) {
394 contact.flagInactive();
395 }
396 mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
397 } else if (type.equals("subscribe")) {
398 if (contact.isBlocked()) {
399 Log.d(
400 Config.LOGTAG,
401 account.getJid().asBareJid()
402 + ": ignoring 'subscribe' presence from blocked "
403 + from);
404 return;
405 }
406 if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
407 this.getManager(RosterManager.class).writeToDatabaseAsync();
408 mXmppConnectionService.getAvatarService().clear(contact);
409 }
410 if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
411 connection
412 .getManager(PresenceManager.class)
413 .subscribed(contact.getJid().asBareJid());
414 } else {
415 contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
416 final Conversation conversation =
417 mXmppConnectionService.findOrCreateConversation(
418 account, contact.getJid().asBareJid(), false, false);
419 final String statusMessage = packet.findChildContent("status");
420 if (statusMessage != null
421 && !statusMessage.isEmpty()
422 && conversation.countMessages() == 0) {
423 conversation.add(
424 new Message(
425 conversation,
426 statusMessage,
427 Message.ENCRYPTION_NONE,
428 Message.STATUS_RECEIVED));
429 }
430 }
431 }
432 mXmppConnectionService.updateRosterUi();
433 }
434
435 private static void logDiscoFailure(final Jid from, ListenableFuture<Void> discoFuture) {
436 Futures.addCallback(
437 discoFuture,
438 new FutureCallback<>() {
439 @Override
440 public void onSuccess(Void result) {}
441
442 @Override
443 public void onFailure(@NonNull Throwable throwable) {
444 if (throwable instanceof TimeoutException) {
445 return;
446 }
447 Log.d(Config.LOGTAG, "could not retrieve disco from " + from, throwable);
448 }
449 },
450 MoreExecutors.directExecutor());
451 }
452
453 @Override
454 public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
455 if (packet.hasChild("x", Namespace.MUC_USER)) {
456 this.parseConferencePresence(packet);
457 } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
458 this.parseConferencePresence(packet);
459 } else if ("error".equals(packet.getAttribute("type"))
460 && mXmppConnectionService.isMuc(getAccount(), packet.getFrom())) {
461 this.parseConferencePresence(packet);
462 } else {
463 this.parseContactPresence(packet);
464 }
465 }
466}