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            OnChannelSearchResultsFound onChannelSearchResultsFound) {
 79        final List<Room> result = cache.getIfPresent(key(method, query));
 80        if (result != null) {
 81            onChannelSearchResultsFound.onChannelSearchResultsFound(result);
 82            return;
 83        }
 84        if (method == Method.LOCAL_SERVER) {
 85            discoverChannelsLocalServers(query, onChannelSearchResultsFound);
 86        } else {
 87            if (query.isEmpty()) {
 88                discoverChannelsJabberNetwork(onChannelSearchResultsFound);
 89            } else {
 90                discoverChannelsJabberNetwork(query, onChannelSearchResultsFound);
 91            }
 92        }
 93    }
 94
 95    private void discoverChannelsJabberNetwork(final OnChannelSearchResultsFound listener) {
 96        if (muclumbusService == null) {
 97            listener.onChannelSearchResultsFound(Collections.emptyList());
 98            return;
 99        }
100        final Call<MuclumbusService.Rooms> call = muclumbusService.getRooms(1);
101        call.enqueue(
102                new Callback<MuclumbusService.Rooms>() {
103                    @Override
104                    public void onResponse(
105                            @NonNull Call<MuclumbusService.Rooms> call,
106                            @NonNull Response<MuclumbusService.Rooms> response) {
107                        final MuclumbusService.Rooms body = response.body();
108                        if (body == null) {
109                            listener.onChannelSearchResultsFound(Collections.emptyList());
110                            logError(response);
111                            return;
112                        }
113                        cache.put(key(Method.JABBER_NETWORK, ""), body.items);
114                        listener.onChannelSearchResultsFound(body.items);
115                    }
116
117                    @Override
118                    public void onFailure(
119                            @NonNull Call<MuclumbusService.Rooms> call,
120                            @NonNull Throwable throwable) {
121                        Log.d(
122                                Config.LOGTAG,
123                                "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
124                                throwable);
125                        listener.onChannelSearchResultsFound(Collections.emptyList());
126                    }
127                });
128    }
129
130    private void discoverChannelsJabberNetwork(
131            final String query, final OnChannelSearchResultsFound listener) {
132        if (muclumbusService == null) {
133            listener.onChannelSearchResultsFound(Collections.emptyList());
134            return;
135        }
136        final MuclumbusService.SearchRequest searchRequest =
137                new MuclumbusService.SearchRequest(query);
138        final Call<MuclumbusService.SearchResult> searchResultCall =
139                muclumbusService.search(searchRequest);
140        searchResultCall.enqueue(
141                new Callback<MuclumbusService.SearchResult>() {
142                    @Override
143                    public void onResponse(
144                            @NonNull Call<MuclumbusService.SearchResult> call,
145                            @NonNull Response<MuclumbusService.SearchResult> response) {
146                        final MuclumbusService.SearchResult body = response.body();
147                        if (body == null) {
148                            listener.onChannelSearchResultsFound(Collections.emptyList());
149                            logError(response);
150                            return;
151                        }
152                        cache.put(key(Method.JABBER_NETWORK, query), body.result.items);
153                        listener.onChannelSearchResultsFound(body.result.items);
154                    }
155
156                    @Override
157                    public void onFailure(
158                            @NonNull Call<MuclumbusService.SearchResult> call,
159                            @NonNull Throwable throwable) {
160                        Log.d(
161                                Config.LOGTAG,
162                                "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY,
163                                throwable);
164                        listener.onChannelSearchResultsFound(Collections.emptyList());
165                    }
166                });
167    }
168
169    private void discoverChannelsLocalServers(
170            final String query, final OnChannelSearchResultsFound listener) {
171        final Map<Jid, Account> localMucService = getLocalMucServices();
172        Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
173        if (localMucService.size() == 0) {
174            listener.onChannelSearchResultsFound(Collections.emptyList());
175            return;
176        }
177        if (!query.isEmpty()) {
178            final List<Room> cached = cache.getIfPresent(key(Method.LOCAL_SERVER, ""));
179            if (cached != null) {
180                final List<Room> results = copyMatching(cached, query);
181                cache.put(key(Method.LOCAL_SERVER, query), results);
182                listener.onChannelSearchResultsFound(results);
183            }
184        }
185        final AtomicInteger queriesInFlight = new AtomicInteger();
186        final List<Room> rooms = new ArrayList<>();
187        for (Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
188            IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
189            queriesInFlight.incrementAndGet();
190            service.sendIqPacket(
191                    entry.getValue(),
192                    itemsRequest,
193                    (account, itemsResponse) -> {
194                        if (itemsResponse.getType() == IqPacket.TYPE.RESULT) {
195                            final List<Jid> items = IqParser.items(itemsResponse);
196                            for (Jid item : items) {
197                                IqPacket infoRequest =
198                                        service.getIqGenerator().queryDiscoInfo(item);
199                                queriesInFlight.incrementAndGet();
200                                service.sendIqPacket(
201                                        account,
202                                        infoRequest,
203                                        new OnIqPacketReceived() {
204                                            @Override
205                                            public void onIqPacketReceived(
206                                                    Account account, IqPacket infoResponse) {
207                                                if (infoResponse.getType()
208                                                        == IqPacket.TYPE.RESULT) {
209                                                    final Room room =
210                                                            IqParser.parseRoom(infoResponse);
211                                                    if (room != null) {
212                                                        rooms.add(room);
213                                                    }
214                                                    if (queriesInFlight.decrementAndGet() <= 0) {
215                                                        finishDiscoSearch(rooms, query, listener);
216                                                    }
217                                                } else {
218                                                    queriesInFlight.decrementAndGet();
219                                                }
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}