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}