From 5c50dd1ed73c7c0ff3f1d5c45a0a042dfa1ac25d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 27 Jun 2025 19:03:03 +0200 Subject: [PATCH] move most MUC handling into Manager --- .../java/de/gultsch/common/FutureMerger.java | 42 + src/main/java/de/gultsch/common/IntMap.java | 100 ++ .../siacs/conversations/entities/Account.java | 4 - .../conversations/entities/Bookmark.java | 32 +- .../conversations/entities/MucOptions.java | 174 +-- .../conversations/generator/IqGenerator.java | 77 +- .../generator/MessageGenerator.java | 67 +- .../conversations/parser/AbstractParser.java | 6 +- .../conversations/parser/MessageParser.java | 94 +- .../conversations/parser/PresenceParser.java | 28 +- .../services/ChannelDiscoveryService.java | 9 +- .../services/MessageArchiveService.java | 13 +- .../services/XmppConnectionService.java | 1162 ++--------------- .../ui/ChannelDiscoveryActivity.java | 5 +- .../ui/ConferenceDetailsActivity.java | 212 +-- .../ui/ConversationFragment.java | 5 +- .../ui/CreatePublicChannelDialog.java | 8 +- .../conversations/ui/adapter/UserAdapter.java | 173 ++- .../ui/adapter/UserPreviewAdapter.java | 5 +- .../ui/util/MucConfiguration.java | 18 +- .../ui/util/MucDetailsContextMenuHelper.java | 70 +- .../siacs/conversations/utils/Resolver.java | 9 +- .../eu/siacs/conversations/xml/Element.java | 11 + .../eu/siacs/conversations/xml/Namespace.java | 6 +- .../conversations/xmpp/IqErrorException.java | 8 + .../eu/siacs/conversations/xmpp/Managers.java | 2 + .../conversations/xmpp/XmppConnection.java | 32 +- .../xmpp/manager/AbstractBookmarkManager.java | 2 +- .../xmpp/manager/BookmarkManager.java | 246 ++-- .../xmpp/manager/DiscoManager.java | 2 +- .../xmpp/manager/LegacyBookmarkManager.java | 2 +- .../xmpp/manager/MultiUserChatManager.java | 974 ++++++++++++++ .../xmpp/manager/NativeBookmarkManager.java | 172 +++ .../xmpp/manager/PingManager.java | 8 + .../xmpp/manager/PresenceManager.java | 9 +- .../xmpp/manager/PubSubManager.java | 6 +- .../xmpp/manager/VCardManager.java | 3 + .../xmpp/model/conference/DirectInvite.java | 21 + .../xmpp/model/conference/package-info.java | 5 + .../android/xmpp/model/data/Data.java | 29 +- .../android/xmpp/model/data/Field.java | 1 + .../android/xmpp/model/hints/NoCopy.java | 11 + .../android/xmpp/model/hints/NoStore.java | 12 + .../android/xmpp/model/jabber/Subject.java | 5 + .../android/xmpp/model/muc/History.java | 5 + .../android/xmpp/model/muc/Item.java | 54 + .../android/xmpp/model/muc/Password.java | 17 + .../android/xmpp/model/muc/admin/Item.java | 30 + .../xmpp/model/muc/admin/MucAdmin.java | 17 + .../xmpp/model/muc/admin/package-info.java | 5 + .../android/xmpp/model/muc/owner/Destroy.java | 12 + .../xmpp/model/muc/owner/MucOwner.java | 16 + .../xmpp/model/muc/owner/package-info.java | 5 + .../android/xmpp/model/muc/user/Invite.java | 17 + .../android/xmpp/model/muc/user/Item.java | 49 +- .../android/xmpp/model/pgp/Signed.java | 9 +- .../android/xmpp/model/stanza/Iq.java | 17 - .../xmpp/processor/AccountStateProcessor.java | 38 +- .../android/xmpp/processor/BindProcessor.java | 21 +- src/main/res/values/strings.xml | 2 +- 60 files changed, 2312 insertions(+), 1882 deletions(-) create mode 100644 src/main/java/de/gultsch/common/FutureMerger.java create mode 100644 src/main/java/de/gultsch/common/IntMap.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/NativeBookmarkManager.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/conference/DirectInvite.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/conference/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/hints/NoCopy.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/hints/NoStore.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/Item.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/Password.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/admin/Item.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/admin/MucAdmin.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/admin/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/owner/Destroy.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/owner/MucOwner.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/owner/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/muc/user/Invite.java diff --git a/src/main/java/de/gultsch/common/FutureMerger.java b/src/main/java/de/gultsch/common/FutureMerger.java new file mode 100644 index 0000000000000000000000000000000000000000..ea108acbb6644063becfa059387eb20f68fa3274 --- /dev/null +++ b/src/main/java/de/gultsch/common/FutureMerger.java @@ -0,0 +1,42 @@ +package de.gultsch.common; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Collection; +import java.util.List; + +public class FutureMerger { + + public static ListenableFuture> successfulAsList( + final Collection>> futures) { + return Futures.transform( + Futures.successfulAsList(futures), + lists -> { + final var builder = new ImmutableList.Builder(); + for (final Collection list : lists) { + if (list == null) { + continue; + } + builder.addAll(list); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); + } + + public static ListenableFuture> allAsList( + final Collection>> futures) { + return Futures.transform( + Futures.allAsList(futures), + lists -> { + final var builder = new ImmutableList.Builder(); + for (final Collection list : lists) { + builder.addAll(list); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/de/gultsch/common/IntMap.java b/src/main/java/de/gultsch/common/IntMap.java new file mode 100644 index 0000000000000000000000000000000000000000..a7b679abfef8cc385a34414829b2a93255168a3d --- /dev/null +++ b/src/main/java/de/gultsch/common/IntMap.java @@ -0,0 +1,100 @@ +package de.gultsch.common; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class IntMap implements Map { + + private final ImmutableMap inner; + + public IntMap(ImmutableMap inner) { + this.inner = inner; + } + + @Override + public int size() { + return this.inner.size(); + } + + @Override + public boolean isEmpty() { + return this.inner.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object key) { + return this.inner.containsKey(key); + } + + @Override + public boolean containsValue(@Nullable Object value) { + return this.inner.containsValue(value); + } + + @Nullable + @Override + public Integer get(@Nullable Object key) { + return this.inner.get(key); + } + + public int getInt(@Nullable E key) { + final var value = this.inner.get(key); + return value == null ? Integer.MIN_VALUE : value; + } + + @Nullable + @Override + public Integer put(E key, Integer value) { + return this.inner.put(key, value); + } + + @Nullable + @Override + public Integer remove(@Nullable Object key) { + return this.inner.remove(key); + } + + @Override + public void putAll(@NonNull Map m) { + this.inner.putAll(m); + } + + @Override + public void clear() { + this.inner.clear(); + } + + @NonNull + @Override + public Set keySet() { + return this.inner.keySet(); + } + + @NonNull + @Override + public Collection values() { + return this.inner.values(); + } + + @NonNull + @Override + public Set> entrySet() { + return this.inner.entrySet(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IntMap intMap)) return false; + return Objects.equal(inner, intMap.inner); + } + + @Override + public int hashCode() { + return Objects.hashCode(inner); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index f919cf347850f5fbebdbb5f06edd8a44de3579f2..b0dbae4458b330c5c01957cb2f4104af39c019ae 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -80,10 +80,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String KEY_SOS_URL = "sos_url"; public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; protected final JSONObject keys; - public final Set pendingConferenceJoins = new HashSet<>(); - public final Set pendingConferenceLeaves = new HashSet<>(); - public final Set inProgressConferenceJoins = new HashSet<>(); - public final Set inProgressConferencePings = new HashSet<>(); protected Jid jid; protected String password; protected int options = 0; diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 934c10a2d48b73fda974181a46f6db4fe50c00bb..a75f78b8bdab8a4d14840f3561062907dbf79cc5 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -10,7 +10,6 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.model.bookmark.Storage; -import im.conversations.android.xmpp.model.bookmark2.Conference; import im.conversations.android.xmpp.model.bookmark2.Extensions; import java.lang.ref.WeakReference; import java.util.Collections; @@ -33,7 +32,7 @@ public class Bookmark extends Element implements ListItem { this.account = account; } - private Bookmark(Account account) { + public Bookmark(Account account) { super("conference"); this.account = account; } @@ -72,31 +71,6 @@ public class Bookmark extends Element implements ListItem { return bookmark; } - public static Bookmark parseFromItem( - final String id, final Conference conference, final Account account) { - if (id == null || conference == null) { - return null; - } - final Bookmark bookmark = new Bookmark(account); - bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id)); - // TODO verify that we only use bare jids and ignore full jids - if (bookmark.jid == null) { - return null; - } - - // TODO use proper API - - bookmark.setBookmarkName(conference.getAttribute("name")); - bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); - bookmark.setNick(conference.findChildContent("nick")); - bookmark.setPassword(conference.findChildContent("password")); - final var extensions = conference.getExtensions(); - if (extensions != null) { - bookmark.extensions = conference.getExtensions(); - } - return bookmark; - } - public Extensions getExtensions() { return extensions; } @@ -260,4 +234,8 @@ public class Bookmark extends Element implements ListItem { public String getAvatarName() { return getDisplayName(); } + + public void setExtensions(Extensions extensions) { + this.extensions = extensions; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index f559bf78e49b4f067e6166d4ac0a00e4e6bcda91..4005981cfbe5b1f6f66c97fb6823ddea4babbbbe 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -1,12 +1,12 @@ package eu.siacs.conversations.entities; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import de.gultsch.common.IntMap; import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.utils.JidHelper; @@ -17,18 +17,39 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.data.Field; import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Item; +import im.conversations.android.xmpp.model.muc.Role; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.Set; public class MucOptions { + private static final IntMap AFFILIATION_RANKS = + new IntMap<>( + new ImmutableMap.Builder() + .put(Affiliation.OWNER, 4) + .put(Affiliation.ADMIN, 3) + .put(Affiliation.MEMBER, 2) + .put(Affiliation.NONE, 1) + .put(Affiliation.OUTCAST, 0) + .build()); + + private static final IntMap ROLE_RANKS = + new IntMap<>( + new ImmutableMap.Builder() + .put(Role.MODERATOR, 3) + .put(Role.PARTICIPANT, 2) + .put(Role.VISITOR, 1) + .put(Role.NONE, 0) + .build()); + public static final String STATUS_CODE_SELF_PRESENCE = "110"; public static final String STATUS_CODE_ROOM_CREATED = "201"; public static final String STATUS_CODE_BANNED = "301"; @@ -38,6 +59,7 @@ public class MucOptions { public static final String STATUS_CODE_LOST_MEMBERSHIP = "322"; public static final String STATUS_CODE_SHUTDOWN = "332"; public static final String STATUS_CODE_TECHNICAL_REASONS = "333"; + // TODO this should be a list private final Set users = new HashSet<>(); private final Conversation conversation; public OnRenameListener onRenameListener = null; @@ -53,8 +75,8 @@ public class MucOptions { this.account = conversation.getAccount(); this.conversation = conversation; this.self = new User(this, createJoinJid(getProposedNick())); - this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation")); - this.self.role = Role.of(conversation.getAttribute("role")); + this.self.affiliation = Item.affiliationOrNone(conversation.getAttribute("affiliation")); + this.self.role = Item.roleOrNone(conversation.getAttribute("role")); } public Account getAccount() { @@ -74,7 +96,8 @@ public class MucOptions { synchronized (users) { if (user != null && user.getRole() == Role.NONE) { users.remove(user); - if (affiliation.ranks(Affiliation.MEMBER)) { + if (AFFILIATION_RANKS.getInt(affiliation) + >= AFFILIATION_RANKS.getInt(Affiliation.MEMBER)) { user.affiliation = affiliation; users.add(user); } @@ -82,8 +105,8 @@ public class MucOptions { } } - public void flagNoAutoPushConfiguration() { - mAutoPushConfiguration = false; + public void setAutoPushConfiguration(final boolean auto) { + this.mAutoPushConfiguration = auto; } public boolean autoPushConfiguration() { @@ -176,7 +199,7 @@ public class MucOptions { public boolean canInvite() { final boolean hasPermission = - !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); + !membersOnly() || self.ranks(Role.MODERATOR) || allowInvites(); return hasPermission && online(); } @@ -190,7 +213,7 @@ public class MucOptions { } public boolean canChangeSubject() { - return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject(); + return self.ranks(Role.MODERATOR) || participantsCanChangeSubject(); } public boolean participantsCanChangeSubject() { @@ -216,9 +239,9 @@ public class MucOptions { if ("anyone".equals(field.getValue())) { return true; } else if ("participants".equals(field.getValue())) { - return self.getRole().ranks(Role.PARTICIPANT); + return self.ranks(Role.PARTICIPANT); } else if ("moderators".equals(field.getValue())) { - return self.getRole().ranks(Role.MODERATOR); + return self.ranks(Role.MODERATOR); } else { return false; } @@ -232,7 +255,7 @@ public class MucOptions { } public boolean participating() { - return self.getRole().ranks(Role.PARTICIPANT) || !moderated(); + return self.ranks(Role.PARTICIPANT) || !moderated(); } public boolean membersOnly() { @@ -283,7 +306,7 @@ public class MucOptions { user.realJid != null && user.realJid.equals(account.getJid().asBareJid()); if (membersOnly() && nonanonymous() - && user.affiliation.ranks(Affiliation.MEMBER) + && user.ranks(Affiliation.MEMBER) && user.realJid != null && !realJidInMuc && !self) { @@ -332,8 +355,8 @@ public class MucOptions { isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid()); - if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER)) - && user.getAffiliation().outranks(Affiliation.OUTCAST) + if ((!membersOnly() || user.ranks(Affiliation.MEMBER)) + && user.outranks(Affiliation.OUTCAST) && !fullJidIsSelf) { this.users.add(user); return !realJidFound && user.realJid != null; @@ -446,8 +469,7 @@ public class MucOptions { synchronized (users) { ArrayList users = new ArrayList<>(); for (User user : this.users) { - if (!user.isDomain() - && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) { + if (!user.isDomain() && (includeOffline || user.ranks(Role.PARTICIPANT))) { users.add(user); } } @@ -725,7 +747,7 @@ public class MucOptions { ArrayList members = new ArrayList<>(); synchronized (users) { for (User user : users) { - if (user.affiliation.ranks(Affiliation.MEMBER) + if (user.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid .asBareJid() @@ -738,90 +760,6 @@ public class MucOptions { return members; } - public enum Affiliation { - OWNER(4, R.string.owner), - ADMIN(3, R.string.admin), - MEMBER(2, R.string.member), - OUTCAST(0, R.string.outcast), - NONE(1, R.string.no_affiliation); - - private final int resId; - private final int rank; - - Affiliation(int rank, int resId) { - this.resId = resId; - this.rank = rank; - } - - public static Affiliation of(@Nullable String value) { - if (value == null) { - return NONE; - } - try { - return Affiliation.valueOf(value.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - return NONE; - } - } - - public int getResId() { - return resId; - } - - @Override - @NonNull - public String toString() { - return name().toLowerCase(Locale.US); - } - - public boolean outranks(Affiliation affiliation) { - return rank > affiliation.rank; - } - - public boolean ranks(Affiliation affiliation) { - return rank >= affiliation.rank; - } - } - - public enum Role { - MODERATOR(R.string.moderator, 3), - VISITOR(R.string.visitor, 1), - PARTICIPANT(R.string.participant, 2), - NONE(R.string.no_role, 0); - - private final int resId; - private final int rank; - - Role(int resId, int rank) { - this.resId = resId; - this.rank = rank; - } - - public static Role of(@Nullable String value) { - if (value == null) { - return NONE; - } - try { - return Role.valueOf(value.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - return NONE; - } - } - - public int getResId() { - return resId; - } - - @Override - public String toString() { - return name().toLowerCase(Locale.US); - } - - public boolean ranks(Role role) { - return rank >= role.rank; - } - } - public enum Error { NO_RESPONSE, SERVER_NOT_FOUND, @@ -873,16 +811,16 @@ public class MucOptions { return this.role; } - public void setRole(String role) { - this.role = Role.of(role); + public void setRole(final Role role) { + this.role = role; } public Affiliation getAffiliation() { return this.affiliation; } - public void setAffiliation(String affiliation) { - this.affiliation = Affiliation.of(affiliation); + public void setAffiliation(final Affiliation affiliation) { + this.affiliation = affiliation; } public long getPgpKeyId() { @@ -941,6 +879,10 @@ public class MucOptions { return options.getAccount(); } + public MucOptions getMucOptions() { + return this.options; + } + public Conversation getConversation() { return options.getConversation(); } @@ -992,9 +934,9 @@ public class MucOptions { @Override public int compareTo(@NonNull User another) { - if (another.getAffiliation().outranks(getAffiliation())) { + if (another.outranks(getAffiliation())) { return 1; - } else if (getAffiliation().outranks(another.getAffiliation())) { + } else if (outranks(another.getAffiliation())) { return -1; } else { return getComparableName().compareToIgnoreCase(another.getComparableName()); @@ -1045,5 +987,19 @@ public class MucOptions { public String getOccupantId() { return this.occupantId; } + + public boolean ranks(final Role role) { + return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role); + } + + public boolean ranks(final Affiliation affiliation) { + return AFFILIATION_RANKS.getInt(this.affiliation) + >= AFFILIATION_RANKS.getInt(affiliation); + } + + public boolean outranks(final Affiliation affiliation) { + return AFFILIATION_RANKS.getInt(this.affiliation) + > AFFILIATION_RANKS.getInt(affiliation); + } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 7960b5f495655158411420d173090f78b9ce81a5..3ae3eed8e85755f15113c3a41db2030b6b9207aa 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -5,7 +5,6 @@ import android.util.Base64; import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; @@ -15,8 +14,6 @@ import eu.siacs.conversations.xmpp.forms.Data; import im.conversations.android.xmpp.model.stanza.Iq; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -57,13 +54,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq deleteNode(final String node) { - final var packet = new Iq(Iq.Type.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); - pubsub.addChild("delete").setAttribute("node", node); - return packet; - } - public Iq retrieveDeviceIds(final Jid to) { final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if (to != null) { @@ -152,7 +142,7 @@ public class IqGenerator extends AbstractGenerator { public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final Iq packet = new Iq(Iq.Type.SET); - final Element query = packet.query(mam.version.namespace); + final Element query = packet.addChild("query", mam.version.namespace); query.setAttribute("queryid", mam.getQueryId()); final Data data = new Data(); data.setFormType(mam.version.namespace); @@ -181,35 +171,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) { - List jids = new ArrayList<>(); - jids.add(jid); - return changeAffiliation(conference, jids, affiliation); - } - - public Iq changeAffiliation(Conversation conference, List jids, String affiliation) { - final Iq packet = new Iq(Iq.Type.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - for (Jid jid : jids) { - Element item = query.addChild("item"); - item.setAttribute("jid", jid); - item.setAttribute("affiliation", affiliation); - } - return packet; - } - - public Iq changeRole(Conversation conference, String nick, String role) { - final Iq packet = new Iq(Iq.Type.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); - item.setAttribute("nick", nick); - item.setAttribute("role", role); - return packet; - } - public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) { return pushTokenToAppServer(appServer, token, deviceId, null); } @@ -268,42 +229,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq queryAffiliation(Conversation conversation, String affiliation) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(conversation.getJid().asBareJid()); - packet.query("http://jabber.org/protocol/muc#admin") - .addChild("item") - .setAttribute("affiliation", affiliation); - return packet; - } - - public static Bundle defaultGroupChatConfiguration() { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "1"); - options.putString("muc#roomconfig_publicroom", "0"); - options.putString("muc#roomconfig_whois", "anyone"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_allowinvites", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); // prosody - options.putString("mam", "1"); // ejabberd community - options.putString("muc#roomconfig_mam", "1"); // ejabberd saas - return options; - } - - public static Bundle defaultChannelConfiguration() { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "0"); - options.putString("muc#roomconfig_publicroom", "1"); - options.putString("muc#roomconfig_whois", "moderators"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); // prosody - options.putString("mam", "1"); // ejabberd community - options.putString("muc#roomconfig_mam", "1"); // ejabberd saas - return options; - } - public Iq requestPubsubConfiguration(Jid jid, String node) { return pubsubConfiguration(jid, node, null); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index d8f71a0b4f1836f6bdbfe5fbd3385e43d99fe366..bd12d1b7d4a9afaae18ffd1255f79c289afe85a4 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -17,6 +17,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import im.conversations.android.xmpp.model.correction.Replace; +import im.conversations.android.xmpp.model.hints.NoStore; import im.conversations.android.xmpp.model.hints.Store; import im.conversations.android.xmpp.model.reactions.Reaction; import im.conversations.android.xmpp.model.reactions.Reactions; @@ -95,7 +96,7 @@ public class MessageGenerator extends AbstractGenerator { } packet.setAxolotlMessage(axolotlMessage.toElement()); packet.setBody(OMEMO_FALLBACK_MESSAGE); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); packet.addChild("encryption", "urn:xmpp:eme:0") .setAttribute("name", "OMEMO") .setAttribute("namespace", AxolotlService.PEP_PREFIX); @@ -109,7 +110,7 @@ public class MessageGenerator extends AbstractGenerator { packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(to); packet.setAxolotlMessage(axolotlMessage.toElement()); - packet.addChild("store", "urn:xmpp:hints"); + packet.addChild(new Store()); return packet; } @@ -161,8 +162,7 @@ public class MessageGenerator extends AbstractGenerator { packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(account.getJid()); packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store* + packet.addExtension(new NoStore()); return packet; } @@ -188,7 +188,7 @@ public class MessageGenerator extends AbstractGenerator { } else { displayed.setAttribute("id", message.getRemoteMsgId()); } - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } @@ -209,52 +209,7 @@ public class MessageGenerator extends AbstractGenerator { for (final String ourReaction : ourReactions) { reactions.addExtension(new Reaction(ourReaction)); } - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - public im.conversations.android.xmpp.model.stanza.Message conferenceSubject( - Conversation conversation, String subject) { - im.conversations.android.xmpp.model.stanza.Message packet = - new im.conversations.android.xmpp.model.stanza.Message(); - packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); - packet.setTo(conversation.getJid().asBareJid()); - packet.addChild("subject").setContent(subject); - packet.setFrom(conversation.getAccount().getJid().asBareJid()); - return packet; - } - - public im.conversations.android.xmpp.model.stanza.Message directInvite( - final Conversation conversation, final Jid contact) { - im.conversations.android.xmpp.model.stanza.Message packet = - new im.conversations.android.xmpp.model.stanza.Message(); - packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL); - packet.setTo(contact); - packet.setFrom(conversation.getAccount().getJid()); - Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getJid().asBareJid()); - String password = conversation.getMucOptions().getPassword(); - if (password != null) { - x.setAttribute("password", password); - } - if (contact.isFullJid()) { - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-copy", "urn:xmpp:hints"); - } - return packet; - } - - public im.conversations.android.xmpp.model.stanza.Message invite( - final Conversation conversation, final Jid contact) { - final var packet = new im.conversations.android.xmpp.model.stanza.Message(); - packet.setTo(conversation.getJid().asBareJid()); - packet.setFrom(conversation.getAccount().getJid()); - Element x = new Element("x"); - x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); - Element invite = new Element("invite"); - invite.setAttribute("to", contact.asBareJid()); - x.addChild(invite); - packet.addChild(x); + packet.addExtension(new Store()); return packet; } @@ -277,7 +232,7 @@ public class MessageGenerator extends AbstractGenerator { packet.setFrom(account.getJid()); packet.setTo(to); packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } @@ -291,7 +246,7 @@ public class MessageGenerator extends AbstractGenerator { finish.setAttribute("id", sessionId); final Element reasonElement = finish.addChild("reason", Namespace.JINGLE); reasonElement.addChild(reason.toString()); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } @@ -311,7 +266,7 @@ public class MessageGenerator extends AbstractGenerator { .setAttribute("media", media.toString()); } packet.addChild("request", "urn:xmpp:receipts"); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } @@ -326,7 +281,7 @@ public class MessageGenerator extends AbstractGenerator { final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); propose.addChild("description", Namespace.JINGLE_APPS_RTP); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } @@ -341,7 +296,7 @@ public class MessageGenerator extends AbstractGenerator { final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", sessionId); propose.addChild("description", Namespace.JINGLE_APPS_RTP); - packet.addChild("store", "urn:xmpp:hints"); + packet.addExtension(new Store()); return packet; } } diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index b520d29c6efeb85031140820114f51a6ef6e40ce..624a91ed8ba74a6c7c2c9a75d751411e95e2be6f 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -133,7 +133,7 @@ public abstract class AbstractParser extends XmppConnection.Delegate { return item.findChildContent("data", "urn:xmpp:avatar:data"); } - public static MucOptions.User parseItem(Conversation conference, Element item) { + public static MucOptions.User parseItem(final Conversation conference, final Element item) { return parseItem(conference, item, null); } @@ -156,8 +156,8 @@ public abstract class AbstractParser extends XmppConnection.Delegate { if (Jid.Invalid.isValid(realJid)) { user.setRealJid(realJid); } - user.setAffiliation(affiliation); - user.setRole(role); + // user.setAffiliation(affiliation); + // user.setRole(role); return user; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 35c1f4b3b6d604808f5be8b98497a0e1b31dc63a..16e597516ed9d8abbc6fb183454c4b03cf006fcb 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -34,15 +34,18 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.axolotl.Encrypted; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; +import im.conversations.android.xmpp.model.conference.DirectInvite; import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.markers.Displayed; +import im.conversations.android.xmpp.model.muc.user.MucUser; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.oob.OutOfBandData; import im.conversations.android.xmpp.model.pubsub.event.Event; @@ -212,7 +215,7 @@ public class MessageParser extends AbstractParser return null; } - private Invite extractInvite(final Element message) { + private Invite extractInvite(final im.conversations.android.xmpp.model.stanza.Message message) { final Element mucUser = message.findChild("x", Namespace.MUC_USER); if (mucUser != null) { final Element invite = mucUser.findChild("invite"); @@ -231,7 +234,7 @@ public class MessageParser extends AbstractParser return new Invite(room, password, false, from); } } - final Element conference = message.findChild("x", "jabber:x:conference"); + final var conference = message.getExtension(DirectInvite.class); if (conference != null) { Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from")); Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid")); @@ -303,7 +306,7 @@ public class MessageParser extends AbstractParser + ": received ping worthy error for seemingly online" + " muc at " + from); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); + getManager(MultiUserChatManager.class).pingAndRejoin(conversation); } } } @@ -347,6 +350,12 @@ public class MessageParser extends AbstractParser packet = f.first; serverMsgId = result.getAttribute("id"); query.incrementMessageCount(); + + if (query.isImplausibleFrom(packet.getFrom())) { + Log.d(Config.LOGTAG, "found implausible from in MUC MAM archive"); + return; + } + if (handleErrorMessage(account, packet)) { return; } @@ -520,7 +529,7 @@ public class MessageParser extends AbstractParser || mucUserElement != null || connection .getMucServersWithholdAccount() - .contains(counterpart.getDomain().toString()); + .contains(counterpart.getDomain()); final Conversation conversation = mXmppConnectionService.findOrCreateConversation( account, @@ -1009,69 +1018,11 @@ public class MessageParser extends AbstractParser } } } - if (conversation != null - && mucUserElement != null - && Jid.Invalid.hasValidFrom(packet) - && from.isBareJid()) { - for (Element child : mucUserElement.getChildren()) { - if ("status".equals(child.getName())) { - try { - int code = Integer.parseInt(child.getAttribute("code")); - if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) { - mXmppConnectionService.fetchConferenceConfiguration(conversation); - break; - } - } catch (Exception e) { - // ignored - } - } else if ("item".equals(child.getName())) { - final var user = AbstractParser.parseItem(conversation, child); - Log.d( - Config.LOGTAG, - account.getJid() - + ": changing affiliation for " - + user.getRealJid() - + " to " - + user.getAffiliation() - + " in " - + conversation.getJid().asBareJid()); - if (!user.realJidMatchesAccount()) { - final var mucOptions = conversation.getMucOptions(); - final boolean isNew = mucOptions.updateUser(user); - final var avatarService = mXmppConnectionService.getAvatarService(); - if (Strings.isNullOrEmpty(mucOptions.getAvatar())) { - avatarService.clear(mucOptions); - } - avatarService.clear(user); - mXmppConnectionService.updateMucRosterUi(); - mXmppConnectionService.updateConversationUi(); - Contact contact = user.getContact(); - if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { - Jid jid = user.getRealJid(); - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - if (cryptoTargets.remove(user.getRealJid())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": removed " - + jid - + " from crypto targets of " - + conversation.getName()); - conversation.setAcceptedCryptoTargets(cryptoTargets); - mXmppConnectionService.updateConversation(conversation); - } - } else if (isNew - && user.getRealJid() != null - && conversation.getMucOptions().isPrivateAndNonAnonymous() - && (contact == null || !contact.mutualPresenceSubscription()) - && account.getAxolotlService() - .hasEmptyDeviceList(user.getRealJid())) { - account.getAxolotlService().fetchDeviceIds(user.getRealJid()); - } - } - } - } + + if (original.hasExtension(MucUser.class)) { + getManager(MultiUserChatManager.class).handleStatusMessage(original); } + if (!isTypeGroupChat) { for (Element child : packet.getChildren()) { if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) @@ -1239,7 +1190,7 @@ public class MessageParser extends AbstractParser } final String nick = packet.findChildContent("nick", Namespace.NICK); - if (nick != null && Jid.Invalid.hasValidFrom(original)) { + if (nick != null && Jid.Invalid.isValid(from)) { if (mXmppConnectionService.isMuc(account, from)) { return; } @@ -1599,12 +1550,15 @@ public class MessageParser extends AbstractParser + ": received invite to " + jid + " but muc is considered to be online"); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); + getManager(MultiUserChatManager.class).pingAndRejoin(conversation); } else { conversation.getMucOptions().setPassword(password); mXmppConnectionService.databaseBackend.updateConversation(conversation); - mXmppConnectionService.joinMuc( - conversation, contact != null && contact.showInContactList()); + if (contact != null && contact.showInContactList()) { + getManager(MultiUserChatManager.class).joinFollowingInvite(conversation); + } else { + getManager(MultiUserChatManager.class).join(conversation); + } mXmppConnectionService.updateConversationUi(); } return true; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index adfede484d16fa25b62a570f3061794701968622..62dec079d548103e31304795a90d6e32b8a66544 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -15,7 +15,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.XmppUri; @@ -25,9 +24,11 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.manager.AvatarManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.model.muc.user.MucUser; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.util.ArrayList; @@ -81,15 +82,16 @@ public class PresenceParser extends AbstractParser final Jid from = packet.getFrom(); if (!from.isBareJid()) { final String type = packet.getAttribute("type"); - final Element x = packet.findChild("x", Namespace.MUC_USER); + final var x = packet.getExtension(MucUser.class); final var vCardUpdate = packet.getExtension(VCardUpdate.class); final List codes = getStatusCodes(x); if (type == null) { if (x != null) { - Element item = x.findChild("item"); + final var item = x.getItem(); if (item != null && !from.isBareJid()) { mucOptions.setError(MucOptions.Error.NONE); - final MucOptions.User user = parseItem(conversation, item, from); + final MucOptions.User user = + MultiUserChatManager.itemToUser(conversation, item, from); final var occupant = packet.getOnlyExtension(OccupantId.class); final String occupantId = mucOptions.occupantId() && occupant != null @@ -135,16 +137,17 @@ public class PresenceParser extends AbstractParser } if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { + final var address = mucOptions.getConversation().getJid().asBareJid(); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": room '" - + mucOptions.getConversation().getJid().asBareJid() + + address + "' created. pushing default configuration"); - mXmppConnectionService.pushConferenceConfiguration( - mucOptions.getConversation(), - IqGenerator.defaultChannelConfiguration(), - null); + getManager(MultiUserChatManager.class) + .pushConfiguration( + conversation, + MultiUserChatManager.defaultChannelConfiguration()); } if (mXmppConnectionService.getPgpEngine() != null) { Element signed = packet.findChild("x", "jabber:x:signed"); @@ -199,7 +202,7 @@ public class PresenceParser extends AbstractParser + " online=" + wasOnline); if (wasOnline) { - mXmppConnectionService.mucSelfPingAndRejoin(conversation); + getManager(MultiUserChatManager.class).pingAndRejoin(conversation); } } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { mucOptions.setError(MucOptions.Error.KICKED); @@ -216,9 +219,10 @@ public class PresenceParser extends AbstractParser Log.d(Config.LOGTAG, "unknown error in conference: " + packet); } } else if (!from.isBareJid()) { - Element item = x.findChild("item"); + final var item = x.getItem(); if (item != null) { - mucOptions.updateUser(parseItem(conversation, item, from)); + mucOptions.updateUser( + MultiUserChatManager.itemToUser(conversation, item, from)); } MucOptions.User user = mucOptions.deleteUser(from); if (user != null) { diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 8d53b61f1a7905e98ce787dd5a943e328a0c013d..0376c59c9aae493b31eb292356d6543487ad9d08 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -19,6 +19,7 @@ import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.items.Item; import im.conversations.android.xmpp.model.disco.items.ItemsQuery; @@ -304,10 +305,10 @@ public class ChannelDiscoveryService { for (final var account : service.getAccounts()) { final var connection = account.getXmppConnection(); if (connection != null && account.isEnabled()) { - for (final String mucService : connection.getMucServers()) { - final Jid jid = Jid.ofOrInvalid(mucService); - if (Jid.Invalid.isValid(jid)) { - localMucServices.put(jid, connection); + for (final var mucService : + connection.getManager(MultiUserChatManager.class).getServices()) { + if (Jid.Invalid.isValid(mucService)) { + localMucServices.put(mucService, connection); } } } diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 09af262147a0756bb1043aa763de9119e0d06151..a2ed3456746a1a800b2d80dd8f30959594b1e607 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -134,7 +134,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.execute(query); } - void catchupMUC(final Conversation conversation) { + public void catchupMUC(final Conversation conversation) { if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { query(conversation, new MamReference(0), 0, true); @@ -749,5 +749,16 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { boolean hasCallback() { return this.callback != null; } + + public boolean isImplausibleFrom(final Jid from) { + if (muc()) { + if (from == null) { + return true; + } + return !from.asBareJid().equals(getWith()); + } else { + return false; + } + } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b77ecc09f61b21c7807230c75c721de8fd3fa626..7324b745354b1d0d009349bf5af9dfd89a93c4a0 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -37,7 +37,6 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.security.KeyChain; -import android.text.TextUtils; import android.util.Log; import android.util.LruCache; import android.util.Pair; @@ -76,7 +75,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.generator.AbstractGenerator; @@ -85,7 +83,6 @@ import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.ServiceOutageStatus; -import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; @@ -110,7 +107,6 @@ import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.TorServiceUtils; import eu.siacs.conversations.utils.WakeLockHelper; import eu.siacs.conversations.utils.XmppUri; @@ -133,21 +129,19 @@ import eu.siacs.conversations.xmpp.manager.AvatarManager; import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.BookmarkManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; -import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.NickManager; +import eu.siacs.conversations.xmpp.manager.PepManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; -import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RegistrationManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.VCardManager; import eu.siacs.conversations.xmpp.pep.Avatar; -import im.conversations.android.xmpp.Entity; -import im.conversations.android.xmpp.IqErrorException; -import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.up.Push; -import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.io.File; import java.security.Security; import java.security.cert.CertificateException; @@ -159,7 +153,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; @@ -1652,7 +1645,10 @@ public class XmppConnectionService extends Service { } } - final boolean inProgressJoin = isJoinInProgress(conversation); + final boolean inProgressJoin = + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .isJoinInProgress(conversation); if (account.isOnlineAndConnected() && !inProgressJoin) { switch (message.getEncryption()) { @@ -1786,29 +1782,6 @@ public class XmppConnectionService extends Service { } } - private boolean isJoinInProgress(final Conversation conversation) { - final Account account = conversation.getAccount(); - synchronized (account.inProgressConferenceJoins) { - if (conversation.getMode() == Conversational.MODE_MULTI) { - final boolean inProgress = account.inProgressConferenceJoins.contains(conversation); - final boolean pending = account.pendingConferenceJoins.contains(conversation); - final boolean inProgressJoin = inProgress || pending; - if (inProgressJoin) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": holding back message to group. inProgress=" - + inProgress - + ", pending=" - + pending); - } - return inProgressJoin; - } else { - return false; - } - } - } - public void sendUnsentMessages(final Conversation conversation) { conversation.findWaitingMessages(message -> resendMessage(message, true)); } @@ -1897,141 +1870,12 @@ public class XmppConnectionService extends Service { return true; } - public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { - final Account account = bookmark.getAccount(); - Conversation conversation = find(bookmark); - if (conversation != null) { - if (conversation.getMode() != Conversation.MODE_MULTI) { - return; - } - bookmark.setConversation(conversation); - if (pep && !bookmark.autojoin()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": archiving conference (" - + conversation.getJid() - + ") after receiving pep"); - archiveConversation(conversation, false); - } else { - final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) { - final String current = mucOptions.getActualNick(); - final String proposed = mucOptions.getProposedNickPure(); - if (current != null && !current.equals(proposed)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": proposed nick changed after bookmark push " - + current - + "->" - + proposed); - joinMuc(conversation); - } - } else { - checkMucRequiresRename(conversation); - } - } - } else if (bookmark.autojoin()) { - conversation = - findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); - bookmark.setConversation(conversation); - } - } - - public void processModifiedBookmark(final Bookmark bookmark) { - processModifiedBookmark(bookmark, true); - } - - public void ensureBookmarkIsAutoJoin(final Conversation conversation) { - final var account = conversation.getAccount(); - final var existingBookmark = conversation.getBookmark(); - if (existingBookmark == null) { - final var bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - bookmark.setAutojoin(true); - createBookmark(account, bookmark); - } else { - if (existingBookmark.autojoin()) { - return; - } - existingBookmark.setAutojoin(true); - createBookmark(account, existingBookmark); - } - } - public void createBookmark(final Account account, final Bookmark bookmark) { - account.putBookmark(bookmark); - final XmppConnection connection = account.getXmppConnection(); - final ListenableFuture future; - if (connection.getManager(BookmarkManager.class).hasFeature()) { - future = connection.getManager(BookmarkManager.class).publish(bookmark); - } else if (connection.getManager(LegacyBookmarkManager.class).hasConversion()) { - future = - connection - .getManager(LegacyBookmarkManager.class) - .publish(account.getBookmarks()); - } else { - future = - connection - .getManager(PrivateStorageManager.class) - .publishBookmarks(account.getBookmarks()); - } - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": created bookmark"); - } - - @Override - public void onFailure(@NonNull Throwable t) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not create bookmark", - t); - } - }, - MoreExecutors.directExecutor()); + account.getXmppConnection().getManager(BookmarkManager.class).create(bookmark); } public void deleteBookmark(final Account account, final Bookmark bookmark) { - account.removeBookmark(bookmark); - final XmppConnection connection = account.getXmppConnection(); - final ListenableFuture future; - if (connection.getManager(BookmarkManager.class).hasFeature()) { - future = - connection - .getManager(BookmarkManager.class) - .retract(bookmark.getJid().asBareJid()); - } else if (connection.getManager(LegacyBookmarkManager.class).hasConversion()) { - future = - connection - .getManager(LegacyBookmarkManager.class) - .publish(account.getBookmarks()); - } else { - future = - connection - .getManager(PrivateStorageManager.class) - .publishBookmarks(account.getBookmarks()); - } - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark"); - } - - @Override - public void onFailure(@NonNull Throwable t) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not delete bookmark", - t); - } - }, - MoreExecutors.directExecutor()); + account.getXmppConnection().getManager(BookmarkManager.class).delete(bookmark); } private void restoreFromDatabase() { @@ -2475,7 +2319,9 @@ public class XmppConnectionService extends Service { null)); this.conversations.add(existing); if (existing.getMode() == Conversational.MODE_MULTI) { - ensureBookmarkIsAutoJoin(existing); + account.getXmppConnection() + .getManager(BookmarkManager.class) + .ensureBookmarkIsAutoJoin(existing); } updateConversationUi(); return existing; @@ -2534,17 +2380,20 @@ public class XmppConnectionService extends Service { public void archiveConversation( Conversation conversation, final boolean maySynchronizeWithBookmarks) { + final var account = conversation.getAccount(); + final var connection = account.getXmppConnection(); getNotificationService().clear(conversation); conversation.setStatus(Conversation.STATUS_ARCHIVED); conversation.setNextMessage(null); synchronized (this.conversations) { getMessageArchiveService().kill(conversation); if (conversation.getMode() == Conversation.MODE_MULTI) { + // TODO always clean up bookmarks no matter if we are currently connected + // TODO always delete reference to conversation in bookmark if (conversation.getAccount().getStatus() == Account.State.ONLINE) { final Bookmark bookmark = conversation.getBookmark(); if (maySynchronizeWithBookmarks && bookmark != null) { if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { - Account account = bookmark.getAccount(); bookmark.setConversation(null); deleteBookmark(account, bookmark); } else if (bookmark.autojoin()) { @@ -2553,7 +2402,7 @@ public class XmppConnectionService extends Service { } } } - leaveMuc(conversation); + connection.getManager(MultiUserChatManager.class).leave(conversation); } else { if (conversation .getContact() @@ -2753,7 +2602,9 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { if (connected) { - leaveMuc(conversation); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .unavailable(conversation); } } conversations.remove(conversation); @@ -3101,344 +2952,16 @@ public class XmppConnectionService extends Service { } } - public void mucSelfPingAndRejoin(final Conversation conversation) { - final Account account = conversation.getAccount(); - synchronized (account.inProgressConferenceJoins) { - if (account.inProgressConferenceJoins.contains(conversation)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": canceling muc self ping because join is already under way"); - return; - } - } - synchronized (account.inProgressConferencePings) { - if (!account.inProgressConferencePings.add(conversation)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": canceling muc self ping because ping is already under way"); - return; - } - } - // TODO use PingManager - final Jid self = conversation.getMucOptions().getSelf().getFullJid(); - final Iq ping = new Iq(Iq.Type.GET); - ping.setTo(self); - ping.addChild("ping", Namespace.PING); - sendIqPacket( - conversation.getAccount(), - ping, - (response) -> { - if (response.getType() == Iq.Type.ERROR) { - final var error = response.getError(); - if (error == null - || error.hasChild("service-unavailable") - || error.hasChild("feature-not-implemented") - || error.hasChild("item-not-found")) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": ping to " - + self - + " came back as ignorable error"); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": ping to " - + self - + " failed. attempting rejoin"); - joinMuc(conversation); - } - } else if (response.getType() == Iq.Type.RESULT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": ping to " - + self - + " came back fine"); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.remove(conversation); - } - }); - } - - public void joinMuc(Conversation conversation) { - joinMuc(conversation, null, false); - } - - public void joinMuc(Conversation conversation, boolean followedInvite) { - joinMuc(conversation, null, followedInvite); - } - - private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { - joinMuc(conversation, onConferenceJoined, false); - } - - private void joinMuc( - final Conversation conversation, - final OnConferenceJoined onConferenceJoined, - final boolean followedInvite) { - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.remove(conversation); - } - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.remove(conversation); - } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.add(conversation); - } - if (Config.MUC_LEAVE_BEFORE_JOIN) { - sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions())); - } - conversation.resetMucOptions(); - if (onConferenceJoined != null) { - conversation.getMucOptions().flagNoAutoPushConfiguration(); - } - conversation.setHasMessagesLeftOnServer(false); - fetchConferenceConfiguration( - conversation, - new OnConferenceConfigurationFetched() { - - private void join(Conversation conversation) { - Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); - - if (mucOptions.nonanonymous() - && !mucOptions.membersOnly() - && !conversation.getBooleanAttribute( - "accept_non_anonymous", false)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - } - mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); - updateConversationUi(); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - return; - } - - final Jid joinJid = mucOptions.getSelf().getFullJid(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": joining conversation " - + joinJid.toString()); - final var packet = - mPresenceGenerator.selfPresence( - account, - im.conversations.android.xmpp.model.stanza.Presence - .Availability.ONLINE, - mucOptions.nonanonymous() - || onConferenceJoined != null); - packet.setTo(joinJid); - Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); - if (conversation.getMucOptions().getPassword() != null) { - x.addChild("password").setContent(mucOptions.getPassword()); - } - - if (mucOptions.mamSupport()) { - // Use MAM instead of the limited muc history to get history - x.addChild("history").setAttribute("maxchars", "0"); - } else { - // Fallback to muc history - x.addChild("history") - .setAttribute( - "since", - PresenceGenerator.getTimestamp( - conversation - .getLastMessageTransmitted() - .getTimestamp())); - } - sendPresencePacket(account, packet); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - if (!joinJid.equals(conversation.getJid())) { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - } - - if (mucOptions.mamSupport()) { - getMessageArchiveService().catchupMUC(conversation); - } - if (mucOptions.isPrivateAndNonAnonymous()) { - fetchConferenceMembers(conversation); - - if (followedInvite) { - final Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - if (!bookmark.autojoin()) { - bookmark.setAutojoin(true); - createBookmark(account, bookmark); - } - } else { - saveConversationAsBookmark(conversation, null); - } - } - } - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - sendUnsentMessages(conversation); - } - } - - @Override - public void onConferenceConfigurationFetched(Conversation conversation) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": conversation (" - + conversation.getJid() - + ") got archived before IQ result"); - return; - } - join(conversation); - } - - @Override - public void onFetchFailed( - final Conversation conversation, final String errorCondition) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": conversation (" - + conversation.getJid() - + ") got archived before IQ result"); - return; - } - if ("remote-server-not-found".equals(errorCondition)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - } - conversation - .getMucOptions() - .setError(MucOptions.Error.SERVER_NOT_FOUND); - updateConversationUi(); - } else { - join(conversation); - fetchConferenceConfiguration(conversation); - } - } - }); - updateConversationUi(); - } else { - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.add(conversation); - } - conversation.resetMucOptions(); - conversation.setHasMessagesLeftOnServer(false); - updateConversationUi(); - } - } - - private void fetchConferenceMembers(final Conversation conversation) { - final Account account = conversation.getAccount(); - final AxolotlService axolotlService = account.getAxolotlService(); - final String[] affiliations = {"member", "admin", "owner"}; - final Consumer callback = - new Consumer() { - - private int i = 0; - private boolean success = true; - - @Override - public void accept(Iq response) { - final boolean omemoEnabled = - conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - Element query = response.query("http://jabber.org/protocol/muc#admin"); - if (response.getType() == Iq.Type.RESULT && query != null) { - for (Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - MucOptions.User user = - AbstractParser.parseItem(conversation, child); - if (!user.realJidMatchesAccount()) { - boolean isNew = - conversation.getMucOptions().updateUser(user); - Contact contact = user.getContact(); - if (omemoEnabled - && isNew - && user.getRealJid() != null - && (contact == null - || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList( - user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); - } - } - } - } - } else { - success = false; - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not request affiliation " - + affiliations[i] - + " in " - + conversation.getJid().asBareJid()); - } - ++i; - if (i >= affiliations.length) { - final var mucOptions = conversation.getMucOptions(); - final var members = mucOptions.getMembers(true); - if (success) { - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - boolean changed = false; - for (ListIterator iterator = cryptoTargets.listIterator(); - iterator.hasNext(); ) { - Jid jid = iterator.next(); - if (!members.contains(jid) - && !members.contains(jid.getDomain())) { - iterator.remove(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": removed " - + jid - + " from crypto targets of " - + conversation.getName()); - changed = true; - } - } - if (changed) { - conversation.setAcceptedCryptoTargets(cryptoTargets); - updateConversation(conversation); - } - } - getAvatarService().clear(mucOptions); - updateMucRosterUi(); - updateConversationUi(); - } - } - }; - for (String affiliation : affiliations) { - sendIqPacket( - account, mIqGenerator.queryAffiliation(conversation, affiliation), callback); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); + public void joinMuc(final Conversation conversation) { + final var account = conversation.getAccount(); + account.getXmppConnection().getManager(MultiUserChatManager.class).join(conversation); } public void providePasswordForMuc(final Conversation conversation, final String password) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.getMucOptions().setPassword(password); - if (conversation.getBookmark() != null) { - final Bookmark bookmark = conversation.getBookmark(); - bookmark.setAutojoin(true); - createBookmark(conversation.getAccount(), bookmark); - } - updateConversation(conversation); - joinMuc(conversation); - } + final var account = conversation.getAccount(); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .setPassword(conversation, password); } public void deleteAvatar(final Account account) { @@ -3476,23 +2999,28 @@ public class XmppConnectionService extends Service { } public void deletePepNode(final Account account, final String node) { - final Iq request = mIqGenerator.deleteNode(node); - sendIqPacket( - account, - request, - (packet) -> { - if (packet.getType() == Iq.Type.RESULT) { + final var future = account.getXmppConnection().getManager(PepManager.class).delete(node); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully deleted pep node " + node); - } else { + } + + @Override + public void onFailure(@NonNull Throwable t) { Log.d( Config.LOGTAG, - account.getJid().asBareJid() + ": failed to delete " + packet); + account.getJid().asBareJid() + ": failed to delete node " + node, + t); } - }); + }, + MoreExecutors.directExecutor()); } private boolean hasEnabledAccounts() { @@ -3565,154 +3093,17 @@ public class XmppConnectionService extends Service { createBookmark(bookmark.getAccount(), bookmark); } - public boolean renameInMuc( - final Conversation conversation, - final String nick, - final UiCallback callback) { - final Account account = conversation.getAccount(); - final Bookmark bookmark = conversation.getBookmark(); - final MucOptions options = conversation.getMucOptions(); - final Jid joinJid = options.createJoinJid(nick); - if (joinJid == null) { - return false; - } - if (options.online()) { - options.setOnRenameListener( - new OnRenameListener() { - - @Override - public void onSuccess() { - callback.success(conversation); - } - - @Override - public void onFailure() { - callback.error(R.string.nick_in_use, conversation); - } - }); - - final var packet = - mPresenceGenerator.selfPresence( - account, - im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE, - options.nonanonymous()); - packet.setTo(joinJid); - sendPresencePacket(account, packet); - if (nick.equals(MucOptions.defaultNick(account)) - && bookmark != null - && bookmark.getNick() != null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": removing nick from bookmark for " - + bookmark.getJid()); - bookmark.setNick(null); - createBookmark(account, bookmark); - } - } else { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - if (account.getStatus() == Account.State.ONLINE) { - if (bookmark != null) { - bookmark.setNick(nick); - createBookmark(account, bookmark); - } - joinMuc(conversation); - } - } - return true; - } - public void checkMucRequiresRename() { synchronized (this.conversations) { for (final Conversation conversation : this.conversations) { if (conversation.getMode() == Conversational.MODE_MULTI) { - checkMucRequiresRename(conversation); - } - } - } - } - - private void checkMucRequiresRename(final Conversation conversation) { - final var options = conversation.getMucOptions(); - if (!options.online()) { - return; - } - final var account = conversation.getAccount(); - final String current = options.getActualNick(); - final String proposed = options.getProposedNickPure(); - if (current == null || current.equals(proposed)) { - return; - } - final Jid joinJid = options.createJoinJid(proposed); - Log.d( - Config.LOGTAG, - String.format( - "%s: muc rename required %s (was: %s)", - account.getJid().asBareJid(), joinJid, current)); - final var packet = - mPresenceGenerator.selfPresence( - account, - im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE, - options.nonanonymous()); - packet.setTo(joinJid); - sendPresencePacket(account, packet); - } - - public void leaveMuc(Conversation conversation) { - leaveMuc(conversation, false); - } - - private void leaveMuc(Conversation conversation, boolean now) { - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.remove(conversation); - } - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.remove(conversation); - } - if (account.getStatus() == Account.State.ONLINE || now) { - sendPresencePacket( - conversation.getAccount(), - mPresenceGenerator.leave(conversation.getMucOptions())); - conversation.getMucOptions().setOffline(); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setConversation(null); - } - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": leaving muc " - + conversation.getJid()); - final var connection = account.getXmppConnection(); - if (connection != null) { - connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid()); - } - } else { - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.add(conversation); - } - } - } - - public String findConferenceServer(final Account account) { - String server; - if (account.getXmppConnection() != null) { - server = account.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - for (Account other : getAccounts()) { - if (other != account && other.getXmppConnection() != null) { - server = other.getXmppConnection().getMucServer(); - if (server != null) { - return server; + final var account = conversation.getAccount(); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .checkMucRequiresRename(conversation); } } } - return null; } public void createPublicChannel( @@ -3720,226 +3111,57 @@ public class XmppConnectionService extends Service { final String name, final Jid address, final UiCallback callback) { - joinMuc( - findOrCreateConversation(account, address, true, false, true), - conversation -> { - final Bundle configuration = IqGenerator.defaultChannelConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); + final var future = + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .createPublicChannel(address, name); + + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Conversation result) { + callback.success(result); } - pushConferenceConfiguration( - conversation, - configuration, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - saveConversationAsBookmark(conversation, name); - callback.success(conversation); - } - @Override - public void onPushFailed() { - if (conversation - .getMucOptions() - .getSelf() - .getAffiliation() - .ranks(MucOptions.Affiliation.OWNER)) { - callback.error( - R.string.unable_to_set_channel_configuration, - conversation); - } else { - callback.error( - R.string.joined_an_existing_channel, conversation); - } - } - }); - }); + @Override + public void onFailure(Throwable t) { + Log.d(Config.LOGTAG, "could not create public channel", t); + // TODO I guess it’s better to just not use callbacks here + callback.error(R.string.unable_to_set_channel_configuration, null); + } + }, + MoreExecutors.directExecutor()); } public boolean createAdhocConference( final Account account, final String name, - final Iterable jids, + final Collection addresses, final UiCallback callback) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": creating adhoc conference with " - + jids.toString()); - if (account.getStatus() == Account.State.ONLINE) { - try { - String server = findConferenceServer(account); - if (server == null) { - if (callback != null) { - callback.error(R.string.no_conference_server_found, null); - } - return false; - } - final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null); - final Conversation conversation = - findOrCreateConversation(account, jid, true, false, true); - joinMuc( - conversation, - new OnConferenceJoined() { - @Override - public void onConferenceJoined(final Conversation conversation) { - final Bundle configuration = - IqGenerator.defaultGroupChatConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration( - conversation, - configuration, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - for (String resource : - account.getSelfContact() - .getPresences() - .toResourceArray()) { - Jid other = - account.getJid().withResource(resource); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": sending direct invite to " - + other); - directInvite(conversation, other); - } - saveConversationAsBookmark(conversation, name); - if (callback != null) { - callback.success(conversation); - } - } - - @Override - public void onPushFailed() { - archiveConversation(conversation); - if (callback != null) { - callback.error( - R.string.conference_creation_failed, - conversation); - } - } - }); - } - }); - return true; - } catch (IllegalArgumentException e) { - if (callback != null) { - callback.error(R.string.conference_creation_failed, null); - } - return false; - } - } else { - if (callback != null) { - callback.error(R.string.not_connected_try_again, null); - } + final var manager = account.getXmppConnection().getManager(MultiUserChatManager.class); + if (manager.getServices().isEmpty()) { return false; } - } - public void fetchConferenceConfiguration(final Conversation conversation) { - fetchConferenceConfiguration(conversation, null); - } + final var future = manager.createPrivateGroupChat(name, addresses); - public void fetchConferenceConfiguration( - final Conversation conversation, final OnConferenceConfigurationFetched callback) { - final var account = conversation.getAccount(); - final var connection = account.getXmppConnection(); - final var address = conversation.getJid().asBareJid(); - if (connection == null) { - return; - } - final var future = - connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null); Futures.addCallback( future, new FutureCallback<>() { @Override - public void onSuccess(InfoQuery result) { - final var avatarHash = - result.getServiceDiscoveryExtension( - Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash"); - if (VCardUpdate.isValidSHA1(avatarHash)) { - connection - .getManager(AvatarManager.class) - .handleVCardUpdate(address, avatarHash); - } - final MucOptions mucOptions = conversation.getMucOptions(); - final Bookmark bookmark = conversation.getBookmark(); - final boolean sameBefore = - StringUtils.equals( - bookmark == null ? null : bookmark.getBookmarkName(), - mucOptions.getName()); - - final var hadOccupantId = mucOptions.occupantId(); - if (mucOptions.updateConfiguration(result)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": muc configuration changed for " - + conversation.getJid().asBareJid()); - updateConversation(conversation); - } - - final var hasOccupantId = mucOptions.occupantId(); - - if (!hadOccupantId && hasOccupantId && mucOptions.online()) { - final var me = mucOptions.getSelf().getFullJid(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": gained support for occupant-id in " - + me - + ". resending presence"); - final var packet = - mPresenceGenerator.selfPresence( - account, - im.conversations.android.xmpp.model.stanza.Presence - .Availability.ONLINE, - mucOptions.nonanonymous()); - packet.setTo(me); - sendPresencePacket(account, packet); - } - - if (bookmark != null - && (sameBefore || bookmark.getBookmarkName() == null)) { - if (bookmark.setBookmarkName( - StringUtils.nullOnEmpty(mucOptions.getName()))) { - createBookmark(account, bookmark); - } - } - - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } - - updateConversationUi(); + public void onSuccess(Conversation result) { + callback.success(result); } @Override - public void onFailure(@NonNull Throwable throwable) { - if (throwable instanceof TimeoutException) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received timeout waiting for conference" - + " configuration fetch"); - } else if (throwable instanceof IqErrorException errorResponseException) { - if (callback != null) { - callback.onFetchFailed( - conversation, - errorResponseException.getResponse().getErrorCondition()); - } - } + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not create private group chat", t); + callback.error(R.string.conference_creation_failed, null); } }, MoreExecutors.directExecutor()); + return true; } public void pushNodeConfiguration( @@ -4001,133 +3223,63 @@ public class XmppConnectionService extends Service { }); } - public void pushConferenceConfiguration( - final Conversation conversation, - final Bundle options, - final OnConfigurationPushed callback) { - if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) { - conversation.setAttribute("accept_non_anonymous", true); - updateConversation(conversation); - } - if (options.containsKey("muc#roomconfig_moderatedroom")) { - final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom")); - options.putString("members_by_default", moderated ? "0" : "1"); - } - if (options.containsKey("muc#roomconfig_allowpm")) { - // ejabberd :-/ - final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm")); - options.putString("allow_private_messages", allow ? "1" : "0"); - options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody"); - } - final var account = conversation.getAccount(); - final Iq request = new Iq(Iq.Type.GET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket( - account, - request, - response -> { - if (response.getType() == Iq.Type.RESULT) { - final Data data = - Data.parse(response.query().findChild("x", Namespace.DATA)); - data.submit(options); - final Iq set = new Iq(Iq.Type.SET); - set.setTo(conversation.getJid().asBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket( - account, - set, - packet -> { - if (callback != null) { - if (packet.getType() == Iq.Type.RESULT) { - callback.onPushSucceeded(); - } else { - Log.d(Config.LOGTAG, "failed: " + packet); - callback.onPushFailed(); - } - } - }); - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - }); - } - public void pushSubjectToConference(final Conversation conference, final String subject) { - final var packet = - this.getMessageGenerator() - .conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); - this.sendMessagePacket(conference.getAccount(), packet); + final var account = conference.getAccount(); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .setSubject(conference, subject); } public void changeAffiliationInConference( final Conversation conference, Jid user, - final MucOptions.Affiliation affiliation, + final Affiliation affiliation, final OnAffiliationChanged callback) { - final Jid jid = user.asBareJid(); - final Iq request = - this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket( - conference.getAccount(), - request, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final var mucOptions = conference.getMucOptions(); - mucOptions.changeAffiliation(jid, affiliation); - getAvatarService().clear(mucOptions); + final var account = conference.getAccount(); + final var future = + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .setAffiliation(conference, affiliation, user); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { if (callback != null) { - callback.onAffiliationChangedSuccessful(jid); + callback.onAffiliationChangedSuccessful(user); } else { Log.d( Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation); } - } else if (callback != null) { - callback.onAffiliationChangeFailed( - jid, R.string.could_not_change_affiliation); - } else { - Log.d(Config.LOGTAG, "unable to change affiliation"); } - }); + + @Override + public void onFailure(Throwable t) { + if (callback != null) { + callback.onAffiliationChangeFailed( + user, R.string.could_not_change_affiliation); + } else { + Log.d(Config.LOGTAG, "could not change affiliation", t); + } + } + }, + MoreExecutors.directExecutor()); } public void changeRoleInConference( - final Conversation conference, final String nick, MucOptions.Role role) { + final Conversation conference, final String nick, Role role) { final var account = conference.getAccount(); - final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - sendIqPacket( - account, - request, - (packet) -> { - if (packet.getType() != Iq.Type.RESULT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + " unable to change role of " + nick); - } - }); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .setRole(conference.getJid().asBareJid(), role, nick); } - public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) { - final Iq request = new Iq(Iq.Type.SET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/muc#owner").addChild("destroy"); - sendIqPacket( - conversation.getAccount(), - request, - response -> { - if (response.getType() == Iq.Type.RESULT) { - if (callback != null) { - callback.onRoomDestroySucceeded(); - } - } else if (response.getType() == Iq.Type.ERROR) { - if (callback != null) { - callback.onRoomDestroyFailed(); - } - } - }); + public ListenableFuture destroyRoom(final Conversation conversation) { + final var account = conversation.getAccount(); + return account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .destroy(conversation.getJid().asBareJid()); } private void disconnect(final Account account, boolean force) { @@ -4140,7 +3292,9 @@ public class XmppConnectionService extends Service { for (Conversation conversation : conversations) { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation, true); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .unavailable(conversation); } } } @@ -4263,17 +3417,7 @@ public class XmppConnectionService extends Service { for (Conversation conversation : conversations) { if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { - final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.online()) { - final var packet = - mPresenceGenerator.selfPresence( - account, - im.conversations.android.xmpp.model.stanza.Presence - .Availability.ONLINE, - mucOptions.nonanonymous()); - packet.setTo(mucOptions.getSelf().getFullJid()); - connection.sendPresencePacket(packet); - } + connection.getManager(MultiUserChatManager.class).resendPresence(conversation); } } } @@ -4319,25 +3463,17 @@ public class XmppConnectionService extends Service { } public void invite(final Conversation conversation, final Jid contact) { - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": inviting " - + contact - + " to " - + conversation.getJid().asBareJid()); - final MucOptions.User user = - conversation.getMucOptions().findUserByRealJid(contact.asBareJid()); - if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { - changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); - } - final var packet = mMessageGenerator.invite(conversation, contact); - sendMessagePacket(conversation.getAccount(), packet); + final var account = conversation.getAccount(); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .invite(conversation, contact); } public void directInvite(Conversation conversation, Jid jid) { - final var packet = mMessageGenerator.directInvite(conversation, jid); - sendMessagePacket(conversation.getAccount(), packet); + final var account = conversation.getAccount(); + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .directInvite(conversation, jid); } public void resetSendingToWaiting(Account account) { @@ -4877,20 +4013,20 @@ public class XmppConnectionService extends Service { } public Collection getKnownConferenceHosts() { - final Set mucServers = new HashSet<>(); + final var builder = new ImmutableSet.Builder(); for (final Account account : accounts) { - if (account.getXmppConnection() != null) { - mucServers.addAll(account.getXmppConnection().getMucServers()); - for (final Bookmark bookmark : account.getBookmarks()) { - final Jid jid = bookmark.getJid(); - final String s = jid == null ? null : jid.getDomain().toString(); - if (s != null) { - mucServers.add(s); - } + final var connection = account.getXmppConnection(); + builder.addAll(connection.getManager(MultiUserChatManager.class).getServices()); + for (final var bookmark : account.getBookmarks()) { + final Jid jid = bookmark.getJid(); + final Jid domain = jid == null ? null : jid.getDomain(); + if (domain == null) { + continue; } + builder.add(domain); } } - return mucServers; + return Collections2.transform(builder.build(), Jid::toString); } public void sendMessagePacket( @@ -4902,15 +4038,6 @@ public class XmppConnectionService extends Service { } } - public void sendPresencePacket( - final Account account, - final im.conversations.android.xmpp.model.stanza.Presence packet) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendPresencePacket(packet); - } - } - public ListenableFuture sendIqPacket(final Account account, final Iq request) { final XmppConnection connection = account.getXmppConnection(); if (connection == null) { @@ -5160,21 +4287,6 @@ public class XmppConnectionService extends Service { return templates; } - public void saveConversationAsBookmark(final Conversation conversation, final String name) { - final Account account = conversation.getAccount(); - final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - final String nick = conversation.getJid().getResource(); - if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { - bookmark.setNick(nick); - } - if (!TextUtils.isEmpty(name)) { - bookmark.setBookmarkName(name); - } - bookmark.setAutojoin(true); - createBookmark(account, bookmark); - bookmark.setConversation(conversation); - } - public boolean verifyFingerprints(Contact contact, List fingerprints) { boolean performedVerification = false; final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); @@ -5259,12 +4371,6 @@ public class XmppConnectionService extends Service { void informUser(int r); } - public interface OnRoomDestroy { - void onRoomDestroySucceeded(); - - void onRoomDestroyFailed(); - } - public interface OnAffiliationChanged { void onAffiliationChangedSuccessful(Jid jid); diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index a133247306a046bf9a49ad4a75c89a4d564a2b76..cfe1ad12679e2d2f1996e558542b5bf80b2c6b13 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -35,6 +35,7 @@ import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.manager.BookmarkManager; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -299,7 +300,9 @@ public class ChannelDiscoveryActivity extends XmppActivity final Conversation conversation = xmppConnectionService.findOrCreateConversation( account, result.getRoom(), true, true, true); - xmppConnectionService.ensureBookmarkIsAutoJoin(conversation); + account.getXmppConnection() + .getManager(BookmarkManager.class) + .ensureBookmarkIsAutoJoin(conversation); switchToConversation(conversation); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 72f41044c86e56dc055a2a61d991f20a2328b2da..e2db6c7022a78247f3c621387148bfd66ad17ce9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -4,7 +4,6 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue; import static eu.siacs.conversations.utils.StringUtils.changed; import android.app.Activity; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; @@ -19,9 +18,15 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import de.gultsch.common.Linkify; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityMucDetailsBinding; @@ -45,10 +50,13 @@ import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.manager.BookmarkManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -58,8 +66,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, - XmppConnectionService.OnConfigurationPushed, - XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded { public static final String ACTION_VIEW_MUC = "view_muc"; @@ -72,24 +78,20 @@ public class ConferenceDetailsActivity extends XmppActivity private boolean mAdvancedMode = false; - private final UiCallback renameCallback = - new UiCallback() { + private FutureCallback renameCallback = + new FutureCallback() { @Override - public void success(Conversation object) { + public void onSuccess(Void result) { displayToast(getString(R.string.your_nick_has_been_changed)); - runOnUiThread( - () -> { - updateView(); - }); + updateView(); } @Override - public void error(final int errorCode, Conversation object) { - displayToast(getString(errorCode)); - } + public void onFailure(Throwable t) { - @Override - public void userInputRequired(PendingIntent pi, Conversation object) {} + // TODO check for NickInUseException and NickInvalid exception + + } }; public static void open(final Activity activity, final Conversation conversation) { @@ -139,6 +141,20 @@ public class ConferenceDetailsActivity extends XmppActivity } }; + private final FutureCallback onConfigurationPushed = + new FutureCallback() { + + @Override + public void onSuccess(Void result) { + displayToast(getString(R.string.modified_conference_options)); + } + + @Override + public void onFailure(Throwable t) { + displayToast(getString(R.string.could_not_modify_conference_options)); + } + }; + private final OnClickListener mChangeConferenceSettings = new OnClickListener() { @Override @@ -159,15 +175,17 @@ public class ConferenceDetailsActivity extends XmppActivity builder.setPositiveButton( R.string.confirm, (dialog, which) -> { - final Bundle options = configuration.toBundle(values); - options.putString("muc#roomconfig_persistentroom", "1"); - if (options.containsKey("muc#roomconfig_allowinvites")) { - options.putString( - "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", - options.getString("muc#roomconfig_allowinvites")); - } - xmppConnectionService.pushConferenceConfiguration( - mConversation, options, ConferenceDetailsActivity.this); + final var options = configuration.toBundle(values); + final var future = + mConversation + .getAccount() + .getXmppConnection() + .getManager(MultiUserChatManager.class) + .pushConfiguration(mConversation, options); + Futures.addCallback( + future, + onConfigurationPushed, + ContextCompat.getMainExecutor(getApplication())); }); builder.create().show(); } @@ -202,12 +220,21 @@ public class ConferenceDetailsActivity extends XmppActivity mConversation.getMucOptions().getActualNick(), R.string.nickname, value -> { - if (xmppConnectionService.renameInMuc( - mConversation, value, renameCallback)) { - return null; - } else { + if (mConversation.getMucOptions().createJoinJid(value) + == null) { return getString(R.string.invalid_muc_nick); } + final var future = + mConversation + .getAccount() + .getXmppConnection() + .getManager(MultiUserChatManager.class) + .changeUsername(mConversation, value); + Futures.addCallback( + future, + renameCallback, + ContextCompat.getMainExecutor(this)); + return null; })); this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false); this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); @@ -223,10 +250,7 @@ public class ConferenceDetailsActivity extends XmppActivity .show(); return; } - if (!mucOptions - .getSelf() - .getAffiliation() - .ranks(MucOptions.Affiliation.OWNER)) { + if (!mucOptions.getSelf().ranks(Affiliation.OWNER)) { Toast.makeText( this, R.string.only_the_owner_can_change_group_chat_avatar, @@ -344,8 +368,7 @@ public class ConferenceDetailsActivity extends XmppActivity this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel)); final String name = mucOptions.getName(); this.binding.mucEditTitle.setText(""); - final boolean owner = - mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER); + final boolean owner = mucOptions.getSelf().ranks(Affiliation.OWNER); if (owner || printableValue(name)) { this.binding.mucEditTitle.setVisibility(View.VISIBLE); if (name != null) { @@ -388,16 +411,23 @@ public class ConferenceDetailsActivity extends XmppActivity } private void onMucInfoUpdated(String subject, String name) { + final var account = mConversation.getAccount(); final MucOptions mucOptions = mConversation.getMucOptions(); if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) { xmppConnectionService.pushSubjectToConference(mConversation, subject); } - if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) - && changed(mucOptions.getName(), name)) { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name)); - xmppConnectionService.pushConferenceConfiguration(mConversation, options, this); + if (mucOptions.getSelf().ranks(Affiliation.OWNER) && changed(mucOptions.getName(), name)) { + final var options = + new ImmutableMap.Builder() + .put("muc#roomconfig_persistentroom", true) + .put("muc#roomconfig_roomname", Strings.nullToEmpty(name)) + .build(); + final var future = + account.getXmppConnection() + .getManager(MultiUserChatManager.class) + .pushConfiguration(mConversation, options); + Futures.addCallback( + future, onConfigurationPushed, ContextCompat.getMainExecutor(getApplication())); } } @@ -426,11 +456,7 @@ public class ConferenceDetailsActivity extends XmppActivity } menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null); menuItemDestroyRoom.setVisible( - mConversation - .getMucOptions() - .getSelf() - .getAffiliation() - .ranks(MucOptions.Affiliation.OWNER)); + mConversation.getMucOptions().getSelf().ranks(Affiliation.OWNER)); return true; } @@ -461,11 +487,33 @@ public class ConferenceDetailsActivity extends XmppActivity } protected void saveAsBookmark() { - xmppConnectionService.saveConversationAsBookmark( - mConversation, mConversation.getMucOptions().getName()); + final var account = mConversation.getAccount(); + account.getXmppConnection() + .getManager(BookmarkManager.class) + .save(mConversation, mConversation.getMucOptions().getName()); } protected void destroyRoom() { + final var destroyCallBack = + new FutureCallback() { + + @Override + public void onSuccess(Void result) { + finish(); + } + + @Override + public void onFailure(Throwable t) { + final boolean groupChat = + mConversation != null && mConversation.isPrivateAndNonAnonymous(); + // TODO show toast directly + displayToast( + getString( + groupChat + ? R.string.could_not_destroy_room + : R.string.could_not_destroy_channel)); + } + }; final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel); @@ -474,8 +522,11 @@ public class ConferenceDetailsActivity extends XmppActivity builder.setPositiveButton( R.string.ok, (dialog, which) -> { - xmppConnectionService.destroyRoom( - mConversation, ConferenceDetailsActivity.this); + final var future = xmppConnectionService.destroyRoom(mConversation); + Futures.addCallback( + future, + destroyCallBack, + ContextCompat.getMainExecutor(getApplication())); }); builder.setNegativeButton(R.string.cancel, null); final AlertDialog dialog = builder.create(); @@ -528,8 +579,7 @@ public class ConferenceDetailsActivity extends XmppActivity ? R.string.action_muc_details : R.string.channel_details); this.binding.editMucNameButton.setVisibility( - (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) - || mucOptions.canChangeSubject()) + (self.ranks(Affiliation.OWNER) || mucOptions.canChangeSubject()) ? View.VISIBLE : View.GONE); this.binding.detailsAccount.setText(getString(R.string.using_account, account)); @@ -579,7 +629,7 @@ public class ConferenceDetailsActivity extends XmppActivity this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); this.binding.mucRole.setVisibility(View.VISIBLE); this.binding.mucRole.setText(getStatus(self)); - if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + if (mucOptions.getSelf().ranks(Affiliation.OWNER)) { this.binding.mucSettings.setVisibility(View.VISIBLE); this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions)); } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) { @@ -594,7 +644,7 @@ public class ConferenceDetailsActivity extends XmppActivity } else { this.binding.mucInfoMam.setText(R.string.server_info_unavailable); } - if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + if (self.ranks(Affiliation.OWNER)) { this.binding.changeConferenceButton.setVisibility(View.VISIBLE); } else { this.binding.changeConferenceButton.setVisibility(View.INVISIBLE); @@ -627,9 +677,9 @@ public class ConferenceDetailsActivity extends XmppActivity Collections.sort( users, (a, b) -> { - if (b.getAffiliation().outranks(a.getAffiliation())) { + if (b.outranks(a.getAffiliation())) { return 1; - } else if (a.getAffiliation().outranks(b.getAffiliation())) { + } else if (a.outranks(b.getAffiliation())) { return -1; } else { if (a.getAvatar() != null && b.getAvatar() == null) { @@ -668,13 +718,32 @@ public class ConferenceDetailsActivity extends XmppActivity if (advanced) { return String.format( "%s (%s)", - context.getString(user.getAffiliation().getResId()), - context.getString(user.getRole().getResId())); + context.getString(affiliationToStringRes(user.getAffiliation())), + context.getString(roleToStringRes(user.getRole()))); } else { - return context.getString(user.getAffiliation().getResId()); + return context.getString(affiliationToStringRes(user.getAffiliation())); } } + private static @StringRes int affiliationToStringRes(final Affiliation affiliation) { + return switch (affiliation) { + case OWNER -> R.string.owner; + case ADMIN -> R.string.admin; + case MEMBER -> R.string.member; + case NONE -> R.string.no_affiliation; + case OUTCAST -> R.string.outcast; + }; + } + + private static @StringRes int roleToStringRes(final Role role) { + return switch (role) { + case MODERATOR -> R.string.moderator; + case VISITOR -> R.string.visitor; + case PARTICIPANT -> R.string.participant; + case NONE -> R.string.no_role; + }; + } + private String getStatus(User user) { return getStatus(this, user, mAdvancedMode); } @@ -689,31 +758,6 @@ public class ConferenceDetailsActivity extends XmppActivity displayToast(getString(resId, jid.asBareJid().toString())); } - @Override - public void onRoomDestroySucceeded() { - finish(); - } - - @Override - public void onRoomDestroyFailed() { - final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); - displayToast( - getString( - groupChat - ? R.string.could_not_destroy_room - : R.string.could_not_destroy_channel)); - } - - @Override - public void onPushSucceeded() { - displayToast(getString(R.string.modified_conference_options)); - } - - @Override - public void onPushFailed() { - displayToast(getString(R.string.could_not_modify_conference_options)); - } - private void displayToast(final String msg) { runOnUiThread( () -> { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 73d8dc55c4688fd671a5465a0ecb0232193ef0b2..5041248909f56c87ea176932b46fe2e9d2a47550 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -128,6 +128,7 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.manager.HttpUploadManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; @@ -1178,9 +1179,9 @@ public class ConversationFragment extends XmppFragment } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); + final var connection = this.conversation.getAccount().getXmppConnection(); menuInviteContact.setVisible( - service != null - && service.findConferenceServer(conversation.getAccount()) != null); + !connection.getManager(MultiUserChatManager.class).getServices().isEmpty()); } if (conversation.isMuted()) { menuMute.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java index 7351823f6708e2e87ad6073f092e64da84b04f8e..461d0780150b2f6b2cc76808e9c219b60e825b17 100644 --- a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.ui.util.DelayedHintHelper; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -158,7 +159,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke } final Editable nameText = binding.groupChatName.getText(); final String name = nameText == null ? "" : nameText.toString().trim(); - final String domain = connection.getMucServer(); + final var domain = connection.getManager(MultiUserChatManager.class).getService(); if (domain == null) { return ""; } @@ -270,9 +271,8 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke private void refreshKnownHosts() { Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - Collection hosts = - ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts(); + if (activity instanceof XmppActivity xmppActivity) { + Collection hosts = xmppActivity.xmppConnectionService.getKnownConferenceHosts(); this.knownHostsAdapter.refresh(hosts); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java index a48b22233308e71dd7699ac31cfaf20cbf8d8a48..f60716ad7c32fd35dad8caf0b9f4328ff8c7eda5 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java @@ -7,17 +7,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; - import com.google.common.base.Strings; - -import org.openintents.openpgp.util.OpenPgpUtils; - import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.databinding.ItemContactBinding; @@ -30,30 +25,36 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.muc.Role; +import org.openintents.openpgp.util.OpenPgpUtils; -public class UserAdapter extends ListAdapter implements View.OnCreateContextMenuListener { - - static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) { - final Jid fullA = a.getFullJid(); - final Jid fullB = b.getFullJid(); - final Jid realA = a.getRealJid(); - final Jid realB = b.getRealJid(); - if (fullA != null && fullB != null) { - return fullA.equals(fullB); - } else if (realA != null && realB != null) { - return realA.equals(realB); - } else { - return false; - } - } +public class UserAdapter extends ListAdapter + implements View.OnCreateContextMenuListener { + + static final DiffUtil.ItemCallback DIFF = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame( + @NonNull MucOptions.User a, @NonNull MucOptions.User b) { + final Jid fullA = a.getFullJid(); + final Jid fullB = b.getFullJid(); + final Jid realA = a.getRealJid(); + final Jid realB = b.getRealJid(); + if (fullA != null && fullB != null) { + return fullA.equals(fullB); + } else if (realA != null && realB != null) { + return realA.equals(realB); + } else { + return false; + } + } - @Override - public boolean areContentsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) { - return a.equals(b); - } - }; + @Override + public boolean areContentsTheSame( + @NonNull MucOptions.User a, @NonNull MucOptions.User b) { + return a.equals(b); + } + }; private final boolean advancedMode; private MucOptions.User selectedUser = null; @@ -65,73 +66,104 @@ public class UserAdapter extends ListAdapter { - final XmppActivity activity = XmppActivity.find(v); - if (activity == null) { - return; - } - final var contact = user.getContact(); - if (user.getRole() == MucOptions.Role.NONE && contact != null) { - Toast.makeText( - activity, - activity.getString( - R.string.user_has_left_conference, - contact.getDisplayName()), - Toast.LENGTH_SHORT) - .show(); - } - activity.highlightInMuc(user.getConversation(), user.getName()); - }); + viewHolder + .binding + .getRoot() + .setOnClickListener( + v -> { + final XmppActivity activity = XmppActivity.find(v); + if (activity == null) { + return; + } + final var contact = user.getContact(); + if (user.getRole() == Role.NONE && contact != null) { + Toast.makeText( + activity, + activity.getString( + R.string.user_has_left_conference, + contact.getDisplayName()), + Toast.LENGTH_SHORT) + .show(); + } + activity.highlightInMuc(user.getConversation(), user.getName()); + }); viewHolder.binding.getRoot().setTag(user); viewHolder.binding.getRoot().setOnCreateContextMenuListener(this); - viewHolder.binding.getRoot().setOnLongClickListener(v -> { - selectedUser = user; - return false; - }); + viewHolder + .binding + .getRoot() + .setOnLongClickListener( + v -> { + selectedUser = user; + return false; + }); final String name = user.getName(); final Contact contact = user.getContact(); if (contact != null) { final String displayName = contact.getDisplayName(); viewHolder.binding.contactDisplayName.setText(displayName); if (name != null && !name.equals(displayName)) { - viewHolder.binding.contactJid.setText(String.format("%s \u2022 %s", name, ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode))); + viewHolder.binding.contactJid.setText( + String.format( + "%s \u2022 %s", + name, + ConferenceDetailsActivity.getStatus( + viewHolder.binding.getRoot().getContext(), + user, + advancedMode))); } else { - viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)); + viewHolder.binding.contactJid.setText( + ConferenceDetailsActivity.getStatus( + viewHolder.binding.getRoot().getContext(), user, advancedMode)); } } else { viewHolder.binding.contactDisplayName.setText(Strings.nullToEmpty(name)); - viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)); + viewHolder.binding.contactJid.setText( + ConferenceDetailsActivity.getStatus( + viewHolder.binding.getRoot().getContext(), user, advancedMode)); } if (advancedMode && user.getPgpKeyId() != 0) { viewHolder.binding.key.setVisibility(View.VISIBLE); - viewHolder.binding.key.setOnClickListener(v -> { - final XmppActivity activity = XmppActivity.find(v); - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine(); - if (pgpEngine != null) { - PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId()); - if (intent != null) { - try { - activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); - } catch (IntentSender.SendIntentException ignored) { - + viewHolder.binding.key.setOnClickListener( + v -> { + final XmppActivity activity = XmppActivity.find(v); + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; + final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine(); + if (pgpEngine != null) { + PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId()); + if (intent != null) { + try { + activity.startIntentSenderForResult( + intent.getIntentSender(), + 0, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); + } catch (IntentSender.SendIntentException ignored) { + + } + } } - } - } - }); + }); viewHolder.binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); } else { viewHolder.binding.key.setVisibility(View.GONE); } - - } public MucOptions.User getSelectedUser() { @@ -139,8 +171,9 @@ public class UserAdapter extends ListAdapter implements View.OnCreateContextMenuListener { @@ -52,7 +51,7 @@ public class UserPreviewAdapter extends ListAdapter toBundle(boolean[] values) { + final var builder = new ImmutableMap.Builder(); for (int i = 0; i < values.length; ++i) { final Option option = options[i]; - bundle.putString(option.name, option.values[values[i] ? 0 : 1]); + builder.put(option.name, option.values[values[i] ? 0 : 1]); } - return bundle; + builder.put("muc#roomconfig_persistentroom", true); + return builder.buildOrThrow(); } private static class Option { public final String name; - public final String[] values; + public final Object[] values; private Option(String name) { this.name = name; - this.values = new String[] {"1", "0"}; + this.values = new Boolean[] {true, false}; } private Option(String name, String on, String off) { diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java index 6141ab07889b0d9152ea9a3b7d4e9343023966bd..43df19d751011204654c71d4337500a094e969dd 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java @@ -24,6 +24,8 @@ import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.MucUsersActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; public final class MucDetailsContextMenuHelper { @@ -81,18 +83,17 @@ public final class MucDetailsContextMenuHelper { } if ((activity instanceof ConferenceDetailsActivity || activity instanceof MucUsersActivity) - && user.getRole() == MucOptions.Role.NONE) { + && user.getRole() == Role.NONE) { invite.setVisible(true); } boolean managePermissionsVisible = false; - if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) - && self.getAffiliation().outranks(user.getAffiliation())) - || self.getAffiliation() == MucOptions.Affiliation.OWNER) { + if ((self.ranks(Affiliation.ADMIN) && self.outranks(user.getAffiliation())) + || self.getAffiliation() == Affiliation.OWNER) { if (advancedMode) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { + if (!user.ranks(Affiliation.MEMBER)) { managePermissionsVisible = true; giveMembership.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) { + } else if (user.getAffiliation() == Affiliation.MEMBER) { managePermissionsVisible = true; removeMembership.setVisible(true); } @@ -106,25 +107,21 @@ public final class MucDetailsContextMenuHelper { } } } - if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - if (isGroupChat - || advancedMode - || user.getAffiliation() == MucOptions.Affiliation.OWNER) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { + if (self.ranks(Affiliation.OWNER)) { + if (isGroupChat || advancedMode || user.getAffiliation() == Affiliation.OWNER) { + if (!user.ranks(Affiliation.OWNER)) { managePermissionsVisible = true; giveOwnerPrivileges.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER) { + } else if (user.getAffiliation() == Affiliation.OWNER) { managePermissionsVisible = true; removeOwnerPrivileges.setVisible(true); } } - if (!isGroupChat - || advancedMode - || user.getAffiliation() == MucOptions.Affiliation.ADMIN) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) { + if (!isGroupChat || advancedMode || user.getAffiliation() == Affiliation.ADMIN) { + if (!user.ranks(Affiliation.ADMIN)) { managePermissionsVisible = true; giveAdminPrivileges.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) { + } else if (user.getAffiliation() == Affiliation.ADMIN) { managePermissionsVisible = true; removeAdminPrivileges.setVisible(true); } @@ -132,15 +129,11 @@ public final class MucDetailsContextMenuHelper { } managePermissions.setVisible(managePermissionsVisible); sendPrivateMessage.setVisible( - !isGroupChat - && mucOptions.allowPm() - && user.getRole().ranks(MucOptions.Role.VISITOR)); + !isGroupChat && mucOptions.allowPm() && user.ranks(Role.VISITOR)); } else { sendPrivateMessage.setVisible(true); sendPrivateMessage.setEnabled( - user != null - && mucOptions.allowPm() - && user.getRole().ranks(MucOptions.Role.VISITOR)); + user != null && mucOptions.allowPm() && user.ranks(Role.VISITOR)); } } @@ -171,31 +164,31 @@ public final class MucDetailsContextMenuHelper { return true; case R.id.give_admin_privileges: activity.xmppConnectionService.changeAffiliationInConference( - conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged); + conversation, jid, Affiliation.ADMIN, onAffiliationChanged); return true; case R.id.give_membership: case R.id.remove_admin_privileges: case R.id.revoke_owner_privileges: activity.xmppConnectionService.changeAffiliationInConference( - conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); + conversation, jid, Affiliation.MEMBER, onAffiliationChanged); return true; case R.id.give_owner_privileges: activity.xmppConnectionService.changeAffiliationInConference( - conversation, jid, MucOptions.Affiliation.OWNER, onAffiliationChanged); + conversation, jid, Affiliation.OWNER, onAffiliationChanged); return true; case R.id.remove_membership: activity.xmppConnectionService.changeAffiliationInConference( - conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged); + conversation, jid, Affiliation.NONE, onAffiliationChanged); return true; case R.id.remove_from_room: removeFromRoom(user, activity, onAffiliationChanged); return true; case R.id.ban_from_conference: activity.xmppConnectionService.changeAffiliationInConference( - conversation, jid, MucOptions.Affiliation.OUTCAST, onAffiliationChanged); - if (user.getRole() != MucOptions.Role.NONE) { + conversation, jid, Affiliation.OUTCAST, onAffiliationChanged); + if (user.getRole() != Role.NONE) { activity.xmppConnectionService.changeRoleInConference( - conversation, user.getName(), MucOptions.Role.NONE); + conversation, user.getName(), Role.NONE); } return true; case R.id.send_private_message: @@ -210,7 +203,7 @@ public final class MucDetailsContextMenuHelper { return true; case R.id.invite: // TODO use direct invites for public conferences - if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { + if (user.ranks(Affiliation.MEMBER)) { activity.xmppConnectionService.directInvite(conversation, jid.asBareJid()); } else { activity.xmppConnectionService.invite(conversation, jid); @@ -228,13 +221,10 @@ public final class MucDetailsContextMenuHelper { final Conversation conversation = user.getConversation(); if (conversation.getMucOptions().membersOnly()) { activity.xmppConnectionService.changeAffiliationInConference( - conversation, - user.getRealJid(), - MucOptions.Affiliation.NONE, - onAffiliationChanged); - if (user.getRole() != MucOptions.Role.NONE) { + conversation, user.getRealJid(), Affiliation.NONE, onAffiliationChanged); + if (user.getRole() != Role.NONE) { activity.xmppConnectionService.changeRoleInConference( - conversation, user.getName(), MucOptions.Role.NONE); + conversation, user.getName(), Role.NONE); } } else { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); @@ -259,11 +249,11 @@ public final class MucDetailsContextMenuHelper { activity.xmppConnectionService.changeAffiliationInConference( conversation, user.getRealJid(), - MucOptions.Affiliation.OUTCAST, + Affiliation.OUTCAST, onAffiliationChanged); - if (user.getRole() != MucOptions.Role.NONE) { + if (user.getRole() != Role.NONE) { activity.xmppConnectionService.changeRoleInConference( - conversation, user.getName(), MucOptions.Role.NONE); + conversation, user.getName(), Role.NONE); } }); builder.create().show(); diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index a283d8496db75ffeb36b5debc4d7775e20635207..4d65b54fb1cd9ea049b0bfdcd73abf25ba79651b 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -16,6 +16,7 @@ import com.google.common.primitives.Ints; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import de.gultsch.common.FutureMerger; import de.gultsch.minidns.AndroidDNSClient; import de.gultsch.minidns.ResolverResult; import eu.siacs.conversations.Config; @@ -115,7 +116,7 @@ public class Resolver { final var startTls = resolveSrvAsFuture(domain, false); final var directTls = resolveSrvAsFuture(domain, true); - final var combined = merge(ImmutableList.of(startTls, directTls)); + final var combined = FutureMerger.successfulAsList(ImmutableList.of(startTls, directTls)); final var combinedWithFallback = Futures.transformAsync( @@ -206,7 +207,7 @@ public class Resolver { futuresBuilder.add(ipv6s); } final ImmutableList>> futures = futuresBuilder.build(); - return merge(futures); + return FutureMerger.successfulAsList(futures); } private static ListenableFuture> merge( @@ -284,13 +285,13 @@ public class Resolver { Lists.transform( ImmutableList.copyOf(result.getAnswersOrEmptySet()), cname -> resolveNoSrvAsFuture(cname.target, false)); - return merge(test); + return FutureMerger.successfulAsList(test); }, MoreExecutors.directExecutor()); futuresBuilder.add(cNameRecordResults); } final ImmutableList>> futures = futuresBuilder.build(); - final var noSrvFallbacks = merge(futures); + final var noSrvFallbacks = FutureMerger.successfulAsList(futures); return Futures.transform( noSrvFallbacks, results -> { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 392f74531e9f82af013202750bfa00e4a9bd3593..452f1419b227fe7f6a1b73d96d7789cf89a0ae1f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xml; import androidx.annotation.NonNull; +import com.google.common.base.CaseFormat; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.primitives.Ints; @@ -123,6 +124,16 @@ public class Element { return this; } + public Element setAttribute(final String name, final Enum e) { + if (e == null) { + this.attributes.remove(name); + } else { + this.attributes.put( + name, CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString())); + } + return this; + } + public Element setAttribute(String name, Jid value) { if (name != null && value != null) { this.attributes.put(name, value.toString()); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 00514d99db638daf8db9c2fb62be521cfc009331..1be3cda1c7ec3b3100a7df304b2b9c83e5c5dc6d 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -20,6 +20,7 @@ public final class Namespace { public static final String REACTIONS = "urn:xmpp:reactions:0"; public static final String VCARD_TEMP = "vcard-temp"; public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update"; + public static final String DIRECT_MUC_INVITATIONS = "jabber:x:conference"; public static final String DELAY = "urn:xmpp:delay"; public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0"; public static final String STREAMS = "http://etherx.jabber.org/streams"; @@ -49,6 +50,9 @@ public final class Namespace { public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_EVENT = PUBSUB + "#event"; public static final String MUC = "http://jabber.org/protocol/muc"; + public static final String MUC_ADMIN = MUC + "#admin"; + public static final String MUC_OWNER = MUC + "#owner"; + public static final String MUC_USER = MUC + "#user"; public static final String MUC_ROOM_INFO = MUC + "#roominfo"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; @@ -96,7 +100,6 @@ public final class Namespace { public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; public static final String COMMANDS = "http://jabber.org/protocol/commands"; - public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1"; public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat"; public static final String PRE_AUTHENTICATED_IN_BAND_REGISTRATION = "urn:xmpp:ibr-token:0"; @@ -114,7 +117,6 @@ public final class Namespace { public static final String MEDIA_ELEMENT = "urn:xmpp:media-element"; public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; - public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; public static final String PRIVATE_XML_STORAGE = "jabber:iq:private"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java index 7959c8ec4b985bf73cbaaa168143f4fed4c0c99d..ea4ed3a4f5db012e68470dc44e653dc1486b5ff0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java +++ b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java @@ -18,6 +18,14 @@ public class IqErrorException extends Exception { return this.response.getError(); } + public Condition getErrorCondition() { + final var error = getError(); + if (error == null) { + return null; + } + return error.getCondition(); + } + private static String getErrorText(final Iq response) { final var error = response.getError(); final var text = error == null ? null : error.getText(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index b14da3b7d3176eb60c0b45957877cdd6465d4305..93a89db2a7835e43137004f35f2537eaac04e71a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -15,6 +15,7 @@ import eu.siacs.conversations.xmpp.manager.HttpUploadManager; import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager; import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; +import eu.siacs.conversations.xmpp.manager.NativeBookmarkManager; import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager; import eu.siacs.conversations.xmpp.manager.PepManager; @@ -50,6 +51,7 @@ public class Managers { MessageDisplayedSynchronizationManager.class, new MessageDisplayedSynchronizationManager(context, connection)) .put(MultiUserChatManager.class, new MultiUserChatManager(context, connection)) + .put(NativeBookmarkManager.class, new NativeBookmarkManager(context, connection)) .put(NickManager.class, new NickManager(context, connection)) .put(OfflineMessagesManager.class, new OfflineMessagesManager(context, connection)) .put(PepManager.class, new PepManager(context, connection)) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index f19361cab73b87f888370d5d9f7d1472689d2cd1..6694f4286a81d52627994cc106d2144f9ef804ad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -18,7 +18,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ClassToInstanceMap; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -71,6 +73,7 @@ import eu.siacs.conversations.xmpp.manager.AbstractManager; import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.PingManager; import eu.siacs.conversations.xmpp.manager.RegistrationManager; import im.conversations.android.xmpp.Entity; @@ -85,7 +88,6 @@ import im.conversations.android.xmpp.model.bind2.Bound; import im.conversations.android.xmpp.model.cb.SaslChannelBinding; import im.conversations.android.xmpp.model.csi.Active; import im.conversations.android.xmpp.model.csi.Inactive; -import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.fast.Fast; import im.conversations.android.xmpp.model.fast.RequestToken; @@ -135,7 +137,6 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -2632,29 +2633,10 @@ public class XmppConnection implements Runnable { return this.managers.getInstance(clazz); } - public List getMucServersWithholdAccount() { - final List servers = getMucServers(); - servers.remove(account.getDomain().toString()); - return servers; - } - - public List getMucServers() { - List servers = new ArrayList<>(); - for (final Entry entry : - getManager(DiscoManager.class).getServerItems().entrySet()) { - final var value = entry.getValue(); - if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc") - && value.hasIdentityWithCategoryAndType("conference", "text") - && !value.getFeatureStrings().contains("jabber:iq:gateway") - && !value.hasIdentityWithCategoryAndType("conference", "irc")) { - servers.add(entry.getKey().toString()); - } - } - return servers; - } - - public String getMucServer() { - return Iterables.getFirst(getMucServers(), null); + public Set getMucServersWithholdAccount() { + final var services = getManager(MultiUserChatManager.class).getServices(); + return ImmutableSet.copyOf( + Collections2.filter(services, s -> !s.equals(account.getDomain()))); } public int getTimeToNextAttempt(final boolean aggressive) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java index 3039b1020d714a1d75fe0ffc0bd85e1e500fd281..93c42dfff666b31c649c4fe9a138dc95f3649dd1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java @@ -28,7 +28,7 @@ public class AbstractBookmarkManager extends AbstractManager { final Set previousBookmarks = account.getBookmarkedJids(); for (final Bookmark bookmark : bookmarks.values()) { previousBookmarks.remove(bookmark.getJid().asBareJid()); - service.processModifiedBookmark(bookmark, pep); + getManager(BookmarkManager.class).processModifiedBookmark(bookmark, pep); } if (pep) { this.processDeletedBookmarks(previousBookmarks); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java index 03a8cc3f3b085221e393214467bf25be2e88ecab..3faaf16e0fa259c76a4241c94acbed3cc90d5e54 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java @@ -1,150 +1,176 @@ package eu.siacs.conversations.xmpp.manager; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; -import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; -import im.conversations.android.xmpp.NodeConfiguration; -import im.conversations.android.xmpp.model.bookmark2.Conference; -import im.conversations.android.xmpp.model.bookmark2.Nick; -import im.conversations.android.xmpp.model.bookmark2.Password; -import im.conversations.android.xmpp.model.pubsub.Items; -import im.conversations.android.xmpp.model.pubsub.event.Retract; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -public class BookmarkManager extends AbstractBookmarkManager { +public class BookmarkManager extends AbstractManager { - public BookmarkManager(final XmppConnectionService service, XmppConnection connection) { - super(service, connection); + private final XmppConnectionService service; + + public BookmarkManager(final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; + } + + public void request() { + if (getManager(NativeBookmarkManager.class).hasFeature()) { + getManager(NativeBookmarkManager.class).fetch(); + } else if (getManager(LegacyBookmarkManager.class).hasConversion()) { + final var account = getAccount(); + Log.d( + Config.LOGTAG, + account.getJid() + ": not fetching bookmarks. waiting for server to push"); + } else { + getManager(PrivateStorageManager.class).fetchBookmarks(); + } } - public void fetch() { - final var future = getManager(PepManager.class).fetchItems(Conference.class); + public void save(final Conversation conversation, final String name) { + final Account account = conversation.getAccount(); + final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + final String nick = conversation.getJid().getResource(); + if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { + bookmark.setNick(nick); + } + if (!TextUtils.isEmpty(name)) { + bookmark.setBookmarkName(name); + } + bookmark.setAutojoin(true); + this.create(bookmark); + bookmark.setConversation(conversation); + } + + public void create(final Bookmark bookmark) { + final var account = getAccount(); + account.putBookmark(bookmark); + final ListenableFuture future; + if (getManager(NativeBookmarkManager.class).hasFeature()) { + future = getManager(NativeBookmarkManager.class).publish(bookmark); + } else if (getManager(LegacyBookmarkManager.class).hasConversion()) { + future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks()); + } else { + future = + getManager(PrivateStorageManager.class) + .publishBookmarks(account.getBookmarks()); + } Futures.addCallback( future, new FutureCallback<>() { @Override - public void onSuccess(final Map bookmarks) { - final var builder = new ImmutableMap.Builder(); - for (final var entry : bookmarks.entrySet()) { - final Bookmark bookmark = - Bookmark.parseFromItem( - entry.getKey(), entry.getValue(), getAccount()); - if (bookmark == null) { - continue; - } - builder.put(bookmark.getJid(), bookmark); - } - processBookmarksInitial(builder.buildKeepingLast(), true); + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": created bookmark"); } @Override - public void onFailure(@NonNull final Throwable throwable) { - Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable); + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not create bookmark", + t); } }, MoreExecutors.directExecutor()); } - public void handleItems(final Items items) { - this.handleItems(items.getItemMap(Conference.class)); - this.handleRetractions(items.getRetractions()); - } - - private void handleRetractions(final Collection retractions) { + public void delete(final Bookmark bookmark) { final var account = getAccount(); - for (final var retract : retractions) { - final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id")); - if (id != null) { - account.removeBookmark(id); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); - processDeletedBookmark(id); - service.updateConversationUi(); - } + account.removeBookmark(bookmark); + final ListenableFuture future; + if (getManager(NativeBookmarkManager.class).hasFeature()) { + future = getManager(NativeBookmarkManager.class).retract(bookmark.getJid().asBareJid()); + } else if (getManager(LegacyBookmarkManager.class).hasConversion()) { + future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks()); + } else { + future = + getManager(PrivateStorageManager.class) + .publishBookmarks(account.getBookmarks()); } + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark"); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not delete bookmark", + t); + } + }, + MoreExecutors.directExecutor()); } - private void handleItems(final Map items) { + public void ensureBookmarkIsAutoJoin(final Conversation conversation) { final var account = getAccount(); - for (final var item : items.entrySet()) { - // TODO parseFromItem can be included in this Manager - final Bookmark bookmark = - Bookmark.parseFromItem(item.getKey(), item.getValue(), account); - if (bookmark == null) { - continue; + final var existingBookmark = conversation.getBookmark(); + if (existingBookmark == null) { + final var bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + bookmark.setAutojoin(true); + create(bookmark); + } else { + if (existingBookmark.autojoin()) { + return; } - account.putBookmark(bookmark); - service.processModifiedBookmark(bookmark); - service.updateConversationUi(); + existingBookmark.setAutojoin(true); + create(existingBookmark); } } - public ListenableFuture publish(final Bookmark bookmark) { - final var address = bookmark.getJid(); - final var name = bookmark.getBookmarkName(); - final var nick = bookmark.getNick(); - final String password = bookmark.getPassword(); - final var itemId = address.toString(); - final var conference = new Conference(); - conference.setAutoJoin(bookmark.autojoin()); - if (nick != null) { - conference.addExtension(new Nick()).setContent(nick); - } - if (name != null) { - conference.setConferenceName(name); - } - if (password != null) { - conference.addExtension(new Password()).setContent(password); + public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { + final var existing = this.service.find(bookmark); + if (existing != null) { + if (existing.getMode() != Conversation.MODE_MULTI) { + return; + } + bookmark.setConversation(existing); + if (pep && !bookmark.autojoin()) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": archiving conference (" + + existing.getJid() + + ") after receiving pep"); + service.archiveConversation(existing, false); + } else { + final MucOptions mucOptions = existing.getMucOptions(); + if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) { + final String current = mucOptions.getActualNick(); + final String proposed = mucOptions.getProposedNickPure(); + if (current != null && !current.equals(proposed)) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": proposed nick changed after bookmark push " + + current + + "->" + + proposed); + getManager(MultiUserChatManager.class).join(existing); + } + } else { + getManager(MultiUserChatManager.class).checkMucRequiresRename(existing); + } + } + } else if (bookmark.autojoin()) { + final var fresh = + this.service.findOrCreateConversation( + getAccount(), bookmark.getFullJid(), true, true, false); + bookmark.setConversation(fresh); } - conference.addExtension(bookmark.getExtensions()); - return Futures.transform( - getManager(PepManager.class) - .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS), - result -> null, - MoreExecutors.directExecutor()); - } - - public ListenableFuture retract(final Jid address) { - final var itemId = address.toString(); - return Futures.transform( - getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2), - result -> null, - MoreExecutors.directExecutor()); - } - - private void deleteAllItems() { - final var account = getAccount(); - final var previous = account.getBookmarkedJids(); - account.setBookmarks(Collections.emptyMap()); - processDeletedBookmarks(previous); - } - - public void handleDelete() { - Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": deleted bookmarks node"); - this.deleteAllItems(); - } - - public void handlePurge() { - Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": purged bookmarks"); - this.deleteAllItems(); - } - - public boolean hasFeature() { - final var pep = getManager(PepManager.class); - final var disco = getManager(DiscoManager.class); - return pep.hasPublishOptions() - && pep.hasConfigNodeMax() - && disco.hasAccountFeature(Namespace.BOOKMARKS2_COMPAT); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index 0650c4980799647b9fba9f8bcda57dbd7eaeef53..106fbd1ab99a4f0358cb5b266cc26611111d7a9a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -336,7 +336,7 @@ public class DiscoManager extends AbstractManager { if (appSettings.isBroadcastLastActivity()) { features.add(Namespace.IDLE); } - if (getManager(BookmarkManager.class).hasFeature()) { + if (getManager(NativeBookmarkManager.class).hasFeature()) { features.add(Namespace.BOOKMARKS2 + "+notify"); } else { features.add(Namespace.BOOKMARKS + "+notify"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java index 0730ab088b671e801b661db20709129e28615e88..0a0bdb525ac1e1c83b3252a87a47ad571c394599 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java @@ -24,7 +24,7 @@ public class LegacyBookmarkManager extends AbstractBookmarkManager { public void handleItems(final Items items) { final var account = this.getAccount(); if (this.hasConversion()) { - if (getManager(BookmarkManager.class).hasFeature()) { + if (getManager(NativeBookmarkManager.class).hasFeature()) { Log.w( Config.LOGTAG, account.getJid().asBareJid() diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java index 8bd5b39be4c9accfa1894602f5f8bda1396bb204..911ac2faff51f995a6a1e1bb2645b78143425cc7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java @@ -1,14 +1,988 @@ package eu.siacs.conversations.xmpp.manager; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import de.gultsch.common.FutureMerger; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.StringUtils; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.conference.DirectInvite; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.hints.NoCopy; +import im.conversations.android.xmpp.model.hints.NoStore; +import im.conversations.android.xmpp.model.jabber.Subject; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.History; +import im.conversations.android.xmpp.model.muc.MultiUserChat; +import im.conversations.android.xmpp.model.muc.Password; +import im.conversations.android.xmpp.model.muc.Role; +import im.conversations.android.xmpp.model.muc.admin.Item; +import im.conversations.android.xmpp.model.muc.admin.MucAdmin; +import im.conversations.android.xmpp.model.muc.owner.Destroy; +import im.conversations.android.xmpp.model.muc.owner.MucOwner; +import im.conversations.android.xmpp.model.muc.user.Invite; +import im.conversations.android.xmpp.model.muc.user.MucUser; +import im.conversations.android.xmpp.model.pgp.Signed; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Message; +import im.conversations.android.xmpp.model.stanza.Presence; +import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; public class MultiUserChatManager extends AbstractManager { private final XmppConnectionService service; + private final Set inProgressConferenceJoins = new HashSet<>(); + private final Set inProgressConferencePings = new HashSet<>(); + public MultiUserChatManager(final XmppConnectionService service, XmppConnection connection) { super(service.getApplicationContext(), connection); this.service = service; } + + public ListenableFuture join(final Conversation conversation) { + return join(conversation, true); + } + + private ListenableFuture join( + final Conversation conversation, final boolean autoPushConfiguration) { + final var account = getAccount(); + synchronized (this.inProgressConferenceJoins) { + this.inProgressConferenceJoins.add(conversation); + } + if (Config.MUC_LEAVE_BEFORE_JOIN) { + unavailable(conversation); + } + conversation.resetMucOptions(); + conversation.getMucOptions().setAutoPushConfiguration(autoPushConfiguration); + conversation.setHasMessagesLeftOnServer(false); + final var disco = fetchDiscoInfo(conversation); + + final var caughtDisco = + Futures.catchingAsync( + disco, + IqErrorException.class, + ex -> { + if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + return Futures.immediateFailedFuture( + new IllegalStateException( + "conversation got archived before disco returned")); + } + Log.d(Config.LOGTAG, "error fetching disco#info", ex); + final var iqError = ex.getError(); + if (iqError != null + && iqError.getCondition() + instanceof Condition.RemoteServerNotFound) { + synchronized (this.inProgressConferenceJoins) { + this.inProgressConferenceJoins.remove(conversation); + } + conversation + .getMucOptions() + .setError(MucOptions.Error.SERVER_NOT_FOUND); + service.updateConversationUi(); + return Futures.immediateFailedFuture(ex); + } else { + return Futures.immediateFuture(new InfoQuery()); + } + }, + MoreExecutors.directExecutor()); + + return Futures.transform( + caughtDisco, + v -> { + checkConfigurationSendPresenceFetchHistory(conversation); + return null; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture joinFollowingInvite(final Conversation conversation) { + // TODO this special treatment is probably unnecessary; just always make sure the bookmark + // exists + return Futures.transform( + join(conversation), + v -> { + // we used to do this only for private groups + final Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + if (bookmark.autojoin()) { + return null; + } + bookmark.setAutojoin(true); + getManager(BookmarkManager.class).create(bookmark); + } else { + getManager(BookmarkManager.class).save(conversation, null); + } + return null; + }, + MoreExecutors.directExecutor()); + } + + private void checkConfigurationSendPresenceFetchHistory(final Conversation conversation) { + + Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); + + if (mucOptions.nonanonymous() + && !mucOptions.membersOnly() + && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { + synchronized (this.inProgressConferenceJoins) { + this.inProgressConferenceJoins.remove(conversation); + } + mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); + service.updateConversationUi(); + return; + } + + final Jid joinJid = mucOptions.getSelf().getFullJid(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": joining conversation " + + joinJid.toString()); + + final var x = new MultiUserChat(); + + if (mucOptions.getPassword() != null) { + x.addExtension(new Password(mucOptions.getPassword())); + } + + final var history = x.addExtension(new History()); + + if (mucOptions.mamSupport()) { + // Use MAM instead of the limited muc history to get history + history.setMaxStanzas(0); + } else { + // Fallback to muc history + history.setSince(conversation.getLastMessageTransmitted().getTimestamp()); + } + available(joinJid, mucOptions.nonanonymous(), x); + if (!joinJid.equals(conversation.getJid())) { + conversation.setContactJid(joinJid); + getDatabase().updateConversation(conversation); + } + + if (mucOptions.mamSupport()) { + this.service.getMessageArchiveService().catchupMUC(conversation); + } + if (mucOptions.isPrivateAndNonAnonymous()) { + fetchMembers(conversation); + } + synchronized (this.inProgressConferenceJoins) { + this.inProgressConferenceJoins.remove(conversation); + this.service.sendUnsentMessages(conversation); + } + } + + public ListenableFuture createPrivateGroupChat( + final String name, final Collection addresses) { + final var service = getService(); + if (service == null) { + return Futures.immediateFailedFuture(new IllegalStateException("No MUC service found")); + } + final var address = Jid.ofLocalAndDomain(CryptoHelper.pronounceable(), service); + final var conversation = + this.service.findOrCreateConversation(getAccount(), address, true, false, true); + final var join = this.join(conversation, false); + final var configured = + Futures.transformAsync( + join, + v -> { + final var options = + configWithName(defaultGroupChatConfiguration(), name); + return pushConfiguration(conversation, options); + }, + MoreExecutors.directExecutor()); + + // TODO add catching to 'configured' to archive the chat again + + return Futures.transform( + configured, + c -> { + for (var invitee : addresses) { + this.service.invite(conversation, invitee); + } + final var account = getAccount(); + for (final var resource : + account.getSelfContact().getPresences().toResourceArray()) { + Jid other = getAccount().getJid().withResource(resource); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": sending direct invite to " + + other); + this.service.directInvite(conversation, other); + } + getManager(BookmarkManager.class).save(conversation, name); + return conversation; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture createPublicChannel( + final Jid address, final String name) { + + final var conversation = + this.service.findOrCreateConversation(getAccount(), address, true, false, true); + + final var join = this.join(conversation, false); + final var configuration = + Futures.transformAsync( + join, + v -> { + final var options = configWithName(defaultChannelConfiguration(), name); + return pushConfiguration(conversation, options); + }, + MoreExecutors.directExecutor()); + + // TODO mostly ignore configuration error + + return Futures.transform( + configuration, + v -> { + getManager(BookmarkManager.class).save(conversation, name); + return conversation; + }, + MoreExecutors.directExecutor()); + } + + public void leave(final Conversation conversation) { + final var mucOptions = conversation.getMucOptions(); + mucOptions.setOffline(); + getManager(DiscoManager.class).clear(conversation.getJid().asBareJid()); + unavailable(conversation); + } + + public void handlePresence(final Presence presence) {} + + public void handleStatusMessage(final Message message) { + final var from = Jid.Invalid.getNullForInvalid(message.getFrom()); + final var mucUser = message.getExtension(MucUser.class); + if (from == null || from.isFullJid() || mucUser == null) { + return; + } + final var conversation = this.service.find(getAccount(), from); + if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) { + return; + } + for (final var status : mucUser.getStatus()) { + handleStatusCode(conversation, status); + } + final var item = mucUser.getItem(); + if (item == null) { + return; + } + final var user = itemToUser(conversation, item, null); + this.handleAffiliationChange(conversation, user); + } + + private void handleAffiliationChange( + final Conversation conversation, final MucOptions.User user) { + final var account = getAccount(); + Log.d( + Config.LOGTAG, + account.getJid() + + ": changing affiliation for " + + user.getRealJid() + + " to " + + user.getAffiliation() + + " in " + + conversation.getJid().asBareJid()); + if (user.realJidMatchesAccount()) { + return; + } + final var mucOptions = conversation.getMucOptions(); + final boolean isNew = mucOptions.updateUser(user); + final var avatarService = this.service.getAvatarService(); + if (Strings.isNullOrEmpty(mucOptions.getAvatar())) { + avatarService.clear(mucOptions); + } + avatarService.clear(user); + this.service.updateMucRosterUi(); + this.service.updateConversationUi(); + if (user.ranks(Affiliation.MEMBER)) { + fetchDeviceIdsIfNeeded(isNew, user); + } else { + final var jid = user.getRealJid(); + final var cryptoTargets = conversation.getAcceptedCryptoTargets(); + if (cryptoTargets.remove(user.getRealJid())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": removed " + + jid + + " from crypto targets of " + + conversation.getName()); + conversation.setAcceptedCryptoTargets(cryptoTargets); + getDatabase().updateConversation(conversation); + } + } + } + + private void fetchDeviceIdsIfNeeded(final boolean isNew, final MucOptions.User user) { + final var contact = user.getContact(); + final var mucOptions = user.getMucOptions(); + final var axolotlService = connection.getAxolotlService(); + if (isNew + && user.getRealJid() != null + && mucOptions.isPrivateAndNonAnonymous() + && (contact == null || !contact.mutualPresenceSubscription()) + && axolotlService.hasEmptyDeviceList(user.getRealJid())) { + axolotlService.fetchDeviceIds(user.getRealJid()); + } + } + + private void handleStatusCode(final Conversation conversation, final int status) { + if ((status >= 170 && status <= 174) || (status >= 102 && status <= 104)) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": fetching disco#info on status code " + + status); + getManager(MultiUserChatManager.class).fetchDiscoInfo(conversation); + } + } + + public ListenableFuture fetchDiscoInfo(final Conversation conversation) { + final var address = conversation.getJid().asBareJid(); + final var future = + connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null); + return Futures.transform( + future, + infoQuery -> { + setDiscoInfo(conversation, infoQuery); + return null; + }, + MoreExecutors.directExecutor()); + } + + private void setDiscoInfo(final Conversation conversation, final InfoQuery result) { + final var account = conversation.getAccount(); + final var address = conversation.getJid().asBareJid(); + final var avatarHash = + result.getServiceDiscoveryExtension( + Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash"); + if (VCardUpdate.isValidSHA1(avatarHash)) { + connection.getManager(AvatarManager.class).handleVCardUpdate(address, avatarHash); + } + final MucOptions mucOptions = conversation.getMucOptions(); + final Bookmark bookmark = conversation.getBookmark(); + final boolean sameBefore = + StringUtils.equals( + bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); + + final var hadOccupantId = mucOptions.occupantId(); + if (mucOptions.updateConfiguration(result)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": muc configuration changed for " + + conversation.getJid().asBareJid()); + getDatabase().updateConversation(conversation); + } + + final var hasOccupantId = mucOptions.occupantId(); + + if (!hadOccupantId && hasOccupantId && mucOptions.online()) { + final var me = mucOptions.getSelf().getFullJid(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": gained support for occupant-id in " + + me + + ". resending presence"); + this.available(me, mucOptions.nonanonymous()); + } + + if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { + if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { + getManager(BookmarkManager.class).create(bookmark); + } + } + this.service.updateConversationUi(); + } + + public void resendPresence(final Conversation conversation) { + final MucOptions mucOptions = conversation.getMucOptions(); + if (mucOptions.online()) { + available(mucOptions.getSelf().getFullJid(), mucOptions.nonanonymous()); + } + } + + private void available( + final Jid address, final boolean nonAnonymous, final Extension... extensions) { + final var presenceManager = getManager(PresenceManager.class); + final var account = getAccount(); + final String pgpSignature = account.getPgpSignature(); + if (nonAnonymous && pgpSignature != null) { + final String message = account.getPresenceStatusMessage(); + presenceManager.available( + address, message, combine(extensions, new Signed(pgpSignature))); + } else { + presenceManager.available(address, extensions); + } + } + + public void unavailable(final Conversation conversation) { + final var mucOptions = conversation.getMucOptions(); + getManager(PresenceManager.class).unavailable(mucOptions.getSelf().getFullJid()); + } + + private static Extension[] combine(final Extension[] extensions, final Extension extension) { + return new ImmutableList.Builder() + .addAll(Arrays.asList(extensions)) + .add(extension) + .build() + .toArray(new Extension[0]); + } + + public ListenableFuture pushConfiguration( + final Conversation conversation, final Map input) { + final var address = conversation.getJid().asBareJid(); + final var configuration = modifyBestInteroperability(input); + + if (configuration.get("muc#roomconfig_whois") instanceof String whois + && whois.equals("anyone")) { + conversation.setAttribute("accept_non_anonymous", true); + getDatabase().updateConversation(conversation); + } + + final var future = fetchConfigurationForm(address); + return Futures.transformAsync( + future, + current -> { + final var modified = current.submit(configuration); + return submitConfigurationForm(address, modified); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchConfigurationForm(final Jid address) { + final var iq = new Iq(Iq.Type.GET, new MucOwner()); + iq.setTo(address); + Log.d(Config.LOGTAG, "fetching configuration form: " + iq); + return Futures.transform( + connection.sendIqPacket(iq), + response -> { + final var mucOwner = response.getExtension(MucOwner.class); + if (mucOwner == null) { + throw new IllegalStateException("Missing MucOwner element in response"); + } + return mucOwner.getConfiguration(); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture submitConfigurationForm(final Jid address, final Data data) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var mucOwner = iq.addExtension(new MucOwner()); + mucOwner.addExtension(data); + Log.d(Config.LOGTAG, "pushing configuration form: " + iq); + return Futures.transform( + this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchMembers(final Conversation conversation) { + final var futures = + Collections2.transform( + Arrays.asList(Affiliation.OWNER, Affiliation.ADMIN, Affiliation.MEMBER), + a -> fetchAffiliations(conversation, a)); + ListenableFuture> future = FutureMerger.allAsList(futures); + return Futures.transform( + future, + members -> { + setMembers(conversation, members); + return null; + }, + MoreExecutors.directExecutor()); + } + + private void setMembers(final Conversation conversation, final List users) { + for (final var user : users) { + if (user.realJidMatchesAccount()) { + continue; + } + boolean isNew = conversation.getMucOptions().updateUser(user); + fetchDeviceIdsIfNeeded(isNew, user); + } + final var mucOptions = conversation.getMucOptions(); + final var members = mucOptions.getMembers(true); + final var cryptoTargets = conversation.getAcceptedCryptoTargets(); + boolean changed = false; + for (final var iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) { + final var jid = iterator.next(); + if (!members.contains(jid) && !members.contains(jid.getDomain())) { + iterator.remove(); + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": removed " + + jid + + " from crypto targets of " + + conversation.getName()); + changed = true; + } + } + if (changed) { + conversation.setAcceptedCryptoTargets(cryptoTargets); + getDatabase().updateConversation(conversation); + } + // TODO only when room has no avatar + this.service.getAvatarService().clear(mucOptions); + this.service.updateMucRosterUi(); + this.service.updateConversationUi(); + } + + private ListenableFuture> fetchAffiliations( + final Conversation conversation, final Affiliation affiliation) { + final var iq = new Iq(Iq.Type.GET); + iq.setTo(conversation.getJid().asBareJid()); + iq.addExtension(new MucAdmin()).addExtension(new Item()).setAffiliation(affiliation); + return Futures.transform( + this.connection.sendIqPacket(iq), + response -> { + final var mucAdmin = response.getExtension(MucAdmin.class); + if (mucAdmin == null) { + throw new IllegalStateException("No query in response"); + } + return Collections2.transform( + mucAdmin.getItems(), i -> itemToUser(conversation, i, null)); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture changeUsername( + final Conversation conversation, final String username) { + + // TODO when online send normal available presence + // TODO when not online do a normal join + + final Bookmark bookmark = conversation.getBookmark(); + final MucOptions options = conversation.getMucOptions(); + final Jid joinJid = options.createJoinJid(username); + if (joinJid == null) { + return Futures.immediateFailedFuture(new IllegalArgumentException()); + } + + if (options.online()) { + final SettableFuture renameFuture = SettableFuture.create(); + options.setOnRenameListener( + new MucOptions.OnRenameListener() { + + @Override + public void onSuccess() { + renameFuture.set(null); + } + + @Override + public void onFailure() { + renameFuture.setException(new IllegalStateException()); + } + }); + + available(joinJid, options.nonanonymous()); + + if (username.equals(MucOptions.defaultNick(getAccount())) + && bookmark != null + && bookmark.getNick() != null) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": removing nick from bookmark for " + + bookmark.getJid()); + bookmark.setNick(null); + getManager(BookmarkManager.class).create(bookmark); + } + return renameFuture; + } else { + conversation.setContactJid(joinJid); + getDatabase().updateConversation(conversation); + if (bookmark != null) { + bookmark.setNick(username); + getManager(BookmarkManager.class).create(bookmark); + } + join(conversation); + return Futures.immediateVoidFuture(); + } + } + + public void checkMucRequiresRename(final Conversation conversation) { + final var options = conversation.getMucOptions(); + if (!options.online()) { + return; + } + final String current = options.getActualNick(); + final String proposed = options.getProposedNickPure(); + if (current == null || current.equals(proposed)) { + return; + } + final Jid joinJid = options.createJoinJid(proposed); + Log.d( + Config.LOGTAG, + String.format( + "%s: muc rename required %s (was: %s)", + getAccount().getJid().asBareJid(), joinJid, current)); + available(joinJid, options.nonanonymous()); + } + + public void setPassword(final Conversation conversation, final String password) { + final var bookmark = conversation.getBookmark(); + conversation.getMucOptions().setPassword(password); + if (bookmark != null) { + bookmark.setAutojoin(true); + getManager(BookmarkManager.class).create(bookmark); + } + getDatabase().updateConversation(conversation); + this.join(conversation); + } + + public void pingAndRejoin(final Conversation conversation) { + final Account account = getAccount(); + synchronized (this.inProgressConferenceJoins) { + if (this.inProgressConferenceJoins.contains(conversation)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": canceling muc self ping because join is already under way"); + return; + } + } + synchronized (this.inProgressConferencePings) { + if (!this.inProgressConferencePings.add(conversation)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": canceling muc self ping because ping is already under way"); + return; + } + } + final Jid self = conversation.getMucOptions().getSelf().getFullJid(); + final var future = getManager(PingManager.class).ping(self); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Iq result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ping to " + + self + + " came back fine"); + synchronized (MultiUserChatManager.this.inProgressConferencePings) { + MultiUserChatManager.this.inProgressConferencePings.remove( + conversation); + } + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + synchronized (MultiUserChatManager.this.inProgressConferencePings) { + MultiUserChatManager.this.inProgressConferencePings.remove( + conversation); + } + if (throwable instanceof IqErrorException iqErrorException) { + final var condition = iqErrorException.getErrorCondition(); + if (condition instanceof Condition.ServiceUnavailable + || condition instanceof Condition.FeatureNotImplemented + || condition instanceof Condition.ItemNotFound) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ping to " + + self + + " came back as ignorable error"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ping to " + + self + + " failed. attempting rejoin"); + join(conversation); + } + } + } + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture destroy(final Jid address) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var mucOwner = iq.addExtension(new MucOwner()); + mucOwner.addExtension(new Destroy()); + return Futures.transform( + connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); + } + + public ListenableFuture setAffiliation( + final Conversation conversation, final Affiliation affiliation, Jid user) { + return setAffiliation(conversation, affiliation, Collections.singleton(user)); + } + + public ListenableFuture setAffiliation( + final Conversation conversation, + final Affiliation affiliation, + final Collection users) { + final var address = conversation.getJid().asBareJid(); + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var admin = iq.addExtension(new MucAdmin()); + for (final var user : users) { + final var item = admin.addExtension(new Item()); + item.setJid(user); + item.setAffiliation(affiliation); + } + return Futures.transform( + this.connection.sendIqPacket(iq), + response -> { + // TODO figure out what this was meant to do + // is this a work around for some servers not sending notifications when + // changing the affiliation of people not in the room? this would explain this + // firing only when getRole == None + final var mucOptions = conversation.getMucOptions(); + for (final var user : users) { + mucOptions.changeAffiliation(user, affiliation); + } + service.getAvatarService().clear(mucOptions); + return null; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture setRole(final Jid address, final Role role, final String user) { + return setRole(address, role, Collections.singleton(user)); + } + + public ListenableFuture setRole( + final Jid address, final Role role, final Collection users) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var admin = iq.addExtension(new MucAdmin()); + for (final var user : users) { + final var item = admin.addExtension(new Item()); + item.setNick(user); + item.setRole(role); + } + return Futures.transform( + this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor()); + } + + public void setSubject(final Conversation conversation, final String subject) { + final var message = new Message(); + message.setType(Message.Type.GROUPCHAT); + message.setTo(conversation.getJid().asBareJid()); + message.addExtension(new Subject(subject)); + connection.sendMessagePacket(message); + } + + public void invite(final Conversation conversation, final Jid address) { + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": inviting " + + address + + " to " + + conversation.getJid().asBareJid()); + final MucOptions.User user = + conversation.getMucOptions().findUserByRealJid(address.asBareJid()); + if (user == null || user.getAffiliation() == Affiliation.OUTCAST) { + this.setAffiliation(conversation, Affiliation.NONE, address); + } + + final var packet = new Message(); + packet.setTo(conversation.getJid().asBareJid()); + final var x = packet.addExtension(new MucUser()); + final var invite = x.addExtension(new Invite()); + invite.setTo(address.asBareJid()); + connection.sendMessagePacket(packet); + } + + public void directInvite(final Conversation conversation, final Jid address) { + final var message = new Message(); + message.setTo(address); + final var directInvite = message.addExtension(new DirectInvite()); + directInvite.setJid(conversation.getJid().asBareJid()); + final var password = conversation.getMucOptions().getPassword(); + if (password != null) { + directInvite.setPassword(password); + } + if (address.isFullJid()) { + message.addExtension(new NoStore()); + message.addExtension(new NoCopy()); + } + this.connection.sendMessagePacket(message); + } + + public boolean isJoinInProgress(final Conversation conversation) { + synchronized (this.inProgressConferenceJoins) { + if (conversation.getMode() == Conversational.MODE_MULTI) { + final boolean inProgress = this.inProgressConferenceJoins.contains(conversation); + if (inProgress) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": holding back message to group. join in progress"); + } + return inProgress; + } else { + return false; + } + } + } + + public void clearInProgress() { + synchronized (this.inProgressConferenceJoins) { + this.inProgressConferenceJoins.clear(); + } + synchronized (this.inProgressConferencePings) { + this.inProgressConferencePings.clear(); + } + } + + public Jid getService() { + return Iterables.getFirst(this.getServices(), null); + } + + public List getServices() { + final var builder = new ImmutableList.Builder(); + for (final var entry : getManager(DiscoManager.class).getServerItems().entrySet()) { + final var value = entry.getValue(); + if (value.getFeatureStrings().contains(Namespace.MUC) + && value.hasIdentityWithCategoryAndType("conference", "text") + && !value.getFeatureStrings().contains("jabber:iq:gateway") + && !value.hasIdentityWithCategoryAndType("conference", "irc")) { + builder.add(entry.getKey()); + } + } + return builder.build(); + } + + public static MucOptions.User itemToUser( + final Conversation conference, + im.conversations.android.xmpp.model.muc.Item item, + final Jid from) { + final var affiliation = item.getAffiliation(); + final var role = item.getRole(); + final var nick = item.getNick(); + final Jid fullAddress; + if (from != null && from.isFullJid()) { + fullAddress = from; + } else if (Strings.isNullOrEmpty(nick)) { + fullAddress = null; + } else { + fullAddress = ofNick(conference, nick); + } + final Jid realJid = item.getAttributeAsJid("jid"); + MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullAddress); + if (Jid.Invalid.isValid(realJid)) { + user.setRealJid(realJid); + } + user.setAffiliation(affiliation); + user.setRole(role); + return user; + } + + private static Jid ofNick(final Conversation conversation, final String nick) { + try { + return conversation.getJid().withResource(nick); + } catch (final IllegalArgumentException e) { + return null; + } + } + + private static Map modifyBestInteroperability( + final Map unmodified) { + final var builder = new ImmutableMap.Builder(); + builder.putAll(unmodified); + + if (unmodified.get("muc#roomconfig_moderatedroom") instanceof Boolean moderated) { + builder.put("members_by_default", !moderated); + } + if (unmodified.get("muc#roomconfig_allowpm") instanceof String allowPm) { + // ejabberd :-/ + final boolean allow = "anyone".equals(allowPm); + builder.put("allow_private_messages", allow); + builder.put("allow_private_messages_from_visitors", allow ? "anyone" : "nobody"); + } + + if (unmodified.get("muc#roomconfig_allowinvites") instanceof Boolean allowInvites) { + // TODO check that this actually does something useful? + builder.put( + "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", allowInvites); + } + + return builder.buildOrThrow(); + } + + private static Map configWithName( + final Map unmodified, final String name) { + if (Strings.isNullOrEmpty(name)) { + return unmodified; + } + return new ImmutableMap.Builder() + .putAll(unmodified) + .put("muc#roomconfig_roomname", name) + .buildKeepingLast(); + } + + public static Map defaultGroupChatConfiguration() { + return new ImmutableMap.Builder() + .put("muc#roomconfig_persistentroom", true) + .put("muc#roomconfig_membersonly", true) + .put("muc#roomconfig_publicroom", false) + .put("muc#roomconfig_whois", "anyone") + .put("muc#roomconfig_changesubject", false) + .put("muc#roomconfig_allowinvites", false) + .put("muc#roomconfig_enablearchiving", true) // prosody + .put("mam", true) // ejabberd community + .put("muc#roomconfig_mam", true) // ejabberd saas + .buildOrThrow(); + } + + public static Map defaultChannelConfiguration() { + return new ImmutableMap.Builder() + .put("muc#roomconfig_persistentroom", true) + .put("muc#roomconfig_membersonly", false) + .put("muc#roomconfig_publicroom", true) + .put("muc#roomconfig_whois", "moderators") + .put("muc#roomconfig_changesubject", false) + .put("muc#roomconfig_enablearchiving", true) // prosody + .put("mam", true) // ejabberd community + .put("muc#roomconfig_mam", true) // ejabberd saas + .buildOrThrow(); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/NativeBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/NativeBookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b4189dac1e39d586dd4e23f2ab55e27ecf1460 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/NativeBookmarkManager.java @@ -0,0 +1,172 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.bookmark2.Conference; +import im.conversations.android.xmpp.model.bookmark2.Nick; +import im.conversations.android.xmpp.model.bookmark2.Password; +import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.pubsub.event.Retract; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +public class NativeBookmarkManager extends AbstractBookmarkManager { + + public NativeBookmarkManager(final XmppConnectionService service, XmppConnection connection) { + super(service, connection); + } + + public void fetch() { + final var future = getManager(PepManager.class).fetchItems(Conference.class); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final Map bookmarks) { + final var builder = new ImmutableMap.Builder(); + for (final var entry : bookmarks.entrySet()) { + final Bookmark bookmark = + itemToBookmark(entry.getKey(), entry.getValue(), getAccount()); + if (bookmark == null) { + continue; + } + builder.put(bookmark.getJid(), bookmark); + } + processBookmarksInitial(builder.buildKeepingLast(), true); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable); + } + }, + MoreExecutors.directExecutor()); + } + + public void handleItems(final Items items) { + this.handleItems(items.getItemMap(Conference.class)); + this.handleRetractions(items.getRetractions()); + } + + private void handleRetractions(final Collection retractions) { + final var account = getAccount(); + for (final var retract : retractions) { + final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id")); + if (id != null) { + account.removeBookmark(id); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); + processDeletedBookmark(id); + service.updateConversationUi(); + } + } + } + + private void handleItems(final Map items) { + final var account = getAccount(); + for (final var item : items.entrySet()) { + final Bookmark bookmark = itemToBookmark(item.getKey(), item.getValue(), account); + if (bookmark == null) { + continue; + } + account.putBookmark(bookmark); + getManager(BookmarkManager.class).processModifiedBookmark(bookmark, true); + service.updateConversationUi(); + } + } + + public ListenableFuture publish(final Bookmark bookmark) { + final var address = bookmark.getJid(); + final var name = bookmark.getBookmarkName(); + final var nick = bookmark.getNick(); + final String password = bookmark.getPassword(); + final var itemId = address.toString(); + final var conference = new Conference(); + conference.setAutoJoin(bookmark.autojoin()); + if (nick != null) { + conference.addExtension(new Nick()).setContent(nick); + } + if (name != null) { + conference.setConferenceName(name); + } + if (password != null) { + conference.addExtension(new Password()).setContent(password); + } + conference.addExtension(bookmark.getExtensions()); + return Futures.transform( + getManager(PepManager.class) + .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS), + result -> null, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retract(final Jid address) { + final var itemId = address.toString(); + return Futures.transform( + getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2), + result -> null, + MoreExecutors.directExecutor()); + } + + private void deleteAllItems() { + final var account = getAccount(); + final var previous = account.getBookmarkedJids(); + account.setBookmarks(Collections.emptyMap()); + processDeletedBookmarks(previous); + } + + public void handleDelete() { + Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": deleted bookmarks node"); + this.deleteAllItems(); + } + + public void handlePurge() { + Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": purged bookmarks"); + this.deleteAllItems(); + } + + public boolean hasFeature() { + final var pep = getManager(PepManager.class); + final var disco = getManager(DiscoManager.class); + return pep.hasPublishOptions() + && pep.hasConfigNodeMax() + && disco.hasAccountFeature(Namespace.BOOKMARKS2_COMPAT); + } + + private static Bookmark itemToBookmark( + final String id, final Conference conference, final Account account) { + if (id == null || conference == null) { + return null; + } + final var jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id)); + if (jid == null || jid.isFullJid()) { + return null; + } + final Bookmark bookmark = new Bookmark(account, jid); + + // TODO use proper API + + bookmark.setBookmarkName(conference.getAttribute("name")); + bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); + bookmark.setNick(conference.findChildContent("nick")); + bookmark.setPassword(conference.findChildContent("password")); + final var extensions = conference.getExtensions(); + if (extensions != null) { + bookmark.setExtensions(conference.getExtensions()); + } + return bookmark; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java index d16ccc41c57ec4bd559ec278749ba5976868e01e..b9080799d0883efb619c90e40359262609e298a0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java @@ -4,7 +4,9 @@ import android.content.Context; import androidx.annotation.NonNull; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.model.ping.Ping; import im.conversations.android.xmpp.model.stanza.Iq; @@ -24,6 +26,12 @@ public class PingManager extends AbstractManager { } } + public ListenableFuture ping(final Jid address) { + final var iq = new Iq(Iq.Type.GET, new Ping()); + iq.setTo(address); + return this.connection.sendIqPacket(iq); + } + public void ping(final Runnable runnable) { final var pingFuture = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping())); Futures.addCallback( diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java index 9059e1fa220e089acb79e90cf64af8a00ccd06cf..ac201f6d44cf6aacbc7145fc8a0605a41893d496 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java @@ -105,9 +105,7 @@ public class PresenceManager extends AbstractManager { presence.setAvailability(availability); presence.setStatus(message); if (pgpSignature != null) { - final var signed = new Signed(); - signed.setContent(pgpSignature); - presence.addExtension(signed); + presence.addExtension(new Signed(pgpSignature)); } final var lastActivity = service.getLastActivity(); @@ -127,8 +125,13 @@ public class PresenceManager extends AbstractManager { } public void available(final Jid to, final Extension... extensions) { + available(to, null, extensions); + } + + public void available(final Jid to, final String message, final Extension... extensions) { final var presence = new Presence(); presence.setTo(to); + presence.setStatus(message); for (final var extension : extensions) { presence.addExtension(extension); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java index f2c4e14c396b9dde4df320a93434eea837bf70d5..c042cdc6c5c994a7d2bfc19b9d1f4144cfc360be 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -176,7 +176,7 @@ public class PubSubManager extends AbstractManager { final var isFromBare = from == null || from.isBareJid(); final var node = items.getNode(); if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { - getManager(BookmarkManager.class).handleItems(items); + getManager(NativeBookmarkManager.class).handleItems(items); return; } if (connection.fromAccount(message) && Namespace.BOOKMARKS.equals(node)) { @@ -205,7 +205,7 @@ public class PubSubManager extends AbstractManager { final var isFromBare = from == null || from.isBareJid(); final var node = purge.getNode(); if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { - getManager(BookmarkManager.class).handlePurge(); + getManager(NativeBookmarkManager.class).handlePurge(); } if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { // purge (delete all items in a node) is functionally equivalent to delete @@ -218,7 +218,7 @@ public class PubSubManager extends AbstractManager { final var isFromBare = from == null || from.isBareJid(); final var node = delete.getNode(); if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { - getManager(BookmarkManager.class).handleDelete(); + getManager(NativeBookmarkManager.class).handleDelete(); return; } if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java index 05ab21a94fad9d9cb3d192c11e826d7d120426cd..6fbbbffe2393083220d7637bb3c4065b85ce8717 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -38,6 +38,9 @@ public class VCardManager extends AbstractManager { } public ListenableFuture retrievePhoto(final Jid address) { + + // TODO add a caching variant + final var vCardFuture = retrieve(address); return Futures.transform( vCardFuture, diff --git a/src/main/java/im/conversations/android/xmpp/model/conference/DirectInvite.java b/src/main/java/im/conversations/android/xmpp/model/conference/DirectInvite.java new file mode 100644 index 0000000000000000000000000000000000000000..9a7cbc6f7023e1cd35e61c8f50e8ffc6144cfd18 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/conference/DirectInvite.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.conference; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class DirectInvite extends Extension { + + public DirectInvite() { + super(DirectInvite.class); + } + + public void setJid(final Jid jid) { + this.setAttribute("jid", jid); + } + + public void setPassword(final String password) { + this.setAttribute("password", password); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/conference/package-info.java b/src/main/java/im/conversations/android/xmpp/model/conference/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..da4a2cccfcf6716f3ca51cc4beae211b141d97de --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/conference/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DIRECT_MUC_INVITATIONS) +package im.conversations.android.xmpp.model.conference; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index 806c79af0386748065c9be17b8502a4b804d2b0e..e6a416ee1148ee3493a41e953085a4931bb410d0 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -1,8 +1,10 @@ package im.conversations.android.xmpp.model.data; +import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; +import eu.siacs.conversations.Config; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import java.util.Collection; @@ -52,11 +54,14 @@ public class Data extends Extension { if (type != null) { field.setType(type); } - if (value instanceof Collection) { - for (final Object subValue : (Collection) value) { - if (subValue instanceof String) { + if (value instanceof Collection collection) { + Log.d(Config.LOGTAG, "submitting collection: " + collection); + for (final Object subValue : collection) { + if (subValue == null) { + Log.d(Config.LOGTAG, "null value in the values for " + name); + } else if (subValue instanceof String s) { final var valueExtension = field.addExtension(new Value()); - valueExtension.setContent((String) subValue); + valueExtension.setContent(s); } else { throw new IllegalArgumentException( String.format( @@ -66,15 +71,15 @@ public class Data extends Extension { } } else { final var valueExtension = field.addExtension(new Value()); - if (value instanceof String) { - valueExtension.setContent((String) value); + if (value instanceof String s) { + valueExtension.setContent(s); } else if (value instanceof Enum e) { valueExtension.setContent( CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString())); - } else if (value instanceof Integer) { - valueExtension.setContent(String.valueOf(value)); - } else if (value instanceof Boolean) { - valueExtension.setContent(Boolean.TRUE.equals(value) ? "1" : "0"); + } else if (value instanceof Integer i) { + valueExtension.setContent(String.valueOf(i)); + } else if (value instanceof Boolean b) { + valueExtension.setContent(Boolean.TRUE.equals(b) ? "1" : "0"); } else { throw new IllegalArgumentException( String.format( @@ -109,9 +114,11 @@ public class Data extends Extension { final var fieldName = existingField.getFieldName(); final Object submittedValue = values.get(fieldName); if (submittedValue != null) { + Log.d(Config.LOGTAG, "submitting value " + fieldName + ": " + submittedValue); submit.addField(fieldName, submittedValue); } else { - submit.addField(fieldName, existingField.getValues()); + Log.d(Config.LOGTAG, "staying with default for: " + fieldName); + submit.addExtension(existingField); } } return submit; diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index 3244ff9c3836d076b801e1d1a3a761346f03021f..3c301809ef71370eacdc7ebf3c4988017981f213 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -19,6 +19,7 @@ public class Field extends Extension { } public Collection getValues() { + // TODO filter null return Collections2.transform(getExtensions(Value.class), Element::getContent); } diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/NoCopy.java b/src/main/java/im/conversations/android/xmpp/model/hints/NoCopy.java new file mode 100644 index 0000000000000000000000000000000000000000..9db78ffa59c1d5103ec450c90aea9067a9b5167a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/NoCopy.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.hints; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class NoCopy extends Extension { + public NoCopy() { + super(NoCopy.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/NoStore.java b/src/main/java/im/conversations/android/xmpp/model/hints/NoStore.java new file mode 100644 index 0000000000000000000000000000000000000000..d0eb598deab9452a7975d6d5e3b415906fc40294 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/NoStore.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.hints; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class NoStore extends Extension { + + public NoStore() { + super(NoStore.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java index 4ae3b8ed519a234698cc3ec30f12d4f1a25d44f4..cc8c8a74e785af5d17dae8c2eda1fe6c1cb28959 100644 --- a/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java @@ -9,4 +9,9 @@ public class Subject extends Extension { public Subject() { super(Subject.class); } + + public Subject(final String subject) { + this(); + this.setContent(subject); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/History.java b/src/main/java/im/conversations/android/xmpp/model/muc/History.java index e09210e60277c22491ae60940543a77d4e3ab523..4112bd4cecb6857db893196f9f998608c5cca173 100644 --- a/src/main/java/im/conversations/android/xmpp/model/muc/History.java +++ b/src/main/java/im/conversations/android/xmpp/model/muc/History.java @@ -1,5 +1,6 @@ package im.conversations.android.xmpp.model.muc; +import eu.siacs.conversations.generator.AbstractGenerator; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; @@ -17,4 +18,8 @@ public class History extends Extension { public void setMaxStanzas(final int maxStanzas) { this.setAttribute("maxstanzas", maxStanzas); } + + public void setSince(long timestamp) { + this.setAttribute("since", AbstractGenerator.getTimestamp(timestamp)); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Item.java b/src/main/java/im/conversations/android/xmpp/model/muc/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..222e8396dd269052cc5fc0032d64e658698dda2c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Item.java @@ -0,0 +1,54 @@ +package im.conversations.android.xmpp.model.muc; + +import android.util.Log; +import com.google.common.base.Strings; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.Extension; +import java.util.Locale; + +public abstract class Item extends Extension { + + public Item(final Class clazz) { + super(clazz); + } + + public Affiliation getAffiliation() { + return affiliationOrNone(this.getAttribute("affiliation")); + } + + public static Affiliation affiliationOrNone(final String affiliation) { + if (Strings.isNullOrEmpty(affiliation)) { + return Affiliation.NONE; + } + try { + return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return Affiliation.NONE; + } + } + + public Role getRole() { + return roleOrNone(this.getAttribute("role")); + } + + public static Role roleOrNone(final String role) { + if (Strings.isNullOrEmpty(role)) { + return Role.NONE; + } + try { + return Role.valueOf(role.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, "could not parse role " + role); + return Role.NONE; + } + } + + public String getNick() { + return this.getAttribute("nick"); + } + + public Jid getJid() { + return this.getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Password.java b/src/main/java/im/conversations/android/xmpp/model/muc/Password.java new file mode 100644 index 0000000000000000000000000000000000000000..2db493294d9fd2f3258236373048f7e51bcf9bcd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Password.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Password extends Extension { + + public Password() { + super(Password.class); + } + + public Password(final String password) { + this(); + this.setContent(password); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/admin/Item.java b/src/main/java/im/conversations/android/xmpp/model/muc/admin/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..032013095eec502ae82b2605a1c71173d51f0616 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/admin/Item.java @@ -0,0 +1,30 @@ +package im.conversations.android.xmpp.model.muc.admin; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; + +@XmlElement +public class Item extends im.conversations.android.xmpp.model.muc.Item { + + public Item() { + super(Item.class); + } + + public void setAffiliation(final Affiliation affiliation) { + this.setAttribute("affiliation", affiliation); + } + + public void setRole(final Role role) { + this.setAttribute("role", role); + } + + public void setJid(final Jid jid) { + this.setAttribute("jid", jid); + } + + public void setNick(String user) { + this.setAttribute("nick", user); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/admin/MucAdmin.java b/src/main/java/im/conversations/android/xmpp/model/muc/admin/MucAdmin.java new file mode 100644 index 0000000000000000000000000000000000000000..c3beb91660a6fe19e47e3c6e647fd968e26f6235 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/admin/MucAdmin.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.muc.admin; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; + +@XmlElement(name = "query") +public class MucAdmin extends Extension { + + public MucAdmin() { + super(MucAdmin.class); + } + + public Collection getItems() { + return this.getExtensions(Item.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/admin/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/admin/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..9e56b983ee1814cf6a3e72220f29045ec0992bf5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/admin/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC_ADMIN) +package im.conversations.android.xmpp.model.muc.admin; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/owner/Destroy.java b/src/main/java/im/conversations/android/xmpp/model/muc/owner/Destroy.java new file mode 100644 index 0000000000000000000000000000000000000000..c2eec3e44086d3aa4164d2bfeb3c69e94b6c0f6f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/owner/Destroy.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.muc.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Destroy extends Extension { + + public Destroy() { + super(Destroy.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/owner/MucOwner.java b/src/main/java/im/conversations/android/xmpp/model/muc/owner/MucOwner.java new file mode 100644 index 0000000000000000000000000000000000000000..44fdec687741ddb996c541bfb9a2aadb715ebef6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/owner/MucOwner.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.muc.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement(name = "query") +public class MucOwner extends Extension { + public MucOwner() { + super(MucOwner.class); + } + + public Data getConfiguration() { + return this.getExtension(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/owner/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/owner/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..6d7fd3bf0ba5fc3035119fbdbbdba9e93eb78361 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/owner/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC_OWNER) +package im.conversations.android.xmpp.model.muc.owner; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Invite.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Invite.java new file mode 100644 index 0000000000000000000000000000000000000000..8c7b9a97808b734c892090c0385d40bbfc9bdb5d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Invite.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.muc.user; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Invite extends Extension { + + public Invite() { + super(Invite.class); + } + + public void setTo(final Jid to) { + this.setAttribute("to", to); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java index 7ff712aeaba976c687928c61c8255b4fb19967b1..b58a0e703f15d08089d150ed908195bdcb416d86 100644 --- a/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java @@ -1,58 +1,11 @@ package im.conversations.android.xmpp.model.muc.user; -import android.util.Log; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.Jid; - import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; -import im.conversations.android.xmpp.model.muc.Affiliation; -import im.conversations.android.xmpp.model.muc.Role; - -import java.util.Locale; @XmlElement -public class Item extends Extension { - +public class Item extends im.conversations.android.xmpp.model.muc.Item { public Item() { super(Item.class); } - - public Affiliation getAffiliation() { - final var affiliation = this.getAttribute("affiliation"); - if (Strings.isNullOrEmpty(affiliation)) { - return Affiliation.NONE; - } - try { - return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT)); - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation); - return Affiliation.NONE; - } - } - - public Role getRole() { - final var role = this.getAttribute("role"); - if (Strings.isNullOrEmpty(role)) { - return Role.NONE; - } - try { - return Role.valueOf(role.toUpperCase(Locale.ROOT)); - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG,"could not parse role "+ role); - return Role.NONE; - } - } - - public String getNick() { - return this.getAttribute("nick"); - } - - public Jid getJid() { - return this.getAttributeAsJid("jid"); - } } diff --git a/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java index c75413972f521366f46f542d222f0d851cd7219c..797c59bcc3520c006f6da41d23158480302d3af6 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java +++ b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java @@ -1,15 +1,18 @@ package im.conversations.android.xmpp.model.pgp; import eu.siacs.conversations.xml.Namespace; - import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; -@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED) +@XmlElement(name = "x", namespace = Namespace.PGP_SIGNED) public class Signed extends Extension { - public Signed() { super(Signed.class); } + + public Signed(final String signature) { + this(); + this.setContent(signature); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java index 545808e69e51c554265b4c2e1e34c03fcd97e4da..8f8d3935a74e2f8d65ccd256bf9e56c69dc4f23a 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java @@ -1,7 +1,6 @@ package im.conversations.android.xmpp.model.stanza; import com.google.common.base.Strings; -import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.error.Error; @@ -50,22 +49,6 @@ public class Iq extends Stanza { return super.isInvalid(); } - // Legacy methods that need to be refactored: - - public Element query() { - final Element query = findChild("query"); - if (query != null) { - return query; - } - return addChild("query"); - } - - public Element query(final String xmlns) { - final Element query = query(); - query.setAttribute("xmlns", xmlns); - return query(); - } - public Iq generateResponse(final Iq.Type type) { final var packet = new Iq(type); packet.setTo(this.getFrom()); diff --git a/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java index 8c3cfb6ba36732bba95ffbd049448b89dda90e45..73bef88309a5ddca0d223a8d6f105908518dd277 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java @@ -5,12 +5,10 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.http.ServiceOutageStatus; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; -import java.util.ArrayList; -import java.util.List; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -48,7 +46,7 @@ public class AccountStateProcessor extends XmppConnection.Delegate this.service.databaseBackend.updateAccount(account); } this.service.getMessageArchiveService().executePendingQueries(account); - if (connection != null && connection.getFeatures().csi()) { + if (this.connection.getFeatures().csi()) { if (this.service.checkListeners()) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive"); connection.sendInactive(); @@ -57,36 +55,14 @@ public class AccountStateProcessor extends XmppConnection.Delegate connection.sendActive(); } } - List conversations = this.service.getConversations(); - for (Conversation conversation : conversations) { - final boolean inProgressJoin; - synchronized (account.inProgressConferenceJoins) { - inProgressJoin = account.inProgressConferenceJoins.contains(conversation); - } - final boolean pendingJoin; - synchronized (account.pendingConferenceJoins) { - pendingJoin = account.pendingConferenceJoins.contains(conversation); - } - if (conversation.getAccount() == account && !pendingJoin && !inProgressJoin) { + final var mucManager = getManager(MultiUserChatManager.class); + final var conversations = this.service.getConversations(); + for (final var conversation : conversations) { + final boolean inProgressJoin = mucManager.isJoinInProgress(conversation); + if (conversation.getAccount() == account && !inProgressJoin) { this.service.sendUnsentMessages(conversation); } } - final List pendingLeaves; - synchronized (account.pendingConferenceLeaves) { - pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); - account.pendingConferenceLeaves.clear(); - } - for (Conversation conversation : pendingLeaves) { - this.service.leaveMuc(conversation); - } - final List pendingJoins; - synchronized (account.pendingConferenceJoins) { - pendingJoins = new ArrayList<>(account.pendingConferenceJoins); - account.pendingConferenceJoins.clear(); - } - for (Conversation conversation : pendingJoins) { - this.service.joinMuc(conversation); - } this.service.scheduleWakeUpCall( Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 7226dc7c1d87c81ab32ea8fdf438ceef4b21333c..04554b4e01e9f1d45a69a4000e0ea0debfea1282 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -12,12 +12,11 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.manager.BookmarkManager; import eu.siacs.conversations.xmpp.manager.HttpUploadManager; -import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager; +import eu.siacs.conversations.xmpp.manager.MultiUserChatManager; import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; -import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; public class BindProcessor extends XmppConnection.Delegate implements Runnable { @@ -63,26 +62,12 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { } getManager(RosterManager.class).clearPresences(); - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.clear(); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.clear(); - } + getManager(MultiUserChatManager.class).clearInProgress(); service.getJingleConnectionManager().notifyRebound(account); service.getQuickConversationsService().considerSyncBackground(false); getManager(RosterManager.class).request(); - - if (getManager(BookmarkManager.class).hasFeature()) { - getManager(BookmarkManager.class).fetch(); - } else if (getManager(LegacyBookmarkManager.class).hasConversion()) { - Log.d( - Config.LOGTAG, - account.getJid() + ": not fetching bookmarks. waiting for server to push"); - } else { - getManager(PrivateStorageManager.class).fetchBookmarks(); - } + getManager(BookmarkManager.class).request(); if (features.mds()) { getManager(MessageDisplayedSynchronizationManager.class).fetch(); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 75c4c1a8c8a787e43ed0172551ba73ca91e0fe89..5e0367104e45d0302a253de9ab29ee76742889a5 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -93,7 +93,7 @@ Send message to %s Send encrypted message Send v\\OMEMO encrypted message - New nickname in use + Your nickname has been changed Send clear text Decryption failed. Maybe you don’t have the proper private key. OpenKeychain