ChannelDiscoveryService.java

  1package eu.siacs.conversations.services;
  2
  3
  4import android.util.Log;
  5
  6import androidx.annotation.NonNull;
  7
  8import com.google.common.base.Strings;
  9import com.google.common.cache.Cache;
 10import com.google.common.cache.CacheBuilder;
 11
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.entities.Account;
 14import eu.siacs.conversations.entities.Room;
 15import eu.siacs.conversations.http.HttpConnectionManager;
 16import eu.siacs.conversations.http.services.MuclumbusService;
 17import eu.siacs.conversations.parser.IqParser;
 18import eu.siacs.conversations.xmpp.Jid;
 19import eu.siacs.conversations.xmpp.XmppConnection;
 20
 21import im.conversations.android.xmpp.model.stanza.Iq;
 22
 23import okhttp3.OkHttpClient;
 24import okhttp3.ResponseBody;
 25
 26import retrofit2.Call;
 27import retrofit2.Callback;
 28import retrofit2.Response;
 29import retrofit2.Retrofit;
 30import retrofit2.converter.gson.GsonConverterFactory;
 31
 32import java.io.IOException;
 33import java.util.ArrayList;
 34import java.util.Collections;
 35import java.util.HashMap;
 36import java.util.List;
 37import java.util.Map;
 38import java.util.concurrent.Executors;
 39import java.util.concurrent.TimeUnit;
 40import java.util.concurrent.atomic.AtomicInteger;
 41
 42public class ChannelDiscoveryService {
 43
 44    private final XmppConnectionService service;
 45
 46    private MuclumbusService muclumbusService;
 47
 48    private final Cache<String, List<Room>> cache;
 49
 50    ChannelDiscoveryService(XmppConnectionService service) {
 51        this.service = service;
 52        this.cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
 53    }
 54
 55    void initializeMuclumbusService() {
 56        if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
 57            this.muclumbusService = null;
 58            return;
 59        }
 60        final OkHttpClient.Builder builder = HttpConnectionManager.okHttpClient(service).newBuilder();
 61        if (service.useTorToConnect()) {
 62            builder.proxy(HttpConnectionManager.getProxy());
 63        }
 64        final Retrofit retrofit =
 65                new Retrofit.Builder()
 66                        .client(builder.build())
 67                        .baseUrl(Config.CHANNEL_DISCOVERY)
 68                        .addConverterFactory(GsonConverterFactory.create())
 69                        .callbackExecutor(Executors.newSingleThreadExecutor())
 70                        .build();
 71        this.muclumbusService = retrofit.create(MuclumbusService.class);
 72    }
 73
 74    void cleanCache() {
 75        cache.invalidateAll();
 76    }
 77
 78    void discover(
 79            @NonNull final String query,
 80            Method method,
 81            OnChannelSearchResultsFound onChannelSearchResultsFound) {
 82        final List<Room> result = cache.getIfPresent(key(method, query));
 83        if (result != null) {
 84            onChannelSearchResultsFound.onChannelSearchResultsFound(result);
 85            return;
 86        }
 87        if (method == Method.LOCAL_SERVER) {
 88            discoverChannelsLocalServers(query, onChannelSearchResultsFound);
 89        } else {
 90            if (query.isEmpty()) {
 91                discoverChannelsJabberNetwork(onChannelSearchResultsFound);
 92            } else {
 93                discoverChannelsJabberNetwork(query, onChannelSearchResultsFound);
 94            }
 95        }
 96    }
 97
 98    private void discoverChannelsJabberNetwork(final OnChannelSearchResultsFound listener) {
 99        if (muclumbusService == null) {
100            listener.onChannelSearchResultsFound(Collections.emptyList());
101            return;
102        }
103        final Call<MuclumbusService.Rooms> call = muclumbusService.getRooms(1);
104        call.enqueue(
105                new Callback<MuclumbusService.Rooms>() {
106                    @Override
107                    public void onResponse(
108                            @NonNull Call<MuclumbusService.Rooms> call,
109                            @NonNull Response<MuclumbusService.Rooms> response) {
110                        final MuclumbusService.Rooms body = response.body();
111                        if (body == null) {
112                            listener.onChannelSearchResultsFound(Collections.emptyList());
113                            logError(response);
114                            return;
115                        }
116                        cache.put(key(Method.JABBER_NETWORK, ""), body.items);
117                        listener.onChannelSearchResultsFound(body.items);
118                    }
119
120                    @Override
121                    public void onFailure(
122                            @NonNull Call<MuclumbusService.Rooms> call,
123                            @NonNull Throwable throwable) {
124                        Log.d(
125                                Config.LOGTAG,
126                                "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
127                                throwable);
128                        listener.onChannelSearchResultsFound(Collections.emptyList());
129                    }
130                });
131    }
132
133    private void discoverChannelsJabberNetwork(
134            final String query, final OnChannelSearchResultsFound listener) {
135        if (muclumbusService == null) {
136            listener.onChannelSearchResultsFound(Collections.emptyList());
137            return;
138        }
139        final MuclumbusService.SearchRequest searchRequest =
140                new MuclumbusService.SearchRequest(query);
141        final Call<MuclumbusService.SearchResult> searchResultCall =
142                muclumbusService.search(searchRequest);
143        searchResultCall.enqueue(
144                new Callback<MuclumbusService.SearchResult>() {
145                    @Override
146                    public void onResponse(
147                            @NonNull Call<MuclumbusService.SearchResult> call,
148                            @NonNull Response<MuclumbusService.SearchResult> response) {
149                        final MuclumbusService.SearchResult body = response.body();
150                        if (body == null) {
151                            listener.onChannelSearchResultsFound(Collections.emptyList());
152                            logError(response);
153                            return;
154                        }
155                        cache.put(key(Method.JABBER_NETWORK, query), body.result.items);
156                        listener.onChannelSearchResultsFound(body.result.items);
157                    }
158
159                    @Override
160                    public void onFailure(
161                            @NonNull Call<MuclumbusService.SearchResult> call,
162                            @NonNull Throwable throwable) {
163                        Log.d(
164                                Config.LOGTAG,
165                                "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
166                                throwable);
167                        listener.onChannelSearchResultsFound(Collections.emptyList());
168                    }
169                });
170    }
171
172    private void discoverChannelsLocalServers(
173            final String query, final OnChannelSearchResultsFound listener) {
174        final Map<Jid, Account> localMucService = getLocalMucServices();
175        Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
176        if (localMucService.isEmpty()) {
177            listener.onChannelSearchResultsFound(Collections.emptyList());
178            return;
179        }
180        if (!query.isEmpty()) {
181            final List<Room> cached = cache.getIfPresent(key(Method.LOCAL_SERVER, ""));
182            if (cached != null) {
183                final List<Room> results = copyMatching(cached, query);
184                cache.put(key(Method.LOCAL_SERVER, query), results);
185                listener.onChannelSearchResultsFound(results);
186            }
187        }
188        final AtomicInteger queriesInFlight = new AtomicInteger();
189        final List<Room> rooms = new ArrayList<>();
190        for (final Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
191            Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
192            queriesInFlight.incrementAndGet();
193            final var account = entry.getValue();
194            service.sendIqPacket(
195                    account,
196                    itemsRequest,
197                    (itemsResponse) -> {
198                        if (itemsResponse.getType() == Iq.Type.RESULT) {
199                            final List<Jid> items = IqParser.items(itemsResponse);
200                            for (final Jid item : items) {
201                                final Iq infoRequest =
202                                        service.getIqGenerator().queryDiscoInfo(item);
203                                queriesInFlight.incrementAndGet();
204                                service.sendIqPacket(
205                                        account,
206                                        infoRequest,
207                                        infoResponse -> {
208                                            if (infoResponse.getType()
209                                                    == Iq.Type.RESULT) {
210                                                final Room room =
211                                                        IqParser.parseRoom(infoResponse);
212                                                if (room != null) {
213                                                    rooms.add(room);
214                                                }
215                                                if (queriesInFlight.decrementAndGet() <= 0) {
216                                                    finishDiscoSearch(rooms, query, listener);
217                                                }
218                                            } else {
219                                                queriesInFlight.decrementAndGet();
220                                            }
221                                        });
222                            }
223                        }
224                        if (queriesInFlight.decrementAndGet() <= 0) {
225                            finishDiscoSearch(rooms, query, listener);
226                        }
227                    });
228        }
229    }
230
231    private void finishDiscoSearch(
232            List<Room> rooms, String query, OnChannelSearchResultsFound listener) {
233        Collections.sort(rooms);
234        cache.put(key(Method.LOCAL_SERVER, ""), rooms);
235        if (query.isEmpty()) {
236            listener.onChannelSearchResultsFound(rooms);
237        } else {
238            List<Room> results = copyMatching(rooms, query);
239            cache.put(key(Method.LOCAL_SERVER, query), results);
240            listener.onChannelSearchResultsFound(rooms);
241        }
242    }
243
244    private static List<Room> copyMatching(List<Room> haystack, String needle) {
245        ArrayList<Room> result = new ArrayList<>();
246        for (Room room : haystack) {
247            if (room.contains(needle)) {
248                result.add(room);
249            }
250        }
251        return result;
252    }
253
254    private Map<Jid, Account> getLocalMucServices() {
255        final HashMap<Jid, Account> localMucServices = new HashMap<>();
256        for (Account account : service.getAccounts()) {
257            if (account.isEnabled()) {
258                final XmppConnection xmppConnection = account.getXmppConnection();
259                if (xmppConnection == null) {
260                    continue;
261                }
262                for (final String mucService : xmppConnection.getMucServers()) {
263                    Jid jid = Jid.ofEscaped(mucService);
264                    if (!localMucServices.containsKey(jid)) {
265                        localMucServices.put(jid, account);
266                    }
267                }
268            }
269        }
270        return localMucServices;
271    }
272
273    private static String key(Method method, String query) {
274        return String.format("%s\00%s", method, query);
275    }
276
277    private static void logError(final Response response) {
278        final ResponseBody errorBody = response.errorBody();
279        Log.d(Config.LOGTAG, "code from muclumbus=" + response.code());
280        if (errorBody == null) {
281            return;
282        }
283        try {
284            Log.d(Config.LOGTAG, "error body=" + errorBody.string());
285        } catch (IOException e) {
286            // ignored
287        }
288    }
289
290    public interface OnChannelSearchResultsFound {
291        void onChannelSearchResultsFound(List<Room> results);
292    }
293
294    public enum Method {
295        JABBER_NETWORK,
296        LOCAL_SERVER
297    }
298}