source: protocols/twitter/twitter.c @ 73ee390

Last change on this file since 73ee390 was 73ee390, checked in by jgeboski <jgeboski@…>, at 2015-01-26T03:46:03Z

twitter: implemented filter based group chats

Filter group chats allow for the ability to read the tweets of select
users without actually following the users, and/or track keywords or
hashtags. A filter group chat can have multiple users, keywords, or
hashtags. These users, keywords, or hashtags can span multiple group
chats. This allows for rather robust filter organization.

The underlying structure for the filters is based on linked list, as
using the glib hash tables requires >= glib-2.16 for sanity. Since the
glib requirement of bitlbee is only 2.14, linked list are used in order
to prevent an overly complex implementation.

The idea for this patch was inspired by Artem Savkov's "Twitter search
channels" patch.

In order to use the filter group chats, a group chat must be added to
the twitter account. The channel room name is either follow:username,
track:keyword, and/or track:#hashtag. Multiple elements can be used by
separating each element by a semicolon.

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