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