1/*  This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird.
2 *  Copyright (C) 2013 Timm Bäder (Corebird)
3 *
4 *  Cawbird is free software: you can redistribute it and/or modify
5 *  it under the terms of the GNU General Public License as published by
6 *  the Free Software Foundation, either version 3 of the License, or
7 *  (at your option) any later version.
8 *
9 *  Cawbird is distributed in the hope that it will be useful,
10 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 *  GNU General Public License for more details.
13 *
14 *  You should have received a copy of the GNU General Public License
15 *  along with cawbird.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18const uint FRIENDSHIP_FOLLOWED_BY   = 1 << 0;
19const uint FRIENDSHIP_FOLLOWING     = 1 << 1;
20const uint FRIENDSHIP_WANT_RETWEETS = 1 << 2;
21const uint FRIENDSHIP_BLOCKING      = 1 << 3;
22const uint FRIENDSHIP_MUTING        = 1 << 4;
23const uint FRIENDSHIP_CAN_DM        = 1 << 5;
24
25struct JsonCursor {
26  int64 next_cursor;
27  bool full;
28  Json.Node? json_object;
29}
30
31
32namespace UserUtils {
33  async uint load_friendship (Account account,
34                              int64   user_id,
35                              string  screen_name)
36  {
37    var call = account.proxy.new_call ();
38    call.set_function ("1.1/friendships/show.json");
39    call.set_method ("GET");
40    call.add_param ("source_id", account.id.to_string ());
41
42    if (user_id != 0)
43      call.add_param ("target_id", user_id.to_string ());
44    else
45      call.add_param ("target_screen_name", screen_name);
46
47
48    Json.Node? root = null;
49    try {
50      root = yield Cb.Utils.load_threaded_async (call, null);
51    } catch (GLib.Error e) {
52      warning (e.message);
53      return 0;
54    }
55
56    var relationship = root.get_object ().get_object_member ("relationship");
57    var target = relationship.get_object_member ("target");
58    var source = relationship.get_object_member ("source");
59
60    uint friendship = 0;
61
62    if (target.get_boolean_member ("following"))
63      friendship |= FRIENDSHIP_FOLLOWED_BY;
64
65    if (target.get_boolean_member ("followed_by"))
66      friendship |= FRIENDSHIP_FOLLOWING;
67
68    if (source.get_boolean_member ("want_retweets"))
69      friendship |= FRIENDSHIP_WANT_RETWEETS;
70
71    if (source.get_boolean_member ("blocking"))
72      friendship |= FRIENDSHIP_BLOCKING;
73
74    if (source.get_boolean_member ("muting"))
75      friendship |= FRIENDSHIP_MUTING;
76
77    if (source.get_boolean_member ("can_dm"))
78      friendship |= FRIENDSHIP_CAN_DM;
79
80    return friendship;
81  }
82
83  async JsonCursor? load_followers (Account account,
84                                int64   user_id,
85                                JsonCursor? old_cursor)
86  {
87    const int requested = 25;
88    var call = account.proxy.new_call ();
89    call.set_function ("1.1/followers/list.json");
90    call.set_method ("GET");
91    call.add_param ("user_id", user_id.to_string ());
92    call.add_param ("count", requested.to_string ());
93    call.add_param ("skip_status", "true");
94    call.add_param ("include_user_entities", "false");
95
96    if (old_cursor != null)
97      call.add_param ("cursor", old_cursor.next_cursor.to_string ());
98
99    Json.Node? root = null;
100    try {
101      root = yield Cb.Utils.load_threaded_async (call, null);
102    } catch (GLib.Error e) {
103      warning (e.message);
104      return null;
105    }
106
107    var root_obj = root.get_object ();
108
109    var user_array = root_obj.get_array_member ("users");
110
111    JsonCursor cursor = JsonCursor ();
112    cursor.next_cursor = root_obj.get_int_member ("next_cursor");
113    cursor.full = (user_array.get_length () < requested);
114    cursor.json_object = root_obj.get_member ("users");
115
116    return cursor;
117  }
118
119  async JsonCursor? load_following (Account account,
120                                int64   user_id,
121                                JsonCursor? old_cursor)
122  {
123    const int requested = 25;
124    var call = account.proxy.new_call ();
125    call.set_function ("1.1/friends/list.json");
126    call.set_method ("GET");
127    call.add_param ("user_id", user_id.to_string ());
128    call.add_param ("count", requested.to_string ());
129    call.add_param ("skip_status", "true");
130    call.add_param ("include_user_entities", "false");
131
132    if (old_cursor != null)
133      call.add_param ("cursor", old_cursor.next_cursor.to_string ());
134
135    Json.Node? root = null;
136    try {
137      root = yield Cb.Utils.load_threaded_async (call, null);
138    } catch (GLib.Error e) {
139      warning (e.message);
140      return null;
141    }
142
143    var root_obj = root.get_object ();
144
145    var user_array = root_obj.get_array_member ("users");
146
147    JsonCursor cursor = JsonCursor ();
148    cursor.next_cursor = root_obj.get_int_member ("next_cursor");
149    cursor.full = (user_array.get_length () < requested);
150    cursor.json_object = root_obj.get_member ("users");
151
152    return cursor;
153  }
154
155  async void mute_user (Account account,
156                        int64   to_block,
157                        bool    setting) throws GLib.Error {
158    var call = account.proxy.new_call ();
159    call.set_method ("POST");
160    if (setting) {
161      call.set_function ("1.1/mutes/users/create.json");
162      call.add_param ("include_entities", "false");
163      call.add_param ("skip_status", "true");
164    } else {
165      call.set_function ("1.1/mutes/users/destroy.json");
166    }
167
168    call.add_param ("user_id", to_block.to_string ());
169    GLib.Error? err = null;
170
171    call.invoke_async.begin (null, (obj, res) => {
172      try {
173        call.invoke_async.end (res);
174        if (setting) {
175          TweetUtils.inject_user_mute(call.get_payload(), account);
176        }
177        else {
178          TweetUtils.inject_user_unmute(call.get_payload(), account);
179        }
180      } catch (GLib.Error e) {
181        var tmp_err = TweetUtils.failed_request_to_error (call, e);
182
183        // Muting muted users fails silently, so errors are important, but code 272 means
184        // we unmuted someone who was already unmuted
185        if (setting || tmp_err.domain != TweetUtils.get_error_domain() || tmp_err.code != 272) {
186          err = tmp_err;
187        }
188      }
189      mute_user.callback();
190    });
191    yield;
192    if (err != null) {
193      throw err;
194    }
195  }
196
197  async void block_user (Account account,
198                        int64   to_block,
199                        bool    setting) throws GLib.Error {
200    var call = account.proxy.new_call ();
201    call.set_method ("POST");
202    if (setting) {
203      call.set_function ("1.1/blocks/create.json");
204    } else {
205      call.set_function ("1.1/blocks/destroy.json");
206    }
207
208    call.add_param ("user_id", to_block.to_string ());
209    call.add_param ("include_entities", "false");
210    call.add_param ("skip_status", "true");
211    GLib.Error? err = null;
212
213    call.invoke_async.begin (null, (obj, res) => {
214      try {
215        call.invoke_async.end (res);
216        if (setting) {
217          TweetUtils.inject_user_block(call.get_payload(), account);
218        }
219        else {
220          TweetUtils.inject_user_unblock(call.get_payload(), account);
221        }
222      } catch (GLib.Error e) {
223        var tmp_err = TweetUtils.failed_request_to_error (call, e);
224        debug("Error: %s", tmp_err.message);
225
226        // Muting muted users fails silently, so errors are important, but code 272 means
227        // we unmuted someone who was already unmuted
228        if (setting || tmp_err.domain != TweetUtils.get_error_domain() || tmp_err.code != 272) {
229          err = tmp_err;
230        }
231      }
232      block_user.callback();
233    });
234    yield;
235    if (err != null) {
236      throw err;
237    }
238  }
239
240  async Json.Array load_user_timeline_by_id(Account account, int64 user_id, int tweet_count, int64 since_id = -1, int64 max_id = -1) throws GLib.Error {
241    return yield load_user_timeline(account, "user_id", user_id.to_string(), tweet_count, since_id, max_id);
242  }
243
244  async Json.Array load_user_timeline_by_screen_name(Account account, string screen_name, int tweet_count, int64 since_id = -1, int64 max_id = -1) throws GLib.Error {
245    return yield load_user_timeline(account, "screen_name", screen_name, tweet_count, since_id, max_id);
246  }
247
248  private async Json.Array load_user_timeline(Account account, string field, string value, int tweet_count, int64 since_id, int64 max_id) throws GLib.Error {
249    var call = account.proxy.new_call ();
250    call.set_function ("1.1/statuses/user_timeline.json");
251    call.set_method ("GET");
252    call.add_param (field, value);
253    if (since_id > 0) {
254      call.add_param ("since_id", since_id.to_string());
255    }
256    if (max_id > 0) {
257      call.add_param ("max_id", max_id.to_string());
258    }
259    call.add_param ("count", tweet_count.to_string());
260    call.add_param ("contributor_details", "true");
261    call.add_param ("tweet_mode", "extended");
262    call.add_param ("include_my_retweet", "true");
263    call.add_param ("include_ext_alt_text", "true");
264
265    Json.Node? root = null;
266    Json.Array? array = null;
267    try {
268      root = yield Cb.Utils.load_threaded_async (call, null);
269      if (root != null) {
270        array = root.get_array();
271      }
272    } catch (GLib.Error e) {
273      if (e.domain == Rest.ProxyError.quark() && e.code == new Rest.ProxyError.SSL ("workaround").code) {
274        debug ("Reloading user timeline on SSL failure");
275        array = yield load_user_timeline(account, field, value, tweet_count, since_id, max_id);
276      }
277      else {
278        throw e;
279      }
280    }
281
282    return array ?? new Json.Array();
283  }
284}