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