source: protocols/twitter/twitter.c @ e3e2059

Last change on this file since e3e2059 was 8b91a1f, checked in by dequis <dx@…>, at 2015-03-10T07:27:25Z

twitter: Fix twitter_parse_id to accept 00 as a valid tweet

Also fix the endptr condition which was backwards and resulted in every
tweet id getting rejected, but you didn't see that one, this commit
really is about the tweet id 00 which is the most important tweet id.

  • Property mode set to 100644
File size: 28.3 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
7*  Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net>              *
8*                                                                           *
9*  This library is free software; you can redistribute it and/or            *
10*  modify it under the terms of the GNU Lesser General Public               *
11*  License as published by the Free Software Foundation, version            *
12*  2.1.                                                                     *
13*                                                                           *
14*  This library is distributed in the hope that it will be useful,          *
15*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
16*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        *
17*  Lesser General Public License for more details.                          *
18*                                                                           *
19*  You should have received a copy of the GNU Lesser General Public License *
20*  along with this library; if not, write to the Free Software Foundation,  *
21*  Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA           *
22*                                                                           *
23****************************************************************************/
24
25#include "nogaim.h"
26#include "oauth.h"
27#include "twitter.h"
28#include "twitter_http.h"
29#include "twitter_lib.h"
30#include "url.h"
31
32GSList *twitter_connections = NULL;
33
34static int twitter_filter_cmp(struct twitter_filter *tf1,
35                              struct twitter_filter *tf2)
36{
37        int i1 = 0;
38        int i2 = 0;
39        int i;
40
41        static const twitter_filter_type_t types[] = {
42                /* Order of the types */
43                TWITTER_FILTER_TYPE_FOLLOW,
44                TWITTER_FILTER_TYPE_TRACK
45        };
46
47        for (i = 0; i < G_N_ELEMENTS(types); i++) {
48                if (types[i] == tf1->type) {
49                        i1 = i + 1;
50                        break;
51                }
52        }
53
54        for (i = 0; i < G_N_ELEMENTS(types); i++) {
55                if (types[i] == tf2->type) {
56                        i2 = i + 1;
57                        break;
58                }
59        }
60
61        if (i1 != i2) {
62                /* With different types, return their difference */
63                return i1 - i2;
64        }
65
66        /* With the same type, return the text comparison */
67        return g_strcasecmp(tf1->text, tf2->text);
68}
69
70static gboolean twitter_filter_update(gpointer data, gint fd,
71                                      b_input_condition cond)
72{
73        struct im_connection *ic = data;
74        struct twitter_data *td = ic->proto_data;
75
76        if (td->filters) {
77                twitter_open_filter_stream(ic);
78        } else if (td->filter_stream) {
79                http_close(td->filter_stream);
80                td->filter_stream = NULL;
81        }
82
83        td->filter_update_id = 0;
84        return FALSE;
85}
86
87static struct twitter_filter *twitter_filter_get(struct groupchat *c,
88                                                 twitter_filter_type_t type,
89                                                 const char *text)
90{
91        struct twitter_data *td = c->ic->proto_data;
92        struct twitter_filter *tf = NULL;
93        struct twitter_filter tfc = { type, (char *) text };
94        GSList *l;
95
96        for (l = td->filters; l; l = g_slist_next(l)) {
97                tf = l->data;
98
99                if (twitter_filter_cmp(tf, &tfc) == 0) {
100                        break;
101                }
102
103                tf = NULL;
104        }
105
106        if (!tf) {
107                tf = g_new0(struct twitter_filter, 1);
108                tf->type = type;
109                tf->text = g_strdup(text);
110                td->filters = g_slist_prepend(td->filters, tf);
111        }
112
113        if (!g_slist_find(tf->groupchats, c)) {
114                tf->groupchats = g_slist_prepend(tf->groupchats, c);
115        }
116
117        if (td->filter_update_id > 0) {
118                b_event_remove(td->filter_update_id);
119        }
120
121        /* Wait for other possible filter changes to avoid request spam */
122        td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
123                                             twitter_filter_update, c->ic);
124        return tf;
125}
126
127static void twitter_filter_free(struct twitter_filter *tf)
128{
129        g_slist_free(tf->groupchats);
130        g_free(tf->text);
131        g_free(tf);
132}
133
134static void twitter_filter_remove(struct groupchat *c)
135{
136        struct twitter_data *td = c->ic->proto_data;
137        struct twitter_filter *tf;
138        GSList *l = td->filters;
139        GSList *p;
140
141        while (l != NULL) {
142                tf = l->data;
143                tf->groupchats = g_slist_remove(tf->groupchats, c);
144
145                p = l;
146                l = g_slist_next(l);
147
148                if (!tf->groupchats) {
149                        twitter_filter_free(tf);
150                        td->filters = g_slist_delete_link(td->filters, p);
151                }
152        }
153
154        if (td->filter_update_id > 0) {
155                b_event_remove(td->filter_update_id);
156        }
157
158        /* Wait for other possible filter changes to avoid request spam */
159        td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
160                                             twitter_filter_update, c->ic);
161}
162
163static void twitter_filter_remove_all(struct im_connection *ic)
164{
165        struct twitter_data *td = ic->proto_data;
166        GSList *chats = NULL;
167        struct twitter_filter *tf;
168        GSList *l = td->filters;
169        GSList *p;
170
171        while (l != NULL) {
172                tf = l->data;
173
174                /* Build up a list of groupchats to be freed */
175                for (p = tf->groupchats; p; p = g_slist_next(p)) {
176                        if (!g_slist_find(chats, p->data)) {
177                                chats = g_slist_prepend(chats, p->data);
178                        }
179                }
180
181                p = l;
182                l = g_slist_next(l);
183                twitter_filter_free(p->data);
184                td->filters = g_slist_delete_link(td->filters, p);
185        }
186
187        l = chats;
188
189        while (l != NULL) {
190                p = l;
191                l = g_slist_next(l);
192
193                /* Freed each remaining groupchat */
194                imcb_chat_free(p->data);
195                chats = g_slist_delete_link(chats, p);
196        }
197
198        if (td->filter_stream) {
199                http_close(td->filter_stream);
200                td->filter_stream = NULL;
201        }
202}
203
204static GSList *twitter_filter_parse(struct groupchat *c, const char *text)
205{
206        char **fs = g_strsplit(text, ";", 0);
207        GSList *ret = NULL;
208        struct twitter_filter *tf;
209        char **f;
210        char *v;
211        int i;
212        int t;
213
214        static const twitter_filter_type_t types[] = {
215                TWITTER_FILTER_TYPE_FOLLOW,
216                TWITTER_FILTER_TYPE_TRACK
217        };
218
219        static const char *typestrs[] = {
220                "follow",
221                "track"
222        };
223
224        for (f = fs; *f; f++) {
225                if ((v = strchr(*f, ':')) == NULL) {
226                        continue;
227                }
228
229                *(v++) = 0;
230
231                for (t = -1, i = 0; i < G_N_ELEMENTS(types); i++) {
232                        if (g_strcasecmp(typestrs[i], *f) == 0) {
233                                t = i;
234                                break;
235                        }
236                }
237
238                if (t < 0 || strlen(v) == 0) {
239                        continue;
240                }
241
242                tf = twitter_filter_get(c, types[t], v);
243                ret = g_slist_prepend(ret, tf);
244        }
245
246        g_strfreev(fs);
247        return ret;
248}
249
250/**
251 * Main loop function
252 */
253gboolean twitter_main_loop(gpointer data, gint fd, b_input_condition cond)
254{
255        struct im_connection *ic = data;
256
257        // Check if we are still logged in...
258        if (!g_slist_find(twitter_connections, ic)) {
259                return FALSE;
260        }
261
262        // Do stuff..
263        return twitter_get_timeline(ic, -1) &&
264               ((ic->flags & OPT_LOGGED_IN) == OPT_LOGGED_IN);
265}
266
267static void twitter_main_loop_start(struct im_connection *ic)
268{
269        struct twitter_data *td = ic->proto_data;
270
271        char *last_tweet = set_getstr(&ic->acc->set, "_last_tweet");
272
273        if (last_tweet) {
274                td->timeline_id = g_ascii_strtoull(last_tweet, NULL, 0);
275        }
276
277        /* Create the room now that we "logged in". */
278        if (td->flags & TWITTER_MODE_CHAT) {
279                twitter_groupchat_init(ic);
280        }
281
282        imcb_log(ic, "Getting initial statuses");
283
284        // Run this once. After this queue the main loop function (or open the
285        // stream if available).
286        twitter_main_loop(ic, -1, 0);
287
288        if (set_getbool(&ic->acc->set, "stream")) {
289                /* That fetch was just to get backlog, the stream will give
290                   us the rest. \o/ */
291                twitter_open_stream(ic);
292
293                /* Stream sends keepalives (empty lines) or actual data at
294                   least twice a minute. Disconnect if this stops. */
295                ic->flags |= OPT_PONGS;
296        } else {
297                /* Not using the streaming API, so keep polling the old-
298                   fashioned way. :-( */
299                td->main_loop_id =
300                        b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000,
301                                      twitter_main_loop, ic);
302        }
303}
304
305struct groupchat *twitter_groupchat_init(struct im_connection *ic)
306{
307        char *name_hint;
308        struct groupchat *gc;
309        struct twitter_data *td = ic->proto_data;
310        GSList *l;
311
312        if (td->timeline_gc) {
313                return td->timeline_gc;
314        }
315
316        td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline");
317
318        name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
319        imcb_chat_name_hint(gc, name_hint);
320        g_free(name_hint);
321
322        for (l = ic->bee->users; l; l = l->next) {
323                bee_user_t *bu = l->data;
324                if (bu->ic == ic) {
325                        imcb_chat_add_buddy(gc, bu->handle);
326                }
327        }
328        imcb_chat_add_buddy(gc, ic->acc->user);
329
330        return gc;
331}
332
333static void twitter_oauth_start(struct im_connection *ic);
334
335void twitter_login_finish(struct im_connection *ic)
336{
337        struct twitter_data *td = ic->proto_data;
338
339        td->flags &= ~TWITTER_DOING_TIMELINE;
340
341        if (set_getbool(&ic->acc->set, "oauth") && !td->oauth_info) {
342                twitter_oauth_start(ic);
343        } else if (!(td->flags & TWITTER_MODE_ONE) &&
344                   !(td->flags & TWITTER_HAVE_FRIENDS)) {
345                imcb_log(ic, "Getting contact list");
346                twitter_get_friends_ids(ic, -1);
347        } else {
348                twitter_main_loop_start(ic);
349        }
350}
351
352static const struct oauth_service twitter_oauth = {
353        "https://api.twitter.com/oauth/request_token",
354        "https://api.twitter.com/oauth/access_token",
355        "https://api.twitter.com/oauth/authorize",
356        .consumer_key = "xsDNKJuNZYkZyMcu914uEA",
357        .consumer_secret = "FCxqcr0pXKzsF9ajmP57S3VQ8V6Drk4o2QYtqMcOszo",
358};
359
360static const struct oauth_service identica_oauth = {
361        "https://identi.ca/api/oauth/request_token",
362        "https://identi.ca/api/oauth/access_token",
363        "https://identi.ca/api/oauth/authorize",
364        .consumer_key = "e147ff789fcbd8a5a07963afbb43f9da",
365        .consumer_secret = "c596267f277457ec0ce1ab7bb788d828",
366};
367
368static gboolean twitter_oauth_callback(struct oauth_info *info);
369
370static const struct oauth_service *get_oauth_service(struct im_connection *ic)
371{
372        struct twitter_data *td = ic->proto_data;
373
374        if (strstr(td->url_host, "identi.ca")) {
375                return &identica_oauth;
376        } else {
377                return &twitter_oauth;
378        }
379
380        /* Could add more services, or allow configuring your own base URL +
381           API keys. */
382}
383
384static void twitter_oauth_start(struct im_connection *ic)
385{
386        struct twitter_data *td = ic->proto_data;
387        const char *url = set_getstr(&ic->acc->set, "base_url");
388
389        imcb_log(ic, "Requesting OAuth request token");
390
391        if (!strstr(url, "twitter.com") && !strstr(url, "identi.ca")) {
392                imcb_log(ic, "Warning: OAuth only works with identi.ca and "
393                         "Twitter.");
394        }
395
396        td->oauth_info = oauth_request_token(get_oauth_service(ic), twitter_oauth_callback, ic);
397
398        /* We need help from the user to complete OAuth login, so don't time
399           out on this login. */
400        ic->flags |= OPT_SLOW_LOGIN;
401}
402
403static gboolean twitter_oauth_callback(struct oauth_info *info)
404{
405        struct im_connection *ic = info->data;
406        struct twitter_data *td;
407
408        if (!g_slist_find(twitter_connections, ic)) {
409                return FALSE;
410        }
411
412        td = ic->proto_data;
413        if (info->stage == OAUTH_REQUEST_TOKEN) {
414                char *name, *msg;
415
416                if (info->request_token == NULL) {
417                        imcb_error(ic, "OAuth error: %s", twitter_parse_error(info->http));
418                        imc_logout(ic, TRUE);
419                        return FALSE;
420                }
421
422                name = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
423                msg = g_strdup_printf("To finish OAuth authentication, please visit "
424                                      "%s and respond with the resulting PIN code.",
425                                      info->auth_url);
426                imcb_buddy_msg(ic, name, msg, 0, 0);
427                g_free(name);
428                g_free(msg);
429        } else if (info->stage == OAUTH_ACCESS_TOKEN) {
430                const char *sn;
431
432                if (info->token == NULL || info->token_secret == NULL) {
433                        imcb_error(ic, "OAuth error: %s", twitter_parse_error(info->http));
434                        imc_logout(ic, TRUE);
435                        return FALSE;
436                }
437
438                if ((sn = oauth_params_get(&info->params, "screen_name"))) {
439                        if (ic->acc->prpl->handle_cmp(sn, ic->acc->user) != 0) {
440                                imcb_log(ic, "Warning: You logged in via OAuth as %s "
441                                         "instead of %s.", sn, ic->acc->user);
442                        }
443                        g_free(td->user);
444                        td->user = g_strdup(sn);
445                }
446
447                /* IM mods didn't do this so far and it's ugly but I should
448                   be able to get away with it... */
449                g_free(ic->acc->pass);
450                ic->acc->pass = oauth_to_string(info);
451
452                twitter_login_finish(ic);
453        }
454
455        return TRUE;
456}
457
458int twitter_url_len_diff(gchar *msg, unsigned int target_len)
459{
460        int url_len_diff = 0;
461
462        static GRegex *regex = NULL;
463        GMatchInfo *match_info;
464
465        if (regex == NULL) {
466                regex = g_regex_new("(^|\\s)(http(s)?://[^\\s$]+)", 0, 0, NULL);
467        }
468
469        g_regex_match(regex, msg, 0, &match_info);
470        while (g_match_info_matches(match_info)) {
471                gchar *url = g_match_info_fetch(match_info, 2);
472                url_len_diff += target_len - g_utf8_strlen(url, -1);
473                /* Add another character for https://t.co/... URLs */
474                if (g_match_info_fetch(match_info, 3) != NULL) {
475                        url_len_diff += 1;
476                }
477                g_free(url);
478                g_match_info_next(match_info, NULL);
479        }
480        g_match_info_free(match_info);
481
482        return url_len_diff;
483}
484
485static gboolean twitter_length_check(struct im_connection *ic, gchar * msg)
486{
487        int max = set_getint(&ic->acc->set, "message_length"), len;
488        int target_len = set_getint(&ic->acc->set, "target_url_length");
489        int url_len_diff = 0;
490
491        if (target_len > 0) {
492                url_len_diff = twitter_url_len_diff(msg, target_len);
493        }
494
495        if (max == 0 || (len = g_utf8_strlen(msg, -1) + url_len_diff) <= max) {
496                return TRUE;
497        }
498
499        twitter_log(ic, "Maximum message length exceeded: %d > %d", len, max);
500
501        return FALSE;
502}
503
504static char *set_eval_commands(set_t * set, char *value)
505{
506        if (g_strcasecmp(value, "strict") == 0) {
507                return value;
508        } else {
509                return set_eval_bool(set, value);
510        }
511}
512
513static char *set_eval_mode(set_t * set, char *value)
514{
515        if (g_strcasecmp(value, "one") == 0 ||
516            g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0) {
517                return value;
518        } else {
519                return NULL;
520        }
521}
522
523static void twitter_init(account_t * acc)
524{
525        set_t *s;
526        char *def_url;
527        char *def_tul;
528        char *def_mentions;
529
530        if (strcmp(acc->prpl->name, "twitter") == 0) {
531                def_url = TWITTER_API_URL;
532                def_tul = "22";
533                def_mentions = "true";
534        } else {                /* if( strcmp( acc->prpl->name, "identica" ) == 0 ) */
535                def_url = IDENTICA_API_URL;
536                def_tul = "0";
537                def_mentions = "false";
538        }
539
540        s = set_add(&acc->set, "auto_reply_timeout", "10800", set_eval_int, acc);
541
542        s = set_add(&acc->set, "base_url", def_url, NULL, acc);
543        s->flags |= ACC_SET_OFFLINE_ONLY;
544
545        s = set_add(&acc->set, "commands", "true", set_eval_commands, acc);
546
547        s = set_add(&acc->set, "fetch_interval", "60", set_eval_int, acc);
548        s->flags |= ACC_SET_OFFLINE_ONLY;
549
550        s = set_add(&acc->set, "fetch_mentions", def_mentions, set_eval_bool, acc);
551
552        s = set_add(&acc->set, "message_length", "140", set_eval_int, acc);
553
554        s = set_add(&acc->set, "target_url_length", def_tul, set_eval_int, acc);
555
556        s = set_add(&acc->set, "mode", "chat", set_eval_mode, acc);
557        s->flags |= ACC_SET_OFFLINE_ONLY;
558
559        s = set_add(&acc->set, "oauth", "true", set_eval_oauth, acc);
560
561        s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc);
562
563        s = set_add(&acc->set, "show_old_mentions", "0", set_eval_int, acc);
564
565        s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
566
567        s = set_add(&acc->set, "_last_tweet", "0", NULL, acc);
568        s->flags |= SET_HIDDEN | SET_NOSAVE;
569
570        if (strcmp(acc->prpl->name, "twitter") == 0) {
571                s = set_add(&acc->set, "stream", "true", set_eval_bool, acc);
572                s->flags |= ACC_SET_OFFLINE_ONLY;
573        }
574}
575
576/**
577 * Login method. Since the twitter API works with separate HTTP request we
578 * only save the user and pass to the twitter_data object.
579 */
580static void twitter_login(account_t * acc)
581{
582        struct im_connection *ic = imcb_new(acc);
583        struct twitter_data *td;
584        char name[strlen(acc->user) + 9];
585        url_t url;
586        char *s;
587
588        if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||
589            (url.proto != PROTO_HTTP && url.proto != PROTO_HTTPS)) {
590                imcb_error(ic, "Incorrect API base URL: %s", set_getstr(&ic->acc->set, "base_url"));
591                imc_logout(ic, FALSE);
592                return;
593        }
594
595        if (!strstr(url.host, "twitter.com") &&
596            set_getbool(&ic->acc->set, "stream")) {
597                imcb_error(ic, "Warning: The streaming API is only supported by Twitter, "
598                           "and you seem to be connecting to a different service.");
599        }
600
601        imcb_log(ic, "Connecting");
602
603        twitter_connections = g_slist_append(twitter_connections, ic);
604        td = g_new0(struct twitter_data, 1);
605        ic->proto_data = td;
606        td->user = g_strdup(acc->user);
607
608        td->url_ssl = url.proto == PROTO_HTTPS;
609        td->url_port = url.port;
610        td->url_host = g_strdup(url.host);
611        if (strcmp(url.file, "/") != 0) {
612                td->url_path = g_strdup(url.file);
613        } else {
614                td->url_path = g_strdup("");
615                if (g_str_has_suffix(url.host, "twitter.com")) {
616                        /* May fire for people who turned on HTTPS. */
617                        imcb_error(ic, "Warning: Twitter requires a version number in API calls "
618                                   "now. Try resetting the base_url account setting.");
619                }
620        }
621
622        /* Hacky string mangling: Turn identi.ca into identi.ca and api.twitter.com
623           into twitter, and try to be sensible if we get anything else. */
624        td->prefix = g_strdup(url.host);
625        if (g_str_has_suffix(td->prefix, ".com")) {
626                td->prefix[strlen(url.host) - 4] = '\0';
627        }
628        if ((s = strrchr(td->prefix, '.')) && strlen(s) > 4) {
629                /* If we have at least 3 chars after the last dot, cut off the rest.
630                   (mostly a www/api prefix or sth) */
631                s = g_strdup(s + 1);
632                g_free(td->prefix);
633                td->prefix = s;
634        }
635
636        if (strstr(acc->pass, "oauth_token=")) {
637                td->oauth_info = oauth_from_string(acc->pass, get_oauth_service(ic));
638        }
639
640        sprintf(name, "%s_%s", td->prefix, acc->user);
641        imcb_add_buddy(ic, name, NULL);
642        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
643
644        td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
645        td->log_id = -1;
646
647        s = set_getstr(&ic->acc->set, "mode");
648        if (g_strcasecmp(s, "one") == 0) {
649                td->flags |= TWITTER_MODE_ONE;
650        } else if (g_strcasecmp(s, "many") == 0) {
651                td->flags |= TWITTER_MODE_MANY;
652        } else {
653                td->flags |= TWITTER_MODE_CHAT;
654        }
655
656        twitter_login_finish(ic);
657}
658
659/**
660 * Logout method. Just free the twitter_data.
661 */
662static void twitter_logout(struct im_connection *ic)
663{
664        struct twitter_data *td = ic->proto_data;
665
666        // Set the status to logged out.
667        ic->flags &= ~OPT_LOGGED_IN;
668
669        // Remove the main_loop function from the function queue.
670        b_event_remove(td->main_loop_id);
671
672        if (td->timeline_gc) {
673                imcb_chat_free(td->timeline_gc);
674        }
675
676        if (td) {
677                if (td->filter_update_id > 0) {
678                        b_event_remove(td->filter_update_id);
679                }
680
681                http_close(td->stream);
682                twitter_filter_remove_all(ic);
683                oauth_info_free(td->oauth_info);
684                g_free(td->user);
685                g_free(td->prefix);
686                g_free(td->url_host);
687                g_free(td->url_path);
688                g_free(td->log);
689                g_free(td);
690        }
691
692        twitter_connections = g_slist_remove(twitter_connections, ic);
693}
694
695static void twitter_handle_command(struct im_connection *ic, char *message);
696
697/**
698 *
699 */
700static int twitter_buddy_msg(struct im_connection *ic, char *who, char *message, int away)
701{
702        struct twitter_data *td = ic->proto_data;
703        int plen = strlen(td->prefix);
704
705        if (g_strncasecmp(who, td->prefix, plen) == 0 && who[plen] == '_' &&
706            g_strcasecmp(who + plen + 1, ic->acc->user) == 0) {
707                if (set_getbool(&ic->acc->set, "oauth") &&
708                    td->oauth_info && td->oauth_info->token == NULL) {
709                        char pin[strlen(message) + 1], *s;
710
711                        strcpy(pin, message);
712                        for (s = pin + sizeof(pin) - 2; s > pin && g_ascii_isspace(*s); s--) {
713                                *s = '\0';
714                        }
715                        for (s = pin; *s && g_ascii_isspace(*s); s++) {
716                        }
717
718                        if (!oauth_access_token(s, td->oauth_info)) {
719                                imcb_error(ic, "OAuth error: %s",
720                                           "Failed to send access token request");
721                                imc_logout(ic, TRUE);
722                                return FALSE;
723                        }
724                } else {
725                        twitter_handle_command(ic, message);
726                }
727        } else {
728                twitter_direct_messages_new(ic, who, message);
729        }
730        return (0);
731}
732
733static void twitter_get_info(struct im_connection *ic, char *who)
734{
735}
736
737static void twitter_add_buddy(struct im_connection *ic, char *who, char *group)
738{
739        twitter_friendships_create_destroy(ic, who, 1);
740}
741
742static void twitter_remove_buddy(struct im_connection *ic, char *who, char *group)
743{
744        twitter_friendships_create_destroy(ic, who, 0);
745}
746
747static void twitter_chat_msg(struct groupchat *c, char *message, int flags)
748{
749        if (c && message) {
750                twitter_handle_command(c->ic, message);
751        }
752}
753
754static void twitter_chat_invite(struct groupchat *c, char *who, char *message)
755{
756}
757
758static struct groupchat *twitter_chat_join(struct im_connection *ic,
759                                           const char *room, const char *nick,
760                                           const char *password, set_t **sets)
761{
762        struct groupchat *c = imcb_chat_new(ic, room);
763        GSList *fs = twitter_filter_parse(c, room);
764        GString *topic = g_string_new("");
765        struct twitter_filter *tf;
766        GSList *l;
767
768        fs = g_slist_sort(fs, (GCompareFunc) twitter_filter_cmp);
769
770        for (l = fs; l; l = g_slist_next(l)) {
771                tf = l->data;
772
773                if (topic->len > 0) {
774                        g_string_append(topic, ", ");
775                }
776
777                if (tf->type == TWITTER_FILTER_TYPE_FOLLOW) {
778                        g_string_append_c(topic, '@');
779                }
780
781                g_string_append(topic, tf->text);
782        }
783
784        if (topic->len > 0) {
785                g_string_prepend(topic, "Twitter Filter: ");
786        }
787
788        imcb_chat_topic(c, NULL, topic->str, 0);
789        imcb_chat_add_buddy(c, ic->acc->user);
790
791        if (topic->len == 0) {
792                imcb_error(ic, "Failed to handle any filters");
793                imcb_chat_free(c);
794                c = NULL;
795        }
796
797        g_string_free(topic, TRUE);
798        g_slist_free(fs);
799
800        return c;
801}
802
803static void twitter_chat_leave(struct groupchat *c)
804{
805        struct twitter_data *td = c->ic->proto_data;
806
807        if (c != td->timeline_gc) {
808                twitter_filter_remove(c);
809                imcb_chat_free(c);
810                return;
811        }
812
813        /* If the user leaves the channel: Fine. Rejoin him/her once new
814           tweets come in. */
815        imcb_chat_free(td->timeline_gc);
816        td->timeline_gc = NULL;
817}
818
819static void twitter_keepalive(struct im_connection *ic)
820{
821}
822
823static void twitter_add_permit(struct im_connection *ic, char *who)
824{
825}
826
827static void twitter_rem_permit(struct im_connection *ic, char *who)
828{
829}
830
831static void twitter_add_deny(struct im_connection *ic, char *who)
832{
833}
834
835static void twitter_rem_deny(struct im_connection *ic, char *who)
836{
837}
838
839//static char *twitter_set_display_name( set_t *set, char *value )
840//{
841//      return value;
842//}
843
844static void twitter_buddy_data_add(struct bee_user *bu)
845{
846        bu->data = g_new0(struct twitter_user_data, 1);
847}
848
849static void twitter_buddy_data_free(struct bee_user *bu)
850{
851        g_free(bu->data);
852}
853
854/* Parses a decimal or hex tweet ID, returns TRUE on success */
855static gboolean twitter_parse_id(char *string, int base, guint64 *id)
856{
857        guint64 parsed;
858        char *endptr;
859
860        errno = 0;
861        parsed = g_ascii_strtoull(string, &endptr, base);
862        if (errno || endptr == string || *endptr != '\0') {
863                return FALSE;
864        }
865        *id = parsed;
866        return TRUE;
867}
868
869/** Convert the given bitlbee tweet ID, bitlbee username, or twitter tweet ID
870 *  into a twitter tweet ID.
871 *
872 *  Returns 0 if the user provides garbage.
873 */
874static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_)
875{
876        struct twitter_data *td = ic->proto_data;
877        struct twitter_user_data *tud;
878        bee_user_t *bu = NULL;
879        guint64 id = 0;
880
881        if (bu_) {
882                *bu_ = NULL;
883        }
884        if (!arg || !arg[0]) {
885                return 0;
886        }
887
888        if (arg[0] != '#' && (bu = bee_user_by_handle(ic->bee, ic, arg))) {
889                if ((tud = bu->data)) {
890                        id = tud->last_id;
891                }
892        } else {
893                if (arg[0] == '#') {
894                        arg++;
895                }
896                if (twitter_parse_id(arg, 16, &id) && id < TWITTER_LOG_LENGTH) {
897                        bu = td->log[id].bu;
898                        id = td->log[id].id;
899                        /* Beware of dangling pointers! */
900                        if (!g_slist_find(ic->bee->users, bu)) {
901                                bu = NULL;
902                        }
903                } else if (twitter_parse_id(arg, 10, &id)) {
904                        /* Allow normal tweet IDs as well; not a very useful
905                           feature but it's always been there. Just ignore
906                           very low IDs to avoid accidents. */
907                        if (id < 1000000) {
908                                id = 0;
909                        }
910                }
911        }
912        if (bu_) {
913                *bu_ = bu;
914        }
915        return id;
916}
917
918static void twitter_handle_command(struct im_connection *ic, char *message)
919{
920        struct twitter_data *td = ic->proto_data;
921        char *cmds, **cmd, *new = NULL;
922        guint64 in_reply_to = 0, id;
923        gboolean allow_post =
924                g_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0;
925        bee_user_t *bu = NULL;
926
927        cmds = g_strdup(message);
928        cmd = split_command_parts(cmds, 2);
929
930        if (cmd[0] == NULL) {
931                goto eof;
932        } else if (!set_getbool(&ic->acc->set, "commands") && allow_post) {
933                /* Not supporting commands if "commands" is set to true/strict. */
934        } else if (g_strcasecmp(cmd[0], "undo") == 0) {
935                if (cmd[1] == NULL) {
936                        twitter_status_destroy(ic, td->last_status_id);
937                } else if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) {
938                        twitter_status_destroy(ic, id);
939                } else {
940                        twitter_log(ic, "Could not undo last action");
941                }
942
943                goto eof;
944        } else if ((g_strcasecmp(cmd[0], "favourite") == 0 ||
945                    g_strcasecmp(cmd[0], "favorite") == 0 ||
946                    g_strcasecmp(cmd[0], "fav") == 0) && cmd[1]) {
947                if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) {
948                        twitter_favourite_tweet(ic, id);
949                } else {
950                        twitter_log(ic, "Please provide a message ID or username.");
951                }
952                goto eof;
953        } else if (g_strcasecmp(cmd[0], "follow") == 0 && cmd[1]) {
954                twitter_add_buddy(ic, cmd[1], NULL);
955                goto eof;
956        } else if (g_strcasecmp(cmd[0], "unfollow") == 0 && cmd[1]) {
957                twitter_remove_buddy(ic, cmd[1], NULL);
958                goto eof;
959        } else if ((g_strcasecmp(cmd[0], "report") == 0 ||
960                    g_strcasecmp(cmd[0], "spam") == 0) && cmd[1]) {
961                char *screen_name;
962
963                /* Report nominally works on users but look up the user who
964                   posted the given ID if the user wants to do it that way */
965                twitter_message_id_from_command_arg(ic, cmd[1], &bu);
966                if (bu) {
967                        screen_name = bu->handle;
968                } else {
969                        screen_name = cmd[1];
970                }
971
972                twitter_report_spam(ic, screen_name);
973                goto eof;
974        } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) {
975                id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
976
977                td->last_status_id = 0;
978                if (id) {
979                        twitter_status_retweet(ic, id);
980                } else {
981                        twitter_log(ic, "User `%s' does not exist or didn't "
982                                    "post any statuses recently", cmd[1]);
983                }
984
985                goto eof;
986        } else if (g_strcasecmp(cmd[0], "reply") == 0 && cmd[1] && cmd[2]) {
987                id = twitter_message_id_from_command_arg(ic, cmd[1], &bu);
988                if (!id || !bu) {
989                        twitter_log(ic, "User `%s' does not exist or didn't "
990                                    "post any statuses recently", cmd[1]);
991                        goto eof;
992                }
993                message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]);
994                in_reply_to = id;
995                allow_post = TRUE;
996        } else if (g_strcasecmp(cmd[0], "rawreply") == 0 && cmd[1] && cmd[2]) {
997                id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
998                if (!id) {
999                        twitter_log(ic, "Tweet `%s' does not exist", cmd[1]);
1000                        goto eof;
1001                }
1002                message = cmd[2];
1003                in_reply_to = id;
1004                allow_post = TRUE;
1005        } else if (g_strcasecmp(cmd[0], "post") == 0) {
1006                message += 5;
1007                allow_post = TRUE;
1008        }
1009
1010        if (allow_post) {
1011                char *s;
1012
1013                if (!twitter_length_check(ic, message)) {
1014                        goto eof;
1015                }
1016
1017                s = cmd[0] + strlen(cmd[0]) - 1;
1018                if (!new && s > cmd[0] && (*s == ':' || *s == ',')) {
1019                        *s = '\0';
1020
1021                        if ((bu = bee_user_by_handle(ic->bee, ic, cmd[0]))) {
1022                                struct twitter_user_data *tud = bu->data;
1023
1024                                new = g_strdup_printf("@%s %s", bu->handle,
1025                                                      message + (s - cmd[0]) + 2);
1026                                message = new;
1027
1028                                if (time(NULL) < tud->last_time +
1029                                    set_getint(&ic->acc->set, "auto_reply_timeout")) {
1030                                        in_reply_to = tud->last_id;
1031                                }
1032                        }
1033                }
1034
1035                /* If the user runs undo between this request and its response
1036                   this would delete the second-last Tweet. Prevent that. */
1037                td->last_status_id = 0;
1038                twitter_post_status(ic, message, in_reply_to);
1039        } else {
1040                twitter_log(ic, "Unknown command: %s", cmd[0]);
1041        }
1042eof:
1043        g_free(new);
1044        g_free(cmds);
1045}
1046
1047void twitter_log(struct im_connection *ic, char *format, ...)
1048{
1049        struct twitter_data *td = ic->proto_data;
1050        va_list params;
1051        char *text;
1052
1053        va_start(params, format);
1054        text = g_strdup_vprintf(format, params);
1055        va_end(params);
1056
1057        if (td->timeline_gc) {
1058                imcb_chat_log(td->timeline_gc, "%s", text);
1059        } else {
1060                imcb_log(ic, "%s", text);
1061        }
1062
1063        g_free(text);
1064}
1065
1066
1067void twitter_initmodule()
1068{
1069        struct prpl *ret = g_new0(struct prpl, 1);
1070
1071        ret->options = OPT_NOOTR;
1072        ret->name = "twitter";
1073        ret->login = twitter_login;
1074        ret->init = twitter_init;
1075        ret->logout = twitter_logout;
1076        ret->buddy_msg = twitter_buddy_msg;
1077        ret->get_info = twitter_get_info;
1078        ret->add_buddy = twitter_add_buddy;
1079        ret->remove_buddy = twitter_remove_buddy;
1080        ret->chat_msg = twitter_chat_msg;
1081        ret->chat_invite = twitter_chat_invite;
1082        ret->chat_join = twitter_chat_join;
1083        ret->chat_leave = twitter_chat_leave;
1084        ret->keepalive = twitter_keepalive;
1085        ret->add_permit = twitter_add_permit;
1086        ret->rem_permit = twitter_rem_permit;
1087        ret->add_deny = twitter_add_deny;
1088        ret->rem_deny = twitter_rem_deny;
1089        ret->buddy_data_add = twitter_buddy_data_add;
1090        ret->buddy_data_free = twitter_buddy_data_free;
1091        ret->handle_cmp = g_strcasecmp;
1092
1093        register_protocol(ret);
1094
1095        /* And an identi.ca variant: */
1096        ret = g_memdup(ret, sizeof(struct prpl));
1097        ret->name = "identica";
1098        register_protocol(ret);
1099}
Note: See TracBrowser for help on using the repository browser.