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                                IqPacket infoRequest =
199                                        service.getIqGenerator().queryDiscoInfo(item);
200                                queriesInFlight.incrementAndGet();
201                                service.sendIqPacket(
202                                        account,
203                                        infoRequest,
204                                        new OnIqPacketReceived() {
205                                            @Override
206                                            public void onIqPacketReceived(
207                                                    Account account, IqPacket infoResponse) {
208                                                if (infoResponse.getType()
209                                                        == IqPacket.TYPE.RESULT) {
210                                                    final Room room =
211                                                            IqParser.parseRoom(infoResponse);
212                                                    if (room != null) {
213                                                        rooms.add(room);
214                                                    }
215                                                }
216                                                if (queriesInFlight.decrementAndGet() <= 0) {
217                                                    finishDiscoSearch(rooms, query, mucServices, listener);
218                                                }
219                                            }
220                                        }, 20L);
221                            }
222                        }
223                        if (queriesInFlight.decrementAndGet() <= 0) {
224                            finishDiscoSearch(rooms, query, mucServices, listener);
225                        }
226                    });
227        }
228    }
229
230    private void finishDiscoSearch(
231            List<Room> rooms, String query, Map<Jid, Account> mucServices, OnChannelSearchResultsFound listener) {
232        Collections.sort(rooms);
233        cache.put(key(Method.LOCAL_SERVER, mucServices, ""), rooms);
234        if (query.isEmpty()) {
235            listener.onChannelSearchResultsFound(rooms);
236        } else {
237            List<Room> results = copyMatching(rooms, query);
238            cache.put(key(Method.LOCAL_SERVER, mucServices, query), results);
239            listener.onChannelSearchResultsFound(rooms);
240        }
241    }
242
243    private static List<Room> copyMatching(List<Room> haystack, String needle) {
244        ArrayList<Room> result = new ArrayList<>();
245        for (Room room : haystack) {
246            if (room.contains(needle)) {
247                result.add(room);
248            }
249        }
250        return result;
251    }
252
253    private Map<Jid, Account> getLocalMucServices() {
254        final HashMap<Jid, Account> localMucServices = new HashMap<>();
255        for (Account account : service.getAccounts()) {
256            if (account.isEnabled()) {
257                final XmppConnection xmppConnection = account.getXmppConnection();
258                if (xmppConnection == null) {
259                    continue;
260                }
261                for (final String mucService : xmppConnection.getMucServers()) {
262                    Jid jid = Jid.ofEscaped(mucService);
263                    if (!localMucServices.containsKey(jid)) {
264                        localMucServices.put(jid, account);
265                    }
266                }
267            }
268        }
269        return localMucServices;
270    }
271
272    private static String key(Method method, Map<Jid, Account> mucServices, String query) {
273        final String servicesKey = mucServices == null ? "\00" : String.join("\00", mucServices.keySet());
274        return String.format("%s\00%s\00%s", method, servicesKey, 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}