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 = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
54 if (service.useTorToConnect()) {
55 builder.proxy(HttpConnectionManager.getProxy());
56 }
57 Retrofit retrofit = new Retrofit.Builder()
58 .client(builder.build())
59 .baseUrl(Config.CHANNEL_DISCOVERY)
60 .addConverterFactory(GsonConverterFactory.create())
61 .callbackExecutor(Executors.newSingleThreadExecutor())
62 .build();
63 this.muclumbusService = retrofit.create(MuclumbusService.class);
64 }
65
66 void cleanCache() {
67 cache.invalidateAll();
68 }
69
70 void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) {
71 final List<Room> result = cache.getIfPresent(key(method, query));
72 if (result != null) {
73 onChannelSearchResultsFound.onChannelSearchResultsFound(result);
74 return;
75 }
76 if (method == Method.LOCAL_SERVER) {
77 discoverChannelsLocalServers(query, onChannelSearchResultsFound);
78 } else {
79 if (query.isEmpty()) {
80 discoverChannelsJabberNetwork(onChannelSearchResultsFound);
81 } else {
82 discoverChannelsJabberNetwork(query, onChannelSearchResultsFound);
83 }
84 }
85 }
86
87 private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) {
88 Call<MuclumbusService.Rooms> call = muclumbusService.getRooms(1);
89 try {
90 call.enqueue(new Callback<MuclumbusService.Rooms>() {
91 @Override
92 public void onResponse(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Response<MuclumbusService.Rooms> response) {
93 final MuclumbusService.Rooms body = response.body();
94 if (body == null) {
95 listener.onChannelSearchResultsFound(Collections.emptyList());
96 logError(response);
97 return;
98 }
99 cache.put(key(Method.JABBER_NETWORK, ""), body.items);
100 listener.onChannelSearchResultsFound(body.items);
101 }
102
103 @Override
104 public void onFailure(@NonNull Call<MuclumbusService.Rooms> call, @NonNull Throwable throwable) {
105 Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
106 listener.onChannelSearchResultsFound(Collections.emptyList());
107 }
108 });
109 } catch (Exception e) {
110 e.printStackTrace();
111 }
112 }
113
114 private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) {
115 MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query);
116 Call<MuclumbusService.SearchResult> searchResultCall = muclumbusService.search(searchRequest);
117
118 searchResultCall.enqueue(new Callback<MuclumbusService.SearchResult>() {
119 @Override
120 public void onResponse(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Response<MuclumbusService.SearchResult> response) {
121 final MuclumbusService.SearchResult body = response.body();
122 if (body == null) {
123 listener.onChannelSearchResultsFound(Collections.emptyList());
124 logError(response);
125 return;
126 }
127 cache.put(key(Method.JABBER_NETWORK, query), body.result.items);
128 listener.onChannelSearchResultsFound(body.result.items);
129 }
130
131 @Override
132 public void onFailure(@NonNull Call<MuclumbusService.SearchResult> call, @NonNull Throwable throwable) {
133 Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable);
134 listener.onChannelSearchResultsFound(Collections.emptyList());
135 }
136 });
137 }
138
139 private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) {
140 final Map<Jid, Account> localMucService = getLocalMucServices();
141 Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
142 if (localMucService.size() == 0) {
143 listener.onChannelSearchResultsFound(Collections.emptyList());
144 return;
145 }
146 if (!query.isEmpty()) {
147 final List<Room> cached = cache.getIfPresent(key(Method.LOCAL_SERVER, ""));
148 if (cached != null) {
149 final List<Room> results = copyMatching(cached, query);
150 cache.put(key(Method.LOCAL_SERVER, query), results);
151 listener.onChannelSearchResultsFound(results);
152 }
153 }
154 final AtomicInteger queriesInFlight = new AtomicInteger();
155 final List<Room> rooms = new ArrayList<>();
156 for (Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
157 IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
158 queriesInFlight.incrementAndGet();
159 service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> {
160 if (itemsResponse.getType() == IqPacket.TYPE.RESULT) {
161 final List<Jid> items = IqParser.items(itemsResponse);
162 for (Jid item : items) {
163 IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item);
164 queriesInFlight.incrementAndGet();
165 service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() {
166 @Override
167 public void onIqPacketReceived(Account account, IqPacket infoResponse) {
168 if (infoResponse.getType() == IqPacket.TYPE.RESULT) {
169 final Room room = IqParser.parseRoom(infoResponse);
170 if (room != null) {
171 rooms.add(room);
172 }
173 if (queriesInFlight.decrementAndGet() <= 0) {
174 finishDiscoSearch(rooms, query, listener);
175 }
176 } else {
177 queriesInFlight.decrementAndGet();
178 }
179 }
180 });
181 }
182 }
183 if (queriesInFlight.decrementAndGet() <= 0) {
184 finishDiscoSearch(rooms, query, listener);
185 }
186 });
187 }
188 }
189
190 private void finishDiscoSearch(List<Room> rooms, String query, OnChannelSearchResultsFound listener) {
191 Collections.sort(rooms);
192 cache.put(key(Method.LOCAL_SERVER, ""), rooms);
193 if (query.isEmpty()) {
194 listener.onChannelSearchResultsFound(rooms);
195 } else {
196 List<Room> results = copyMatching(rooms, query);
197 cache.put(key(Method.LOCAL_SERVER, query), results);
198 listener.onChannelSearchResultsFound(rooms);
199 }
200 }
201
202 private static List<Room> copyMatching(List<Room> haystack, String needle) {
203 ArrayList<Room> result = new ArrayList<>();
204 for (Room room : haystack) {
205 if (room.contains(needle)) {
206 result.add(room);
207 }
208 }
209 return result;
210 }
211
212 private Map<Jid, Account> getLocalMucServices() {
213 final HashMap<Jid, Account> localMucServices = new HashMap<>();
214 for (Account account : service.getAccounts()) {
215 if (account.isEnabled()) {
216 final XmppConnection xmppConnection = account.getXmppConnection();
217 if (xmppConnection == null) {
218 continue;
219 }
220 for (final String mucService : xmppConnection.getMucServers()) {
221 Jid jid = Jid.ofEscaped(mucService);
222 if (!localMucServices.containsKey(jid)) {
223 localMucServices.put(jid, account);
224 }
225 }
226 }
227 }
228 return localMucServices;
229 }
230
231 private static String key(Method method, String query) {
232 return String.format("%s\00%s", method, query);
233 }
234
235 private static void logError(final Response response) {
236 final ResponseBody errorBody = response.errorBody();
237 Log.d(Config.LOGTAG, "code from muclumbus=" + response.code());
238 if (errorBody == null) {
239 return;
240 }
241 try {
242 Log.d(Config.LOGTAG, "error body=" + errorBody.string());
243 } catch (IOException e) {
244 //ignored
245 }
246 }
247
248 public interface OnChannelSearchResultsFound {
249 void onChannelSearchResultsFound(List<Room> results);
250 }
251
252 public enum Method {
253 JABBER_NETWORK,
254 LOCAL_SERVER
255 }
256}