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