ChannelDiscoveryService.java

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