1 /* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird.
2 * Copyright (C) 2016 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
18 #include "CbTweet.h"
19 #include "CbTextTransform.h"
20 #include <string.h>
21
22
23 /* TODO: We might want to put this into a utils.c later */
24 static gboolean
usable_json_value(JsonObject * object,const char * name)25 usable_json_value (JsonObject *object, const char *name)
26 {
27 if (!json_object_has_member (object, name))
28 return FALSE;
29
30 return !json_object_get_null_member (object, name);
31 }
32
33 G_DEFINE_TYPE (CbTweet, cb_tweet, G_TYPE_OBJECT);
34
35 enum {
36 STATE_CHANGED,
37 QUOTE_STATE_CHANGED,
38 LAST_SIGNAL
39 };
40
41 static guint tweet_signals[LAST_SIGNAL] = { 0 };
42
43
44 gboolean
cb_tweet_is_hidden(CbTweet * tweet)45 cb_tweet_is_hidden (CbTweet *tweet)
46 {
47 g_return_val_if_fail (CB_IS_TWEET (tweet), TRUE);
48
49 return (tweet->state & (CB_TWEET_STATE_HIDDEN_FORCE |
50 CB_TWEET_STATE_HIDDEN_UNFOLLOWED |
51 CB_TWEET_STATE_HIDDEN_FILTERED |
52 CB_TWEET_STATE_HIDDEN_RTS_DISABLED |
53 CB_TWEET_STATE_HIDDEN_RT_BY_USER |
54 CB_TWEET_STATE_HIDDEN_RT_BY_FOLLOWEE |
55 CB_TWEET_STATE_HIDDEN_AUTHOR_BLOCKED |
56 CB_TWEET_STATE_HIDDEN_RETWEETER_BLOCKED |
57 CB_TWEET_STATE_HIDDEN_AUTHOR_MUTED |
58 CB_TWEET_STATE_HIDDEN_RETWEETER_MUTED)) > 0;
59 }
60
61 gboolean
cb_tweet_has_inline_media(CbTweet * tweet)62 cb_tweet_has_inline_media (CbTweet *tweet)
63 {
64 g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE);
65
66 if (tweet->retweeted_tweet != NULL)
67 return tweet->retweeted_tweet->n_medias > 0;
68
69 return tweet->source_tweet.n_medias > 0;
70 }
71
72 gboolean
cb_tweet_has_quoted_inline_media(CbTweet * tweet)73 cb_tweet_has_quoted_inline_media (CbTweet *tweet)
74 {
75 g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE);
76
77 return tweet->quoted_tweet != NULL && tweet->quoted_tweet->n_medias > 0;
78 }
79
80 /* TODO: Replace these 3 functinos with one that returns a pointer to a CbUserIdentity? */
81 gint64
cb_tweet_get_user_id(CbTweet * tweet)82 cb_tweet_get_user_id (CbTweet *tweet)
83 {
84 if (tweet->retweeted_tweet != NULL)
85 return tweet->retweeted_tweet->author.id;
86
87 return tweet->source_tweet.author.id;
88 }
89
90 const char *
cb_tweet_get_screen_name(CbTweet * tweet)91 cb_tweet_get_screen_name (CbTweet *tweet)
92 {
93 if (tweet->retweeted_tweet != NULL)
94 return tweet->retweeted_tweet->author.screen_name;
95
96 return tweet->source_tweet.author.screen_name;
97 }
98
99 const char *
cb_tweet_get_user_name(CbTweet * tweet)100 cb_tweet_get_user_name (CbTweet *tweet)
101 {
102 if (tweet->retweeted_tweet != NULL)
103 return tweet->retweeted_tweet->author.user_name;
104
105 return tweet->source_tweet.author.user_name;
106 }
107
108 const char *
cb_tweet_get_language(CbTweet * tweet)109 cb_tweet_get_language (CbTweet *tweet) {
110 if (tweet->retweeted_tweet != NULL)
111 return tweet->retweeted_tweet->language;
112
113 return tweet->source_tweet.language;
114 }
115
116 CbMedia **
cb_tweet_get_medias(CbTweet * tweet,int * n_medias)117 cb_tweet_get_medias (CbTweet *tweet,
118 int *n_medias)
119 {
120 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
121 g_return_val_if_fail (n_medias != NULL, NULL);
122
123 if (tweet->retweeted_tweet != NULL)
124 {
125 *n_medias = tweet->retweeted_tweet->n_medias;
126 return tweet->retweeted_tweet->medias;
127 }
128 else
129 {
130 *n_medias = tweet->source_tweet.n_medias;
131 return tweet->source_tweet.medias;
132 }
133 }
134
135 CbMedia **
cb_tweet_get_quoted_medias(CbTweet * tweet,int * n_medias)136 cb_tweet_get_quoted_medias (CbTweet *tweet,
137 int *n_medias)
138 {
139 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
140 g_return_val_if_fail (tweet->quoted_tweet != NULL, NULL);
141
142 *n_medias = tweet->quoted_tweet->n_medias;
143 return tweet->quoted_tweet->medias;
144 }
145
146 char **
cb_tweet_get_mentions(CbTweet * tweet,int * n_mentions)147 cb_tweet_get_mentions (CbTweet *tweet,
148 int *n_mentions)
149 {
150 CbTextEntity *entities;
151 gsize n_entities;
152 gsize i, x;
153 char **mentions;
154
155 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
156 g_return_val_if_fail (n_mentions != NULL, NULL);
157
158 if (tweet->retweeted_tweet != NULL)
159 {
160 entities = tweet->retweeted_tweet->entities;
161 n_entities = tweet->retweeted_tweet->n_entities;
162 }
163 else
164 {
165 entities = tweet->source_tweet.entities;
166 n_entities = tweet->source_tweet.n_entities;
167 }
168
169 *n_mentions = 0;
170 for (i = 0; i < n_entities; i ++)
171 if (entities[i].display_text[0] == '@')
172 (*n_mentions) ++;
173
174 if (*n_mentions == 0)
175 return NULL;
176
177
178 mentions = g_malloc (sizeof(char*) * (*n_mentions));
179
180 x = 0;
181 for (i = 0; i < n_entities; i ++)
182 if (entities[i].display_text[0] == '@')
183 {
184 mentions[x] = g_strdup (&entities[i].display_text[1]);
185 x ++;
186 }
187
188 return mentions;
189 }
190
191 void
cb_tweet_load_from_json(CbTweet * tweet,JsonNode * status_node,gint64 account_id,GDateTime * now)192 cb_tweet_load_from_json (CbTweet *tweet,
193 JsonNode *status_node,
194 gint64 account_id,
195 GDateTime *now)
196 {
197 JsonObject *status;
198 JsonObject *user;
199
200 g_return_if_fail (CB_IS_TWEET (tweet));
201 g_return_if_fail (status_node != NULL);
202 g_return_if_fail (now != NULL);
203
204 status = json_node_get_object (status_node);
205 user = json_object_get_object_member (status, "user");
206
207 tweet->id = json_object_get_int_member (status, "id");
208 tweet->retweet_count = (guint) json_object_get_int_member (status, "retweet_count");
209 tweet->favorite_count = (guint) json_object_get_int_member (status, "favorite_count");
210
211
212 cb_mini_tweet_parse (&tweet->source_tweet, status);
213
214 if (json_object_has_member (status, "retweeted_status"))
215 {
216 JsonObject *rt = json_object_get_object_member (status, "retweeted_status");
217 JsonObject *rt_user = json_object_get_object_member (rt, "user");
218
219 tweet->retweeted_tweet = g_malloc (sizeof(CbMiniTweet));
220 cb_mini_tweet_init (tweet->retweeted_tweet);
221 cb_mini_tweet_parse (tweet->retweeted_tweet, rt);
222 cb_mini_tweet_parse_entities (tweet->retweeted_tweet, rt);
223
224 tweet->avatar_url = g_strdup (json_object_get_string_member (rt_user, "profile_image_url_https"));
225 if (json_object_get_boolean_member (rt_user, "protected"))
226 tweet->state |= CB_TWEET_STATE_PROTECTED;
227
228 if (json_object_get_boolean_member (rt_user, "verified"))
229 tweet->state |= CB_TWEET_STATE_VERIFIED;
230
231 if (usable_json_value (rt, "possibly_sensitive") &&
232 json_object_get_boolean_member (rt, "possibly_sensitive"))
233 tweet->state |= CB_TWEET_STATE_NSFW;
234 }
235 else
236 {
237 cb_mini_tweet_parse_entities (&tweet->source_tweet, status);
238 tweet->avatar_url = g_strdup (json_object_get_string_member (user, "profile_image_url_https"));
239
240 if (json_object_get_boolean_member (user, "protected"))
241 tweet->state |= CB_TWEET_STATE_PROTECTED;
242
243 if (json_object_get_boolean_member (user, "verified"))
244 tweet->state |= CB_TWEET_STATE_VERIFIED;
245
246 if (usable_json_value (status, "possibly_sensitive") &&
247 json_object_get_boolean_member (status, "possibly_sensitive"))
248 tweet->state |= CB_TWEET_STATE_NSFW;
249 }
250
251 if (json_object_has_member (status, "quoted_status"))
252 {
253 JsonObject *quote = json_object_get_object_member (status, "quoted_status");
254 tweet->quoted_tweet = g_malloc (sizeof (CbMiniTweet));
255 cb_mini_tweet_init (tweet->quoted_tweet);
256 cb_mini_tweet_parse (tweet->quoted_tweet, quote);
257 cb_mini_tweet_parse_entities (tweet->quoted_tweet, quote);
258
259 if (usable_json_value (quote, "possibly_sensitive") &&
260 json_object_get_boolean_member (quote, "possibly_sensitive"))
261 tweet->quote_state |= CB_TWEET_STATE_NSFW;
262 }
263 else if (tweet->retweeted_tweet != NULL &&
264 json_object_has_member (json_object_get_object_member (status, "retweeted_status"), "quoted_status")) {
265 JsonObject *quote = json_object_get_object_member (json_object_get_object_member (status, "retweeted_status"),
266 "quoted_status");
267
268 tweet->quoted_tweet = g_malloc (sizeof (CbMiniTweet));
269 cb_mini_tweet_init (tweet->quoted_tweet);
270 cb_mini_tweet_parse (tweet->quoted_tweet, quote);
271 cb_mini_tweet_parse_entities (tweet->quoted_tweet, quote);
272
273 if (usable_json_value (quote, "possibly_sensitive") &&
274 json_object_get_boolean_member (quote, "possibly_sensitive"))
275 tweet->quote_state |= CB_TWEET_STATE_NSFW;
276 }
277
278 if (json_object_get_boolean_member (status, "favorited"))
279 tweet->state |= CB_TWEET_STATE_FAVORITED;
280
281 if (json_object_has_member (status, "current_user_retweet"))
282 {
283 JsonObject *cur_rt = json_object_get_object_member (status, "current_user_retweet");
284 tweet->my_retweet = json_object_get_int_member (cur_rt, "id");
285 tweet->state |= CB_TWEET_STATE_RETWEETED;
286 }
287 else if (json_object_get_boolean_member (status, "retweeted") ||
288 (tweet->retweeted_tweet != NULL && tweet->source_tweet.author.id == account_id))
289 {
290 /* The 'retweeted' flag is not reliable so we additionally check if the tweet is authored
291 by the authenticating user */
292 tweet->my_retweet = tweet->id;
293 tweet->state |= CB_TWEET_STATE_RETWEETED;
294 }
295
296
297 #ifdef DEBUG
298 {
299 JsonGenerator *generator = json_generator_new ();
300 json_generator_set_root (generator, status_node);
301 json_generator_set_pretty (generator, TRUE);
302 tweet->json_data = json_generator_to_data (generator, NULL);
303
304 g_object_unref (generator);
305 }
306 #endif
307 }
308
309 gboolean
cb_tweet_is_reply(CbTweet * tweet)310 cb_tweet_is_reply (CbTweet *tweet)
311 {
312 return (tweet->retweeted_tweet != NULL && tweet->retweeted_tweet->reply_id != 0) || (tweet->retweeted_tweet == NULL && tweet->source_tweet.reply_id != 0);
313 }
314
315 gboolean
cb_tweet_is_flag_set(CbTweet * tweet,guint flag)316 cb_tweet_is_flag_set (CbTweet *tweet, guint flag)
317 {
318 return (tweet->state & flag) > 0;
319 }
320
321 void
cb_tweet_set_flag(CbTweet * tweet,guint flag)322 cb_tweet_set_flag (CbTweet *tweet, guint flag)
323 {
324 guint prev_state;
325
326 g_return_if_fail (CB_IS_TWEET (tweet));
327
328 prev_state = tweet->state;
329
330 tweet->state |= flag;
331
332 if (tweet->state != prev_state)
333 g_signal_emit (tweet, tweet_signals[STATE_CHANGED], 0);
334 }
335
336 void
cb_tweet_unset_flag(CbTweet * tweet,guint flag)337 cb_tweet_unset_flag (CbTweet *tweet, guint flag)
338 {
339 guint prev_state;
340
341 g_return_if_fail (CB_IS_TWEET (tweet));
342
343 prev_state = tweet->state;
344
345 tweet->state &= ~flag;
346
347 if (tweet->state != prev_state)
348 g_signal_emit (tweet, tweet_signals[STATE_CHANGED], 0);
349 }
350
351 gboolean
cb_tweet_is_quoted_flag_set(CbTweet * tweet,guint flag)352 cb_tweet_is_quoted_flag_set (CbTweet *tweet, guint flag)
353 {
354 return (tweet->quote_state & flag) > 0;
355 }
356
357 void
cb_tweet_set_quoted_flag(CbTweet * tweet,guint flag)358 cb_tweet_set_quoted_flag (CbTweet *tweet, guint flag)
359 {
360 guint prev_state;
361
362 g_return_if_fail (CB_IS_TWEET (tweet));
363
364 prev_state = tweet->quote_state;
365
366 tweet->quote_state |= flag;
367
368 if (tweet->quote_state != prev_state)
369 g_signal_emit (tweet, tweet_signals[QUOTE_STATE_CHANGED], 0);
370 }
371
372 void
cb_tweet_unset_quoted_flag(CbTweet * tweet,guint flag)373 cb_tweet_unset_quoted_flag (CbTweet *tweet, guint flag)
374 {
375 guint prev_state;
376
377 g_return_if_fail (CB_IS_TWEET (tweet));
378
379 prev_state = tweet->quote_state;
380
381 tweet->quote_state &= ~flag;
382
383 if (tweet->quote_state != prev_state)
384 g_signal_emit (tweet, tweet_signals[QUOTE_STATE_CHANGED], 0);
385 }
386
387 char *
cb_tweet_get_formatted_text(CbTweet * tweet)388 cb_tweet_get_formatted_text (CbTweet *tweet)
389 {
390 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
391
392 if (tweet->retweeted_tweet != NULL)
393 return cb_text_transform_tweet (tweet->retweeted_tweet, 0, 0);
394 else
395 return cb_text_transform_tweet (&tweet->source_tweet, 0, 0);
396 }
397
398 char *
cb_tweet_get_trimmed_text(CbTweet * tweet,guint transform_flags)399 cb_tweet_get_trimmed_text (CbTweet *tweet, guint transform_flags)
400 {
401 gint64 quote_id;
402
403 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
404
405 quote_id = tweet->quoted_tweet != NULL ? tweet->quoted_tweet->id : 0;
406
407 if (tweet->retweeted_tweet != NULL)
408 return cb_text_transform_tweet (tweet->retweeted_tweet, transform_flags, quote_id);
409 else
410 return cb_text_transform_tweet (&tweet->source_tweet, transform_flags, quote_id);
411 }
412
413 char *
cb_tweet_get_real_text(CbTweet * tweet)414 cb_tweet_get_real_text (CbTweet *tweet)
415 {
416 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
417
418 if (tweet->retweeted_tweet != NULL)
419 return cb_text_transform_tweet (tweet->retweeted_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0);
420 else
421 return cb_text_transform_tweet (&tweet->source_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0);
422 }
423
424 char *
cb_tweet_get_filter_text(CbTweet * tweet)425 cb_tweet_get_filter_text (CbTweet *tweet)
426 {
427 GString *string;
428 char *text;
429
430 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
431
432 string = g_string_new (0);
433
434 if (tweet->retweeted_tweet != NULL)
435 text = cb_text_transform_tweet (tweet->retweeted_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0);
436 else
437 text = cb_text_transform_tweet (&tweet->source_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS ,0);
438
439 g_string_append (string, text);
440 g_free (text);
441
442 int n_mentions;
443 char ** mentions = cb_tweet_get_mentions(tweet, &n_mentions);
444
445 for (int i = 0; i < n_mentions; i++) {
446 g_string_append (string, " @");
447 g_string_append (string, mentions[i]);
448 }
449
450 g_free (mentions);
451
452 g_string_append (string, " [");
453
454 if (tweet->retweeted_tweet != NULL)
455 g_string_append (string, "rt");
456
457 if (tweet->quoted_tweet != NULL)
458 g_string_append (string, ",quote");
459
460 g_string_append_c (string, ']');
461
462 return g_string_free (string, FALSE);
463 }
464
465 gboolean
cb_tweet_get_seen(CbTweet * tweet)466 cb_tweet_get_seen (CbTweet *tweet)
467 {
468 g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE);
469
470 return tweet->seen;
471 }
472
473 void
cb_tweet_set_seen(CbTweet * tweet,gboolean value)474 cb_tweet_set_seen (CbTweet *tweet, gboolean value)
475 {
476 g_return_if_fail (CB_IS_TWEET (tweet));
477
478 value = !!value;
479
480 if (value && !tweet->seen && tweet->notification_id != NULL)
481 {
482 GApplication *app = g_application_get_default ();
483
484 g_application_withdraw_notification (app, tweet->notification_id);
485 tweet->notification_id = NULL;
486 }
487
488 tweet->seen = value;
489 }
490
491 CbUserIdentity *
cb_tweet_get_reply_users(CbTweet * tweet,guint * n_reply_users)492 cb_tweet_get_reply_users (CbTweet *tweet,
493 guint *n_reply_users)
494 {
495 g_return_val_if_fail (CB_IS_TWEET (tweet), NULL);
496 g_return_val_if_fail (n_reply_users != NULL, NULL);
497
498 if (tweet->retweeted_tweet != NULL)
499 {
500 *n_reply_users = tweet->retweeted_tweet->n_reply_users;
501 return tweet->retweeted_tweet->reply_users;
502 }
503 else
504 {
505 *n_reply_users = tweet->source_tweet.n_reply_users;
506 return tweet->source_tweet.reply_users;
507 }
508
509 *n_reply_users = 0;
510 return NULL; /* shrug */
511 }
512
513 CbTweet *
cb_tweet_new(void)514 cb_tweet_new (void)
515 {
516 return (CbTweet *)g_object_new (CB_TYPE_TWEET, NULL);
517 }
518
519 static void
cb_tweet_finalize(GObject * object)520 cb_tweet_finalize (GObject *object)
521 {
522 CbTweet *tweet = (CbTweet *)object;
523
524 g_free (tweet->avatar_url);
525 g_free (tweet->notification_id);
526 cb_mini_tweet_free (&tweet->source_tweet);
527
528 if (tweet->retweeted_tweet != NULL)
529 {
530 cb_mini_tweet_free (tweet->retweeted_tweet);
531 g_free (tweet->retweeted_tweet);
532 }
533
534 if (tweet->quoted_tweet != NULL)
535 {
536 cb_mini_tweet_free (tweet->quoted_tweet);
537 g_free (tweet->quoted_tweet);
538 }
539
540 #ifdef DEBUG
541 g_free (tweet->json_data);
542 #endif
543
544 G_OBJECT_CLASS (cb_tweet_parent_class)->finalize (object);
545 }
546
547 static void
cb_tweet_init(CbTweet * tweet)548 cb_tweet_init (CbTweet *tweet)
549 {
550 tweet->state = 0;
551 tweet->quoted_tweet = NULL;
552 tweet->retweeted_tweet = NULL;
553 tweet->notification_id = NULL;
554 tweet->seen = TRUE;
555 }
556
557 static void
cb_tweet_class_init(CbTweetClass * class)558 cb_tweet_class_init (CbTweetClass *class)
559 {
560 GObjectClass *gobject_class = (GObjectClass *)class;
561
562 gobject_class->finalize = cb_tweet_finalize;
563
564 tweet_signals[STATE_CHANGED] = g_signal_new ("state-changed",
565 G_OBJECT_CLASS_TYPE (gobject_class),
566 G_SIGNAL_RUN_FIRST,
567 0,
568 NULL, NULL,
569 NULL, G_TYPE_NONE, 0);
570 }
571