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