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