1 //
2 // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 #include "td/telegram/DialogFilter.h"
8 
9 #include "td/telegram/DialogId.h"
10 
11 #include "td/utils/algorithm.h"
12 #include "td/utils/emoji.h"
13 #include "td/utils/format.h"
14 #include "td/utils/logging.h"
15 
16 #include <unordered_set>
17 
18 namespace td {
19 
get_dialog_filter(telegram_api::object_ptr<telegram_api::dialogFilter> filter,bool with_id)20 unique_ptr<DialogFilter> DialogFilter::get_dialog_filter(telegram_api::object_ptr<telegram_api::dialogFilter> filter,
21                                                          bool with_id) {
22   DialogFilterId dialog_filter_id(filter->id_);
23   if (with_id && !dialog_filter_id.is_valid()) {
24     LOG(ERROR) << "Receive invalid " << to_string(filter);
25     return nullptr;
26   }
27   auto dialog_filter = make_unique<DialogFilter>();
28   dialog_filter->dialog_filter_id = dialog_filter_id;
29   dialog_filter->title = std::move(filter->title_);
30   dialog_filter->emoji = std::move(filter->emoticon_);
31   std::unordered_set<DialogId, DialogIdHash> added_dialog_ids;
32   dialog_filter->pinned_dialog_ids = InputDialogId::get_input_dialog_ids(filter->pinned_peers_, &added_dialog_ids);
33   dialog_filter->included_dialog_ids = InputDialogId::get_input_dialog_ids(filter->include_peers_, &added_dialog_ids);
34   dialog_filter->excluded_dialog_ids = InputDialogId::get_input_dialog_ids(filter->exclude_peers_, &added_dialog_ids);
35   auto flags = filter->flags_;
36   dialog_filter->exclude_muted = (flags & telegram_api::dialogFilter::EXCLUDE_MUTED_MASK) != 0;
37   dialog_filter->exclude_read = (flags & telegram_api::dialogFilter::EXCLUDE_READ_MASK) != 0;
38   dialog_filter->exclude_archived = (flags & telegram_api::dialogFilter::EXCLUDE_ARCHIVED_MASK) != 0;
39   dialog_filter->include_contacts = (flags & telegram_api::dialogFilter::CONTACTS_MASK) != 0;
40   dialog_filter->include_non_contacts = (flags & telegram_api::dialogFilter::NON_CONTACTS_MASK) != 0;
41   dialog_filter->include_bots = (flags & telegram_api::dialogFilter::BOTS_MASK) != 0;
42   dialog_filter->include_groups = (flags & telegram_api::dialogFilter::GROUPS_MASK) != 0;
43   dialog_filter->include_channels = (flags & telegram_api::dialogFilter::BROADCASTS_MASK) != 0;
44   return dialog_filter;
45 }
46 
remove_secret_chat_dialog_ids()47 void DialogFilter::remove_secret_chat_dialog_ids() {
48   auto remove_secret_chats = [](vector<InputDialogId> &input_dialog_ids) {
49     td::remove_if(input_dialog_ids, [](InputDialogId input_dialog_id) {
50       return input_dialog_id.get_dialog_id().get_type() == DialogType::SecretChat;
51     });
52   };
53   remove_secret_chats(pinned_dialog_ids);
54   remove_secret_chats(included_dialog_ids);
55   remove_secret_chats(excluded_dialog_ids);
56 }
57 
is_empty(bool for_server) const58 bool DialogFilter::is_empty(bool for_server) const {
59   if (include_contacts || include_non_contacts || include_bots || include_groups || include_channels) {
60     return false;
61   }
62 
63   if (for_server) {
64     vector<InputDialogId> empty_input_dialog_ids;
65     return InputDialogId::are_equivalent(pinned_dialog_ids, empty_input_dialog_ids) &&
66            InputDialogId::are_equivalent(included_dialog_ids, empty_input_dialog_ids);
67   } else {
68     return pinned_dialog_ids.empty() && included_dialog_ids.empty();
69   }
70 }
71 
check_limits() const72 Status DialogFilter::check_limits() const {
73   auto get_server_dialog_count = [](const vector<InputDialogId> &input_dialog_ids) {
74     int32 result = 0;
75     for (auto &input_dialog_id : input_dialog_ids) {
76       if (input_dialog_id.get_dialog_id().get_type() != DialogType::SecretChat) {
77         result++;
78       }
79     }
80     return result;
81   };
82 
83   auto excluded_server_dialog_count = get_server_dialog_count(excluded_dialog_ids);
84   auto included_server_dialog_count = get_server_dialog_count(included_dialog_ids);
85   auto pinned_server_dialog_count = get_server_dialog_count(pinned_dialog_ids);
86 
87   auto excluded_secret_dialog_count = static_cast<int32>(excluded_dialog_ids.size()) - excluded_server_dialog_count;
88   auto included_secret_dialog_count = static_cast<int32>(included_dialog_ids.size()) - included_server_dialog_count;
89   auto pinned_secret_dialog_count = static_cast<int32>(pinned_dialog_ids.size()) - pinned_server_dialog_count;
90 
91   if (excluded_server_dialog_count > MAX_INCLUDED_FILTER_DIALOGS ||
92       excluded_secret_dialog_count > MAX_INCLUDED_FILTER_DIALOGS) {
93     return Status::Error(400, "The maximum number of excluded chats exceeded");
94   }
95   if (included_server_dialog_count > MAX_INCLUDED_FILTER_DIALOGS ||
96       included_secret_dialog_count > MAX_INCLUDED_FILTER_DIALOGS) {
97     return Status::Error(400, "The maximum number of included chats exceeded");
98   }
99   if (included_server_dialog_count + pinned_server_dialog_count > MAX_INCLUDED_FILTER_DIALOGS ||
100       included_secret_dialog_count + pinned_secret_dialog_count > MAX_INCLUDED_FILTER_DIALOGS) {
101     return Status::Error(400, "The maximum number of pinned chats exceeded");
102   }
103 
104   if (is_empty(false)) {
105     return Status::Error(400, "Folder must contain at least 1 chat");
106   }
107 
108   if (include_contacts && include_non_contacts && include_bots && include_groups && include_channels &&
109       exclude_archived && !exclude_read && !exclude_muted) {
110     return Status::Error(400, "Folder must be different from the main chat list");
111   }
112 
113   return Status::OK();
114 }
115 
get_emoji_by_icon_name(const string & icon_name)116 string DialogFilter::get_emoji_by_icon_name(const string &icon_name) {
117   init_icon_names();
118   auto it = icon_name_to_emoji_.find(icon_name);
119   if (it != icon_name_to_emoji_.end()) {
120     return it->second;
121   }
122   return string();
123 }
124 
get_icon_name() const125 string DialogFilter::get_icon_name() const {
126   init_icon_names();
127   auto it = emoji_to_icon_name_.find(emoji);
128   if (it != emoji_to_icon_name_.end()) {
129     return it->second;
130   }
131   return string();
132 }
133 
get_default_icon_name(const td_api::chatFilter * filter)134 string DialogFilter::get_default_icon_name(const td_api::chatFilter *filter) {
135   if (!filter->icon_name_.empty() && !get_emoji_by_icon_name(filter->icon_name_).empty()) {
136     return filter->icon_name_;
137   }
138 
139   if (!filter->pinned_chat_ids_.empty() || !filter->included_chat_ids_.empty() || !filter->excluded_chat_ids_.empty()) {
140     return "Custom";
141   }
142 
143   if (filter->include_contacts_ || filter->include_non_contacts_) {
144     if (!filter->include_bots_ && !filter->include_groups_ && !filter->include_channels_) {
145       return "Private";
146     }
147   } else {
148     if (!filter->include_bots_ && !filter->include_channels_) {
149       if (!filter->include_groups_) {
150         // just in case
151         return "Custom";
152       }
153       return "Groups";
154     }
155     if (!filter->include_bots_ && !filter->include_groups_) {
156       return "Channels";
157     }
158     if (!filter->include_groups_ && !filter->include_channels_) {
159       return "Bots";
160     }
161   }
162   if (filter->exclude_read_ && !filter->exclude_muted_) {
163     return "Unread";
164   }
165   if (filter->exclude_muted_ && !filter->exclude_read_) {
166     return "Unmuted";
167   }
168   return "Custom";
169 }
170 
get_input_dialog_filter() const171 telegram_api::object_ptr<telegram_api::dialogFilter> DialogFilter::get_input_dialog_filter() const {
172   int32 flags = 0;
173   if (!emoji.empty()) {
174     flags |= telegram_api::dialogFilter::EMOTICON_MASK;
175   }
176   if (exclude_muted) {
177     flags |= telegram_api::dialogFilter::EXCLUDE_MUTED_MASK;
178   }
179   if (exclude_read) {
180     flags |= telegram_api::dialogFilter::EXCLUDE_READ_MASK;
181   }
182   if (exclude_archived) {
183     flags |= telegram_api::dialogFilter::EXCLUDE_ARCHIVED_MASK;
184   }
185   if (include_contacts) {
186     flags |= telegram_api::dialogFilter::CONTACTS_MASK;
187   }
188   if (include_non_contacts) {
189     flags |= telegram_api::dialogFilter::NON_CONTACTS_MASK;
190   }
191   if (include_bots) {
192     flags |= telegram_api::dialogFilter::BOTS_MASK;
193   }
194   if (include_groups) {
195     flags |= telegram_api::dialogFilter::GROUPS_MASK;
196   }
197   if (include_channels) {
198     flags |= telegram_api::dialogFilter::BROADCASTS_MASK;
199   }
200 
201   return telegram_api::make_object<telegram_api::dialogFilter>(
202       flags, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/, false /*ignored*/,
203       false /*ignored*/, false /*ignored*/, false /*ignored*/, dialog_filter_id.get(), title, emoji,
204       InputDialogId::get_input_peers(pinned_dialog_ids), InputDialogId::get_input_peers(included_dialog_ids),
205       InputDialogId::get_input_peers(excluded_dialog_ids));
206 }
207 
get_chat_filter_info_object() const208 td_api::object_ptr<td_api::chatFilterInfo> DialogFilter::get_chat_filter_info_object() const {
209   return td_api::make_object<td_api::chatFilterInfo>(dialog_filter_id.get(), title, get_icon_name());
210 }
211 
212 // merges changes from old_server_filter to new_server_filter in old_filter
merge_dialog_filter_changes(const DialogFilter * old_filter,const DialogFilter * old_server_filter,const DialogFilter * new_server_filter)213 unique_ptr<DialogFilter> DialogFilter::merge_dialog_filter_changes(const DialogFilter *old_filter,
214                                                                    const DialogFilter *old_server_filter,
215                                                                    const DialogFilter *new_server_filter) {
216   CHECK(old_filter != nullptr);
217   CHECK(old_server_filter != nullptr);
218   CHECK(new_server_filter != nullptr);
219   CHECK(old_filter->dialog_filter_id == old_server_filter->dialog_filter_id);
220   CHECK(old_filter->dialog_filter_id == new_server_filter->dialog_filter_id);
221   auto dialog_filter_id = old_filter->dialog_filter_id;
222   auto new_filter = make_unique<DialogFilter>(*old_filter);
223   new_filter->dialog_filter_id = dialog_filter_id;
224 
225   auto merge_ordered_changes = [dialog_filter_id](auto &new_dialog_ids, auto old_server_dialog_ids,
226                                                   auto new_server_dialog_ids) {
227     if (old_server_dialog_ids == new_server_dialog_ids) {
228       LOG(INFO) << "Pinned chats was not changed remotely in " << dialog_filter_id << ", keep local changes";
229       return;
230     }
231 
232     if (InputDialogId::are_equivalent(new_dialog_ids, old_server_dialog_ids)) {
233       LOG(INFO) << "Pinned chats was not changed locally in " << dialog_filter_id << ", keep remote changes";
234 
235       size_t kept_server_dialogs = 0;
236       std::unordered_set<DialogId, DialogIdHash> removed_dialog_ids;
237       auto old_it = old_server_dialog_ids.rbegin();
238       for (auto &input_dialog_id : reversed(new_server_dialog_ids)) {
239         auto dialog_id = input_dialog_id.get_dialog_id();
240         while (old_it < old_server_dialog_ids.rend()) {
241           if (old_it->get_dialog_id() == dialog_id) {
242             kept_server_dialogs++;
243             ++old_it;
244             break;
245           }
246 
247           // remove the dialog, it could be added back later
248           removed_dialog_ids.insert(old_it->get_dialog_id());
249           ++old_it;
250         }
251       }
252       while (old_it < old_server_dialog_ids.rend()) {
253         // remove the dialog, it could be added back later
254         removed_dialog_ids.insert(old_it->get_dialog_id());
255         ++old_it;
256       }
257       td::remove_if(new_dialog_ids, [&removed_dialog_ids](auto input_dialog_id) {
258         return removed_dialog_ids.count(input_dialog_id.get_dialog_id()) > 0;
259       });
260       new_dialog_ids.insert(new_dialog_ids.begin(), new_server_dialog_ids.begin(),
261                             new_server_dialog_ids.end() - kept_server_dialogs);
262     } else {
263       LOG(WARNING) << "Ignore remote changes of pinned chats in " << dialog_filter_id;
264       // there are both local and remote changes; ignore remote changes for now
265     }
266   };
267 
268   auto merge_changes = [](auto &new_dialog_ids, const auto &old_server_dialog_ids, const auto &new_server_dialog_ids) {
269     if (old_server_dialog_ids == new_server_dialog_ids) {
270       // fast path
271       return;
272     }
273 
274     // merge additions and deletions from other clients to the local changes
275     std::unordered_set<DialogId, DialogIdHash> deleted_dialog_ids;
276     for (const auto &old_dialog_id : old_server_dialog_ids) {
277       deleted_dialog_ids.insert(old_dialog_id.get_dialog_id());
278     }
279     std::unordered_set<DialogId, DialogIdHash> added_dialog_ids;
280     for (const auto &new_dialog_id : new_server_dialog_ids) {
281       auto dialog_id = new_dialog_id.get_dialog_id();
282       if (deleted_dialog_ids.erase(dialog_id) == 0) {
283         added_dialog_ids.insert(dialog_id);
284       }
285     }
286     vector<InputDialogId> result;
287     for (const auto &input_dialog_id : new_dialog_ids) {
288       // do not add dialog twice
289       added_dialog_ids.erase(input_dialog_id.get_dialog_id());
290     }
291     for (const auto &new_dialog_id : new_server_dialog_ids) {
292       if (added_dialog_ids.count(new_dialog_id.get_dialog_id()) == 1) {
293         result.push_back(new_dialog_id);
294       }
295     }
296     for (const auto &input_dialog_id : new_dialog_ids) {
297       if (deleted_dialog_ids.count(input_dialog_id.get_dialog_id()) == 0) {
298         result.push_back(input_dialog_id);
299       }
300     }
301     new_dialog_ids = std::move(result);
302   };
303 
304   merge_ordered_changes(new_filter->pinned_dialog_ids, old_server_filter->pinned_dialog_ids,
305                         new_server_filter->pinned_dialog_ids);
306   merge_changes(new_filter->included_dialog_ids, old_server_filter->included_dialog_ids,
307                 new_server_filter->included_dialog_ids);
308   merge_changes(new_filter->excluded_dialog_ids, old_server_filter->excluded_dialog_ids,
309                 new_server_filter->excluded_dialog_ids);
310 
311   {
312     std::unordered_set<DialogId, DialogIdHash> added_dialog_ids;
313     auto remove_duplicates = [&added_dialog_ids](auto &input_dialog_ids) {
314       td::remove_if(input_dialog_ids, [&added_dialog_ids](auto input_dialog_id) {
315         return !added_dialog_ids.insert(input_dialog_id.get_dialog_id()).second;
316       });
317     };
318     remove_duplicates(new_filter->pinned_dialog_ids);
319     remove_duplicates(new_filter->included_dialog_ids);
320     remove_duplicates(new_filter->excluded_dialog_ids);
321   }
322 
323   auto update_value = [](auto &new_value, const auto &old_server_value, const auto &new_server_value) {
324     // if the value was changed from other client and wasn't changed from the current client, update it
325     if (new_server_value != old_server_value && old_server_value == new_value) {
326       new_value = new_server_value;
327     }
328   };
329 
330   update_value(new_filter->exclude_muted, old_server_filter->exclude_muted, new_server_filter->exclude_muted);
331   update_value(new_filter->exclude_read, old_server_filter->exclude_read, new_server_filter->exclude_read);
332   update_value(new_filter->exclude_archived, old_server_filter->exclude_archived, new_server_filter->exclude_archived);
333   update_value(new_filter->include_contacts, old_server_filter->include_contacts, new_server_filter->include_contacts);
334   update_value(new_filter->include_non_contacts, old_server_filter->include_non_contacts,
335                new_server_filter->include_non_contacts);
336   update_value(new_filter->include_bots, old_server_filter->include_bots, new_server_filter->include_bots);
337   update_value(new_filter->include_groups, old_server_filter->include_groups, new_server_filter->include_groups);
338   update_value(new_filter->include_channels, old_server_filter->include_channels, new_server_filter->include_channels);
339 
340   if (new_filter->check_limits().is_error()) {
341     LOG(WARNING) << "Failed to merge local and remote changes in " << new_filter->dialog_filter_id
342                  << ", keep only local changes";
343     *new_filter = *old_filter;
344   }
345 
346   update_value(new_filter->title, old_server_filter->title, new_server_filter->title);
347   update_value(new_filter->emoji, old_server_filter->emoji, new_server_filter->emoji);
348   return new_filter;
349 }
350 
are_similar(const DialogFilter & lhs,const DialogFilter & rhs)351 bool DialogFilter::are_similar(const DialogFilter &lhs, const DialogFilter &rhs) {
352   if (lhs.title == rhs.title) {
353     return true;
354   }
355   if (!are_flags_equal(lhs, rhs)) {
356     return false;
357   }
358 
359   vector<InputDialogId> empty_input_dialog_ids;
360   if (InputDialogId::are_equivalent(lhs.excluded_dialog_ids, empty_input_dialog_ids) !=
361       InputDialogId::are_equivalent(rhs.excluded_dialog_ids, empty_input_dialog_ids)) {
362     return false;
363   }
364   if ((InputDialogId::are_equivalent(lhs.pinned_dialog_ids, empty_input_dialog_ids) &&
365        InputDialogId::are_equivalent(lhs.included_dialog_ids, empty_input_dialog_ids)) !=
366       (InputDialogId::are_equivalent(rhs.pinned_dialog_ids, empty_input_dialog_ids) &&
367        InputDialogId::are_equivalent(rhs.included_dialog_ids, empty_input_dialog_ids))) {
368     return false;
369   }
370 
371   return true;
372 }
373 
are_equivalent(const DialogFilter & lhs,const DialogFilter & rhs)374 bool DialogFilter::are_equivalent(const DialogFilter &lhs, const DialogFilter &rhs) {
375   return lhs.title == rhs.title && lhs.emoji == rhs.emoji &&
376          InputDialogId::are_equivalent(lhs.pinned_dialog_ids, rhs.pinned_dialog_ids) &&
377          InputDialogId::are_equivalent(lhs.included_dialog_ids, rhs.included_dialog_ids) &&
378          InputDialogId::are_equivalent(lhs.excluded_dialog_ids, rhs.excluded_dialog_ids) && are_flags_equal(lhs, rhs);
379 }
380 
operator <<(StringBuilder & string_builder,const DialogFilter & filter)381 StringBuilder &operator<<(StringBuilder &string_builder, const DialogFilter &filter) {
382   return string_builder << filter.dialog_filter_id << " (pinned " << filter.pinned_dialog_ids << ", included "
383                         << filter.included_dialog_ids << ", excluded " << filter.excluded_dialog_ids << ", "
384                         << filter.exclude_muted << ' ' << filter.exclude_read << ' ' << filter.exclude_archived << '/'
385                         << filter.include_contacts << ' ' << filter.include_non_contacts << ' ' << filter.include_bots
386                         << ' ' << filter.include_groups << ' ' << filter.include_channels << ')';
387 }
388 
init_icon_names()389 void DialogFilter::init_icon_names() {
390   static bool is_inited = [&] {
391     vector<string> emojis{"\xF0\x9F\x92\xAC",         "\xE2\x9C\x85",     "\xF0\x9F\x94\x94",
392                           "\xF0\x9F\xA4\x96",         "\xF0\x9F\x93\xA2", "\xF0\x9F\x91\xA5",
393                           "\xF0\x9F\x91\xA4",         "\xF0\x9F\x93\x81", "\xF0\x9F\x93\x8B",
394                           "\xF0\x9F\x90\xB1",         "\xF0\x9F\x91\x91", "\xE2\xAD\x90\xEF\xB8\x8F",
395                           "\xF0\x9F\x8C\xB9",         "\xF0\x9F\x8E\xAE", "\xF0\x9F\x8F\xA0",
396                           "\xE2\x9D\xA4\xEF\xB8\x8F", "\xF0\x9F\x8E\xAD", "\xF0\x9F\x8D\xB8",
397                           "\xE2\x9A\xBD\xEF\xB8\x8F", "\xF0\x9F\x8E\x93", "\xF0\x9F\x93\x88",
398                           "\xE2\x9C\x88\xEF\xB8\x8F", "\xF0\x9F\x92\xBC"};
399     vector<string> icon_names{"All",   "Unread", "Unmuted", "Bots",     "Channels", "Groups", "Private", "Custom",
400                               "Setup", "Cat",    "Crown",   "Favorite", "Flower",   "Game",   "Home",    "Love",
401                               "Mask",  "Party",  "Sport",   "Study",    "Trade",    "Travel", "Work"};
402     CHECK(emojis.size() == icon_names.size());
403     for (size_t i = 0; i < emojis.size(); i++) {
404       remove_emoji_modifiers_in_place(emojis[i]);
405       emoji_to_icon_name_[emojis[i]] = icon_names[i];
406       icon_name_to_emoji_[icon_names[i]] = emojis[i];
407     }
408     return true;
409   }();
410   CHECK(is_inited);
411 }
412 
are_flags_equal(const DialogFilter & lhs,const DialogFilter & rhs)413 bool DialogFilter::are_flags_equal(const DialogFilter &lhs, const DialogFilter &rhs) {
414   return lhs.exclude_muted == rhs.exclude_muted && lhs.exclude_read == rhs.exclude_read &&
415          lhs.exclude_archived == rhs.exclude_archived && lhs.include_contacts == rhs.include_contacts &&
416          lhs.include_non_contacts == rhs.include_non_contacts && lhs.include_bots == rhs.include_bots &&
417          lhs.include_groups == rhs.include_groups && lhs.include_channels == rhs.include_channels;
418 }
419 
420 std::unordered_map<string, string> DialogFilter::emoji_to_icon_name_;
421 std::unordered_map<string, string> DialogFilter::icon_name_to_emoji_;
422 
423 }  // namespace td
424