ChannelDiscoveryService.java

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