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}