From 8f3531d24df50054f3ed25e5c6c0d958f6139691 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 May 2025 19:59:30 +0200 Subject: [PATCH] refactor channel discovery to use new APIs --- .../eu/siacs/conversations/entities/Room.java | 52 +++-- .../conversations/generator/IqGenerator.java | 14 -- .../siacs/conversations/parser/IqParser.java | 35 ---- .../services/ChannelDiscoveryService.java | 178 ++++++++++++------ .../services/XmppConnectionService.java | 3 +- .../android/xmpp/model/data/Data.java | 5 + 6 files changed, 163 insertions(+), 124 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Room.java b/src/main/java/eu/siacs/conversations/entities/Room.java index c702c3189bfde4306b067b8448b718d6ab173731..88b2e7485a5faf0bcf6d47f4bba988c0e1565529 100644 --- a/src/main/java/eu/siacs/conversations/entities/Room.java +++ b/src/main/java/eu/siacs/conversations/entities/Room.java @@ -3,31 +3,42 @@ package eu.siacs.conversations.entities; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.utils.LanguageUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; public class Room implements AvatarService.Avatarable, Comparable { - public String address; - public String name; - public String description; - public String language; - public int nusers; + public final String address; + public final String name; + public final String description; + public final String language; + public final int numberOfUsers; - public Room(String address, String name, String description, String language, int nusers) { + public Room( + final String address, + final String name, + final String description, + final String language, + final Integer numberOfUsers) { this.address = address; this.name = name; this.description = description; this.language = language; - this.nusers = nusers; + this.numberOfUsers = numberOfUsers == null ? 0 : numberOfUsers; } - public Room() {} - public String getName() { - return name; + if (Strings.isNullOrEmpty(name)) { + final var jid = Jid.ofOrInvalid(address); + return jid.getLocal(); + } else { + return name; + } } public String getDescription() { @@ -81,9 +92,28 @@ public class Room implements AvatarService.Avatarable, Comparable { @Override public int compareTo(Room o) { return ComparisonChain.start() - .compare(o.nusers, nusers) + .compare(o.numberOfUsers, numberOfUsers) .compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name)) .compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address)) .result(); } + + public static Room of(final Jid address, InfoQuery query) { + final var identity = Iterables.getFirst(query.getIdentities(), null); + final var ri = + query.getServiceDiscoveryExtension("http://jabber.org/protocol/muc#roominfo"); + final String name = identity == null ? null : identity.getIdentityName(); + String roomName = ri == null ? null : ri.getValue("muc#roomconfig_roomname"); + String description = ri == null ? null : ri.getValue("muc#roominfo_description"); + String language = ri == null ? null : ri.getValue("muc#roominfo_lang"); + String occupants = ri == null ? null : ri.getValue("muc#roominfo_occupants"); + final Integer numberOfUsers = Ints.tryParse(Strings.nullToEmpty(occupants)); + + return new Room( + address.toString(), + Strings.isNullOrEmpty(roomName) ? name : roomName, + description, + language, + numberOfUsers); + } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index e2b2638efbab5563079a606d68afaf4cf05fd7e2..5d219b52fb5a46b64b165b9ec3465c95147313f4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -577,18 +577,4 @@ public class IqGenerator extends AbstractGenerator { } return packet; } - - public Iq queryDiscoItems(final Jid jid) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_ITEMS); - return packet; - } - - public Iq queryDiscoInfo(final Jid jid) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_INFO); - return packet; - } } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 37809e5c4402145ee42ecd54bf0ab329f4b8aefb..f711c3100254632d541bde58da5205ef87d77569 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.parser; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; @@ -10,13 +9,11 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; @@ -61,38 +58,6 @@ public class IqParser extends AbstractParser implements Consumer { return items; } - public static Room parseRoom(Iq packet) { - final Element query = packet.findChild("query", Namespace.DISCO_INFO); - if (query == null) { - return null; - } - final Element x = query.findChild("x"); - if (x == null) { - return null; - } - final Element identity = query.findChild("identity"); - Data data = Data.parse(x); - String address = packet.getFrom().toString(); - String name = identity == null ? null : identity.getAttribute("name"); - String roomName = data.getValue("muc#roomconfig_roomname"); - String description = data.getValue("muc#roominfo_description"); - String language = data.getValue("muc#roominfo_lang"); - String occupants = data.getValue("muc#roominfo_occupants"); - int nusers; - try { - nusers = occupants == null ? 0 : Integer.parseInt(occupants); - } catch (NumberFormatException e) { - nusers = 0; - } - - return new Room( - address, - TextUtils.isEmpty(roomName) ? name : roomName, - description, - language, - nusers); - } - private void rosterItems(final Account account, final Element query) { final String version = query.getAttribute("ver"); if (version != null) { diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 9e09e56e962c40cf9c34f069f61bc6902ce16e1a..8d53b61f1a7905e98ce787dd5a943e328a0c013d 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -5,24 +5,33 @@ import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Ordering; +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.Room; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; -import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +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; import im.conversations.android.xmpp.model.stanza.Iq; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Call; @@ -164,7 +173,7 @@ public class ChannelDiscoveryService { private void discoverChannelsLocalServers( final String query, final OnChannelSearchResultsFound listener) { - final Map localMucService = getLocalMucServices(); + final var localMucService = getLocalMucServices(); Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); if (localMucService.isEmpty()) { listener.onChannelSearchResultsFound(Collections.emptyList()); @@ -178,57 +187,104 @@ public class ChannelDiscoveryService { listener.onChannelSearchResultsFound(results); } } - final AtomicInteger queriesInFlight = new AtomicInteger(); - final List rooms = new ArrayList<>(); - for (final Map.Entry entry : localMucService.entrySet()) { - Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); - queriesInFlight.incrementAndGet(); - final var account = entry.getValue(); - service.sendIqPacket( - account, - itemsRequest, - (itemsResponse) -> { - if (itemsResponse.getType() == Iq.Type.RESULT) { - final List items = IqParser.items(itemsResponse); - for (final Jid item : items) { - final Iq infoRequest = - service.getIqGenerator().queryDiscoInfo(item); - queriesInFlight.incrementAndGet(); - service.sendIqPacket( - account, - infoRequest, - infoResponse -> { - if (infoResponse.getType() == Iq.Type.RESULT) { - final Room room = IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - } else { - queriesInFlight.decrementAndGet(); - } - }); + final var roomsRoomsFuture = + Futures.successfulAsList( + Collections2.transform( + localMucService.entrySet(), + e -> discoverRooms(e.getValue(), e.getKey()))); + final var roomsFuture = + Futures.transform( + roomsRoomsFuture, + rooms -> { + final var builder = new ImmutableList.Builder(); + for (final var inner : rooms) { + if (inner == null) { + continue; + } + builder.addAll(inner); } - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - }); - } + return builder.build(); + }, + MoreExecutors.directExecutor()); + Futures.addCallback( + roomsFuture, + new FutureCallback<>() { + @Override + public void onSuccess(ImmutableList rooms) { + finishDiscoSearch(rooms, query, listener); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "could not perform room search", throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture> discoverRooms( + final XmppConnection connection, final Jid server) { + final var request = new Iq(Iq.Type.GET); + request.addExtension(new ItemsQuery()); + request.setTo(server); + final ListenableFuture> itemsFuture = + Futures.transform( + connection.sendIqPacket(request), + iq -> { + final var itemsQuery = iq.getExtension(ItemsQuery.class); + if (itemsQuery == null) { + return Collections.emptyList(); + } + final var items = itemsQuery.getExtensions(Item.class); + return Collections2.filter(items, i -> Objects.nonNull(i.getJid())); + }, + MoreExecutors.directExecutor()); + final var roomsFutures = + Futures.transformAsync( + itemsFuture, + items -> { + final var infoFutures = + Collections2.transform( + items, i -> discoverRoom(connection, i.getJid())); + return Futures.successfulAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + return Futures.transform( + roomsFutures, + rooms -> Collections2.filter(rooms, Objects::nonNull), + MoreExecutors.directExecutor()); + } + + private ListenableFuture discoverRoom(final XmppConnection connection, final Jid room) { + final var request = new Iq(Iq.Type.GET); + request.addExtension(new InfoQuery()); + request.setTo(room); + final var infoQueryResponseFuture = connection.sendIqPacket(request); + return Futures.transform( + infoQueryResponseFuture, + result -> { + final var infoQuery = result.getExtension(InfoQuery.class); + if (infoQuery == null) { + return null; + } + return Room.of(room, infoQuery); + }, + MoreExecutors.directExecutor()); } private void finishDiscoSearch( - List rooms, String query, OnChannelSearchResultsFound listener) { - Collections.sort(rooms); - cache.put(key(Method.LOCAL_SERVER, ""), rooms); + final List rooms, + final String query, + final OnChannelSearchResultsFound listener) { + Log.d(Config.LOGTAG, "finishDiscoSearch with " + rooms.size() + " rooms"); + final var sorted = Ordering.natural().sortedCopy(rooms); + cache.put(key(Method.LOCAL_SERVER, ""), sorted); if (query.isEmpty()) { - listener.onChannelSearchResultsFound(rooms); + listener.onChannelSearchResultsFound(sorted); } else { - List results = copyMatching(rooms, query); + List results = copyMatching(sorted, query); cache.put(key(Method.LOCAL_SERVER, query), results); - listener.onChannelSearchResultsFound(rooms); + listener.onChannelSearchResultsFound(sorted); } } @@ -242,23 +298,21 @@ public class ChannelDiscoveryService { return result; } - private Map getLocalMucServices() { - final HashMap localMucServices = new HashMap<>(); - for (Account account : service.getAccounts()) { - if (account.isEnabled()) { - final XmppConnection xmppConnection = account.getXmppConnection(); - if (xmppConnection == null) { - continue; - } - for (final String mucService : xmppConnection.getMucServers()) { - final Jid jid = Jid.of(mucService); - if (!localMucServices.containsKey(jid)) { - localMucServices.put(jid, account); + private Map getLocalMucServices() { + final ImmutableMap.Builder localMucServices = + new ImmutableMap.Builder<>(); + 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); } } } } - return localMucServices; + return localMucServices.buildKeepingLast(); } private static String key(Method method, String query) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0181a2f76509df3ffd6f63a58c061379cabe5e61..687339108ded64da19dad7423f569918ba82a2d8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4369,7 +4369,6 @@ public class XmppConnectionService extends Service { public void fetchConferenceConfiguration( final Conversation conversation, final OnConferenceConfigurationFetched callback) { - final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); final var account = conversation.getAccount(); final var connection = account.getXmppConnection(); if (connection == null) { @@ -4381,7 +4380,7 @@ public class XmppConnectionService extends Service { .info(Entity.discoItem(conversation.getJid().asBareJid()), null); Futures.addCallback( future, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(InfoQuery result) { final MucOptions mucOptions = conversation.getMucOptions(); 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 7fc03360d3d275ec27c20d7dc0df8b1987c4298a..d0d546b37bdf5f9b944966828b9719aeb3db6923 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 @@ -33,6 +33,11 @@ public class Data extends Extension { return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null); } + public String getValue(final String name) { + final var field = getFieldByName(name); + return field == null ? null : field.getValue(); + } + private void addField(final String name, final Object value) { addField(name, value, null); }