source: protocols/twitter/twitter.c @ 2f73692

Last change on this file since 2f73692 was 1201fcb, checked in by dequis <dx@…>, at 2015-06-08T03:42:11Z

twitter: show full url in the url command, with username

By asking the server for the username.

Storing the username somewhere would have made sense, but this command
isn't going to be used very often, so, whatever.

  • Property mode set to 100644
File size: 28.6 KB
RevLine 
[1b221e0]1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
[96dd574]6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
[0e788f5]7*  Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net>              *
[1b221e0]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"
[713d611]26#include "oauth.h"
[1b221e0]27#include "twitter.h"
28#include "twitter_http.h"
29#include "twitter_lib.h"
[bb5ce4d1]30#include "url.h"
[1b221e0]31
[9c9a29c]32GSList *twitter_connections = NULL;
[73ee390]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;
[5ebff60]93        struct twitter_filter tfc = { type, (char *) text };
[73ee390]94        GSList *l;
95
96        for (l = td->filters; l; l = g_slist_next(l)) {
97                tf = l->data;
98
[5ebff60]99                if (twitter_filter_cmp(tf, &tfc) == 0) {
[73ee390]100                        break;
[5ebff60]101                }
[73ee390]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
[5ebff60]113        if (!g_slist_find(tf->groupchats, c)) {
[73ee390]114                tf->groupchats = g_slist_prepend(tf->groupchats, c);
[5ebff60]115        }
[73ee390]116
[5ebff60]117        if (td->filter_update_id > 0) {
[73ee390]118                b_event_remove(td->filter_update_id);
[5ebff60]119        }
[73ee390]120
121        /* Wait for other possible filter changes to avoid request spam */
122        td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
[5ebff60]123                                             twitter_filter_update, c->ic);
[73ee390]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
[5ebff60]154        if (td->filter_update_id > 0) {
[73ee390]155                b_event_remove(td->filter_update_id);
[5ebff60]156        }
[73ee390]157
158        /* Wait for other possible filter changes to avoid request spam */
159        td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
[5ebff60]160                                             twitter_filter_update, c->ic);
161}
[73ee390]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)) {
[5ebff60]176                        if (!g_slist_find(chats, p->data)) {
[73ee390]177                                chats = g_slist_prepend(chats, p->data);
[5ebff60]178                        }
[73ee390]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++) {
[5ebff60]225                if ((v = strchr(*f, ':')) == NULL) {
[73ee390]226                        continue;
[5ebff60]227                }
[73ee390]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
[5ebff60]238                if (t < 0 || strlen(v) == 0) {
[73ee390]239                        continue;
[5ebff60]240                }
[73ee390]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
[1b221e0]250/**
[62d2cfb]251 * Main loop function
252 */
[1b221e0]253gboolean twitter_main_loop(gpointer data, gint fd, b_input_condition cond)
254{
255        struct im_connection *ic = data;
[5983eca]256
[1b221e0]257        // Check if we are still logged in...
[5ebff60]258        if (!g_slist_find(twitter_connections, ic)) {
[2f9027c]259                return FALSE;
[5ebff60]260        }
[1b221e0]261
262        // Do stuff..
[2f9027c]263        return twitter_get_timeline(ic, -1) &&
264               ((ic->flags & OPT_LOGGED_IN) == OPT_LOGGED_IN);
[1b221e0]265}
266
[5983eca]267static void twitter_main_loop_start(struct im_connection *ic)
[713d611]268{
269        struct twitter_data *td = ic->proto_data;
[5983eca]270
[eb4ad8d]271        char *last_tweet = set_getstr(&ic->acc->set, "_last_tweet");
[5ebff60]272
273        if (last_tweet) {
[7549d00]274                td->timeline_id = g_ascii_strtoull(last_tweet, NULL, 0);
[5ebff60]275        }
[7549d00]276
[631ec80]277        /* Create the room now that we "logged in". */
[5ebff60]278        if (td->flags & TWITTER_MODE_CHAT) {
[631ec80]279                twitter_groupchat_init(ic);
[5ebff60]280        }
[631ec80]281
[5983eca]282        imcb_log(ic, "Getting initial statuses");
[713d611]283
[f26d9a3e]284        // Run this once. After this queue the main loop function (or open the
285        // stream if available).
[713d611]286        twitter_main_loop(ic, -1, 0);
[5ebff60]287
[dff0e0b]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);
[5ebff60]292
[f26d9a3e]293                /* Stream sends keepalives (empty lines) or actual data at
294                   least twice a minute. Disconnect if this stops. */
[e132b60]295                ic->flags |= OPT_PONGS;
[dff0e0b]296        } else {
297                /* Not using the streaming API, so keep polling the old-
298                   fashioned way. :-( */
299                td->main_loop_id =
[5ebff60]300                        b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000,
301                                      twitter_main_loop, ic);
[dff0e0b]302        }
[713d611]303}
304
[631ec80]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
[5ebff60]312        if (td->timeline_gc) {
[631ec80]313                return td->timeline_gc;
[5ebff60]314        }
[631ec80]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;
[5ebff60]324                if (bu->ic == ic) {
[29f72b7]325                        imcb_chat_add_buddy(gc, bu->handle);
[5ebff60]326                }
[631ec80]327        }
328        imcb_chat_add_buddy(gc, ic->acc->user);
[5ebff60]329
[631ec80]330        return gc;
331}
332
[5983eca]333static void twitter_oauth_start(struct im_connection *ic);
[d6aa6dd]334
[5983eca]335void twitter_login_finish(struct im_connection *ic)
[d6aa6dd]336{
337        struct twitter_data *td = ic->proto_data;
[5983eca]338
[2322a9f]339        td->flags &= ~TWITTER_DOING_TIMELINE;
340
[5ebff60]341        if (set_getbool(&ic->acc->set, "oauth") && !td->oauth_info) {
[5983eca]342                twitter_oauth_start(ic);
[5ebff60]343        } else if (!(td->flags & TWITTER_MODE_ONE) &&
344                   !(td->flags & TWITTER_HAVE_FRIENDS)) {
[5983eca]345                imcb_log(ic, "Getting contact list");
[de923d5]346                twitter_get_friends_ids(ic, -1);
[5ebff60]347        } else {
[5983eca]348                twitter_main_loop_start(ic);
[5ebff60]349        }
[d6aa6dd]350}
[c2ecadc]351
[5983eca]352static const struct oauth_service twitter_oauth = {
[3f808ca]353        "https://api.twitter.com/oauth/request_token",
354        "https://api.twitter.com/oauth/access_token",
[c01bbd1]355        "https://api.twitter.com/oauth/authorize",
[c2ecadc]356        .consumer_key = "xsDNKJuNZYkZyMcu914uEA",
357        .consumer_secret = "FCxqcr0pXKzsF9ajmP57S3VQ8V6Drk4o2QYtqMcOszo",
358};
359
[5983eca]360static const struct oauth_service identica_oauth = {
[3f808ca]361        "https://identi.ca/api/oauth/request_token",
362        "https://identi.ca/api/oauth/access_token",
[ce617f0]363        "https://identi.ca/api/oauth/authorize",
364        .consumer_key = "e147ff789fcbd8a5a07963afbb43f9da",
365        .consumer_secret = "c596267f277457ec0ce1ab7bb788d828",
366};
367
[5983eca]368static gboolean twitter_oauth_callback(struct oauth_info *info);
[713d611]369
[5983eca]370static const struct oauth_service *get_oauth_service(struct im_connection *ic)
[ce617f0]371{
372        struct twitter_data *td = ic->proto_data;
[5983eca]373
[5ebff60]374        if (strstr(td->url_host, "identi.ca")) {
[ce617f0]375                return &identica_oauth;
[5ebff60]376        } else {
[ce617f0]377                return &twitter_oauth;
[5ebff60]378        }
[5983eca]379
[ce617f0]380        /* Could add more services, or allow configuring your own base URL +
381           API keys. */
382}
383
[5983eca]384static void twitter_oauth_start(struct im_connection *ic)
[713d611]385{
[18dbb20]386        struct twitter_data *td = ic->proto_data;
[f2d8aa2]387        const char *url = set_getstr(&ic->acc->set, "base_url");
[c42e8b9]388
[5983eca]389        imcb_log(ic, "Requesting OAuth request token");
[5ebff60]390
391        if (!strstr(url, "twitter.com") && !strstr(url, "identi.ca")) {
[f2d8aa2]392                imcb_log(ic, "Warning: OAuth only works with identi.ca and "
[5ebff60]393                         "Twitter.");
394        }
[5983eca]395
396        td->oauth_info = oauth_request_token(get_oauth_service(ic), twitter_oauth_callback, ic);
397
[748bcdd]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;
[713d611]401}
402
[5983eca]403static gboolean twitter_oauth_callback(struct oauth_info *info)
[713d611]404{
405        struct im_connection *ic = info->data;
[18dbb20]406        struct twitter_data *td;
[5983eca]407
[5ebff60]408        if (!g_slist_find(twitter_connections, ic)) {
[18dbb20]409                return FALSE;
[5ebff60]410        }
[5983eca]411
[18dbb20]412        td = ic->proto_data;
[5983eca]413        if (info->stage == OAUTH_REQUEST_TOKEN) {
[f2d8aa2]414                char *name, *msg;
[5983eca]415
416                if (info->request_token == NULL) {
417                        imcb_error(ic, "OAuth error: %s", twitter_parse_error(info->http));
418                        imc_logout(ic, TRUE);
[18dbb20]419                        return FALSE;
[c42e8b9]420                }
[5983eca]421
[f2d8aa2]422                name = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
[5983eca]423                msg = g_strdup_printf("To finish OAuth authentication, please visit "
[5ebff60]424                                      "%s and respond with the resulting PIN code.",
425                                      info->auth_url);
[5983eca]426                imcb_buddy_msg(ic, name, msg, 0, 0);
[f2d8aa2]427                g_free(name);
[5983eca]428                g_free(msg);
429        } else if (info->stage == OAUTH_ACCESS_TOKEN) {
[cfbecc9]430                const char *sn;
[5ebff60]431
[5983eca]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);
[18dbb20]435                        return FALSE;
[cfbecc9]436                }
[5ebff60]437
[cfbecc9]438                if ((sn = oauth_params_get(&info->params, "screen_name"))) {
[5ebff60]439                        if (ic->acc->prpl->handle_cmp(sn, ic->acc->user) != 0) {
[5983eca]440                                imcb_log(ic, "Warning: You logged in via OAuth as %s "
[cfbecc9]441                                         "instead of %s.", sn, ic->acc->user);
[5ebff60]442                        }
[de923d5]443                        g_free(td->user);
444                        td->user = g_strdup(sn);
[93cc86f]445                }
[5983eca]446
[288b215]447                /* IM mods didn't do this so far and it's ugly but I should
448                   be able to get away with it... */
[5983eca]449                g_free(ic->acc->pass);
450                ic->acc->pass = oauth_to_string(info);
451
452                twitter_login_finish(ic);
[c42e8b9]453        }
[5983eca]454
[18dbb20]455        return TRUE;
[713d611]456}
457
[7d27962]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
[5ebff60]465        if (regex == NULL) {
[7d27962]466                regex = g_regex_new("(^|\\s)(http(s)?://[^\\s$]+)", 0, 0, NULL);
[5ebff60]467        }
468
[7d27962]469        g_regex_match(regex, msg, 0, &match_info);
470        while (g_match_info_matches(match_info)) {
[61e7e02]471                gchar *s, *url;
472
473                url = g_match_info_fetch(match_info, 2);
[7d27962]474                url_len_diff += target_len - g_utf8_strlen(url, -1);
[61e7e02]475
[a685409]476                /* Add another character for https://t.co/... URLs */
[61e7e02]477                if ((s = g_match_info_fetch(match_info, 3))) {
[7d27962]478                        url_len_diff += 1;
[61e7e02]479                        g_free(s);
[5ebff60]480                }
[7d27962]481                g_free(url);
482                g_match_info_next(match_info, NULL);
483        }
484        g_match_info_free(match_info);
485
486        return url_len_diff;
487}
488
[33528bd]489int twitter_message_len(gchar *msg, int target_len)
[9997691]490{
[7d27962]491        int url_len_diff = 0;
[a685409]492
[5ebff60]493        if (target_len > 0) {
[7d27962]494                url_len_diff = twitter_url_len_diff(msg, target_len);
[5ebff60]495        }
[5983eca]496
[33528bd]497        return g_utf8_strlen(msg, -1) + url_len_diff;
498}
499
500static gboolean twitter_length_check(struct im_connection *ic, gchar * msg)
501{
502        int max = set_getint(&ic->acc->set, "message_length");
503        int target_len = set_getint(&ic->acc->set, "target_url_length");
504        int len = twitter_message_len(msg, target_len);
505
506        if (max == 0 || len <= max) {
[9997691]507                return TRUE;
[5ebff60]508        }
[5983eca]509
[96dd574]510        twitter_log(ic, "Maximum message length exceeded: %d > %d", len, max);
[5983eca]511
[9997691]512        return FALSE;
513}
514
[3592b95]515static char *set_eval_commands(set_t * set, char *value)
516{
[5ebff60]517        if (g_strcasecmp(value, "strict") == 0) {
[3592b95]518                return value;
[5ebff60]519        } else {
[3592b95]520                return set_eval_bool(set, value);
[5ebff60]521        }
[3592b95]522}
523
524static char *set_eval_mode(set_t * set, char *value)
525{
526        if (g_strcasecmp(value, "one") == 0 ||
[5ebff60]527            g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0) {
[3592b95]528                return value;
[5ebff60]529        } else {
[3592b95]530                return NULL;
[5ebff60]531        }
[3592b95]532}
533
[5983eca]534static void twitter_init(account_t * acc)
[1b221e0]535{
[62d2cfb]536        set_t *s;
[ffcdf13]537        char *def_url;
[7d27962]538        char *def_tul;
[777461b]539        char *def_mentions;
[5983eca]540
541        if (strcmp(acc->prpl->name, "twitter") == 0) {
[ffcdf13]542                def_url = TWITTER_API_URL;
[a685409]543                def_tul = "22";
[777461b]544                def_mentions = "true";
[5ebff60]545        } else {                /* if( strcmp( acc->prpl->name, "identica" ) == 0 ) */
[ffcdf13]546                def_url = IDENTICA_API_URL;
[7d27962]547                def_tul = "0";
[777461b]548                def_mentions = "false";
[ffcdf13]549        }
[5983eca]550
551        s = set_add(&acc->set, "auto_reply_timeout", "10800", set_eval_int, acc);
552
553        s = set_add(&acc->set, "base_url", def_url, NULL, acc);
[bb5ce4d1]554        s->flags |= ACC_SET_OFFLINE_ONLY;
[5983eca]555
[3592b95]556        s = set_add(&acc->set, "commands", "true", set_eval_commands, acc);
[5983eca]557
[2322a9f]558        s = set_add(&acc->set, "fetch_interval", "60", set_eval_int, acc);
559        s->flags |= ACC_SET_OFFLINE_ONLY;
560
[777461b]561        s = set_add(&acc->set, "fetch_mentions", def_mentions, set_eval_bool, acc);
[2322a9f]562
[5983eca]563        s = set_add(&acc->set, "message_length", "140", set_eval_int, acc);
564
[7d27962]565        s = set_add(&acc->set, "target_url_length", def_tul, set_eval_int, acc);
566
[5983eca]567        s = set_add(&acc->set, "mode", "chat", set_eval_mode, acc);
[2abceca]568        s->flags |= ACC_SET_OFFLINE_ONLY;
[5983eca]569
[b3d99e3]570        s = set_add(&acc->set, "oauth", "true", set_eval_oauth, acc);
[ce199b7]571
[e93fa05]572        s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc);
[5983eca]573
[f75b3a7]574        s = set_add(&acc->set, "show_old_mentions", "0", set_eval_int, acc);
[948abab]575
576        s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
[5ebff60]577
[eb4ad8d]578        s = set_add(&acc->set, "_last_tweet", "0", NULL, acc);
579        s->flags |= SET_HIDDEN | SET_NOSAVE;
[7549d00]580
[f26d9a3e]581        if (strcmp(acc->prpl->name, "twitter") == 0) {
582                s = set_add(&acc->set, "stream", "true", set_eval_bool, acc);
583                s->flags |= ACC_SET_OFFLINE_ONLY;
584        }
[1b221e0]585}
586
587/**
[f26d9a3e]588 * Login method. Since the twitter API works with separate HTTP request we
[1b221e0]589 * only save the user and pass to the twitter_data object.
590 */
[5983eca]591static void twitter_login(account_t * acc)
[1b221e0]592{
[5983eca]593        struct im_connection *ic = imcb_new(acc);
[bb5ce4d1]594        struct twitter_data *td;
[5983eca]595        char name[strlen(acc->user) + 9];
[bb5ce4d1]596        url_t url;
[c0f33f1]597        char *s;
[5ebff60]598
[5983eca]599        if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||
600            (url.proto != PROTO_HTTP && url.proto != PROTO_HTTPS)) {
601                imcb_error(ic, "Incorrect API base URL: %s", set_getstr(&ic->acc->set, "base_url"));
602                imc_logout(ic, FALSE);
[bb5ce4d1]603                return;
604        }
[5983eca]605
[f26d9a3e]606        if (!strstr(url.host, "twitter.com") &&
607            set_getbool(&ic->acc->set, "stream")) {
608                imcb_error(ic, "Warning: The streaming API is only supported by Twitter, "
[5ebff60]609                           "and you seem to be connecting to a different service.");
[f26d9a3e]610        }
611
[c0f33f1]612        imcb_log(ic, "Connecting");
613
[5983eca]614        twitter_connections = g_slist_append(twitter_connections, ic);
615        td = g_new0(struct twitter_data, 1);
[713d611]616        ic->proto_data = td;
[de923d5]617        td->user = g_strdup(acc->user);
[5983eca]618
[bb5ce4d1]619        td->url_ssl = url.proto == PROTO_HTTPS;
620        td->url_port = url.port;
[5983eca]621        td->url_host = g_strdup(url.host);
[5ebff60]622        if (strcmp(url.file, "/") != 0) {
[5983eca]623                td->url_path = g_strdup(url.file);
[5ebff60]624        } else {
[5983eca]625                td->url_path = g_strdup("");
[5ebff60]626                if (g_str_has_suffix(url.host, "twitter.com")) {
[c0f33f1]627                        /* May fire for people who turned on HTTPS. */
628                        imcb_error(ic, "Warning: Twitter requires a version number in API calls "
[5ebff60]629                                   "now. Try resetting the base_url account setting.");
630                }
[c0f33f1]631        }
[5ebff60]632
[c0f33f1]633        /* Hacky string mangling: Turn identi.ca into identi.ca and api.twitter.com
634           into twitter, and try to be sensible if we get anything else. */
635        td->prefix = g_strdup(url.host);
[5ebff60]636        if (g_str_has_suffix(td->prefix, ".com")) {
[c0f33f1]637                td->prefix[strlen(url.host) - 4] = '\0';
[5ebff60]638        }
[4bc66ae]639        if ((s = strrchr(td->prefix, '.')) && strlen(s) > 4) {
640                /* If we have at least 3 chars after the last dot, cut off the rest.
641                   (mostly a www/api prefix or sth) */
[c0f33f1]642                s = g_strdup(s + 1);
643                g_free(td->prefix);
644                td->prefix = s;
645        }
[5ebff60]646
647        if (strstr(acc->pass, "oauth_token=")) {
[5983eca]648                td->oauth_info = oauth_from_string(acc->pass, get_oauth_service(ic));
[5ebff60]649        }
[5983eca]650
651        sprintf(name, "%s_%s", td->prefix, acc->user);
652        imcb_add_buddy(ic, name, NULL);
653        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
654
[c9b5817]655        td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
656        td->log_id = -1;
[5ebff60]657
[631ec80]658        s = set_getstr(&ic->acc->set, "mode");
[5ebff60]659        if (g_strcasecmp(s, "one") == 0) {
[631ec80]660                td->flags |= TWITTER_MODE_ONE;
[5ebff60]661        } else if (g_strcasecmp(s, "many") == 0) {
[631ec80]662                td->flags |= TWITTER_MODE_MANY;
[5ebff60]663        } else {
[631ec80]664                td->flags |= TWITTER_MODE_CHAT;
[5ebff60]665        }
[5983eca]666
667        twitter_login_finish(ic);
[1b221e0]668}
669
670/**
671 * Logout method. Just free the twitter_data.
672 */
[5983eca]673static void twitter_logout(struct im_connection *ic)
[1b221e0]674{
675        struct twitter_data *td = ic->proto_data;
[5983eca]676
[1b221e0]677        // Set the status to logged out.
[5983eca]678        ic->flags &= ~OPT_LOGGED_IN;
[1b221e0]679
[2abceca]680        // Remove the main_loop function from the function queue.
681        b_event_remove(td->main_loop_id);
682
[5ebff60]683        if (td->timeline_gc) {
[2322a9f]684                imcb_chat_free(td->timeline_gc);
[5ebff60]685        }
[1014cab]686
[5983eca]687        if (td) {
[5ebff60]688                if (td->filter_update_id > 0) {
[73ee390]689                        b_event_remove(td->filter_update_id);
[5ebff60]690                }
[73ee390]691
[1388d30]692                http_close(td->stream);
[73ee390]693                twitter_filter_remove_all(ic);
[5983eca]694                oauth_info_free(td->oauth_info);
[de923d5]695                g_free(td->user);
[5983eca]696                g_free(td->prefix);
697                g_free(td->url_host);
698                g_free(td->url_path);
699                g_free(td->log);
700                g_free(td);
[1b221e0]701        }
[62d2cfb]702
[5983eca]703        twitter_connections = g_slist_remove(twitter_connections, ic);
[1b221e0]704}
705
[5983eca]706static void twitter_handle_command(struct im_connection *ic, char *message);
[7b87539]707
[1b221e0]708/**
709 *
710 */
[5983eca]711static int twitter_buddy_msg(struct im_connection *ic, char *who, char *message, int away)
[1b221e0]712{
[c42e8b9]713        struct twitter_data *td = ic->proto_data;
[5983eca]714        int plen = strlen(td->prefix);
715
[ffcdf13]716        if (g_strncasecmp(who, td->prefix, plen) == 0 && who[plen] == '_' &&
[5983eca]717            g_strcasecmp(who + plen + 1, ic->acc->user) == 0) {
718                if (set_getbool(&ic->acc->set, "oauth") &&
719                    td->oauth_info && td->oauth_info->token == NULL) {
720                        char pin[strlen(message) + 1], *s;
721
722                        strcpy(pin, message);
[5ebff60]723                        for (s = pin + sizeof(pin) - 2; s > pin && g_ascii_isspace(*s); s--) {
[64f8c425]724                                *s = '\0';
[5ebff60]725                        }
[6b13103]726                        for (s = pin; *s && g_ascii_isspace(*s); s++) {
[5983eca]727                        }
728
729                        if (!oauth_access_token(s, td->oauth_info)) {
730                                imcb_error(ic, "OAuth error: %s",
[5ebff60]731                                           "Failed to send access token request");
[5983eca]732                                imc_logout(ic, TRUE);
[c2ecadc]733                                return FALSE;
734                        }
[5ebff60]735                } else {
[7b87539]736                        twitter_handle_command(ic, message);
[5ebff60]737                }
[5983eca]738        } else {
[e88fbe27]739                twitter_direct_messages_new(ic, who, message);
[c42e8b9]740        }
[5983eca]741        return (0);
[1b221e0]742}
743
[5983eca]744static void twitter_get_info(struct im_connection *ic, char *who)
[1b221e0]745{
746}
747
[5983eca]748static void twitter_add_buddy(struct im_connection *ic, char *who, char *group)
[1b221e0]749{
[7d53efb]750        twitter_friendships_create_destroy(ic, who, 1);
[1b221e0]751}
752
[5983eca]753static void twitter_remove_buddy(struct im_connection *ic, char *who, char *group)
[1b221e0]754{
[7d53efb]755        twitter_friendships_create_destroy(ic, who, 0);
[1b221e0]756}
757
[5983eca]758static void twitter_chat_msg(struct groupchat *c, char *message, int flags)
[1b221e0]759{
[5ebff60]760        if (c && message) {
[5983eca]761                twitter_handle_command(c->ic, message);
[5ebff60]762        }
[1b221e0]763}
764
[5983eca]765static void twitter_chat_invite(struct groupchat *c, char *who, char *message)
[1b221e0]766{
767}
768
[73ee390]769static struct groupchat *twitter_chat_join(struct im_connection *ic,
770                                           const char *room, const char *nick,
771                                           const char *password, set_t **sets)
772{
773        struct groupchat *c = imcb_chat_new(ic, room);
774        GSList *fs = twitter_filter_parse(c, room);
775        GString *topic = g_string_new("");
776        struct twitter_filter *tf;
777        GSList *l;
778
779        fs = g_slist_sort(fs, (GCompareFunc) twitter_filter_cmp);
780
781        for (l = fs; l; l = g_slist_next(l)) {
782                tf = l->data;
783
[5ebff60]784                if (topic->len > 0) {
[73ee390]785                        g_string_append(topic, ", ");
[5ebff60]786                }
[73ee390]787
[5ebff60]788                if (tf->type == TWITTER_FILTER_TYPE_FOLLOW) {
[73ee390]789                        g_string_append_c(topic, '@');
[5ebff60]790                }
[73ee390]791
792                g_string_append(topic, tf->text);
793        }
794
[5ebff60]795        if (topic->len > 0) {
[73ee390]796                g_string_prepend(topic, "Twitter Filter: ");
[5ebff60]797        }
[73ee390]798
799        imcb_chat_topic(c, NULL, topic->str, 0);
800        imcb_chat_add_buddy(c, ic->acc->user);
801
802        if (topic->len == 0) {
803                imcb_error(ic, "Failed to handle any filters");
804                imcb_chat_free(c);
805                c = NULL;
806        }
807
808        g_string_free(topic, TRUE);
809        g_slist_free(fs);
810
811        return c;
812}
813
[5983eca]814static void twitter_chat_leave(struct groupchat *c)
[1b221e0]815{
[16592d8]816        struct twitter_data *td = c->ic->proto_data;
[5983eca]817
[73ee390]818        if (c != td->timeline_gc) {
819                twitter_filter_remove(c);
820                imcb_chat_free(c);
821                return;
822        }
[5983eca]823
[16592d8]824        /* If the user leaves the channel: Fine. Rejoin him/her once new
825           tweets come in. */
[2322a9f]826        imcb_chat_free(td->timeline_gc);
827        td->timeline_gc = NULL;
[1b221e0]828}
829
[5983eca]830static void twitter_keepalive(struct im_connection *ic)
[1b221e0]831{
832}
833
[5983eca]834static void twitter_add_permit(struct im_connection *ic, char *who)
[1b221e0]835{
836}
837
[5983eca]838static void twitter_rem_permit(struct im_connection *ic, char *who)
[1b221e0]839{
840}
841
[5983eca]842static void twitter_add_deny(struct im_connection *ic, char *who)
[1b221e0]843{
844}
845
[5983eca]846static void twitter_rem_deny(struct im_connection *ic, char *who)
[1b221e0]847{
848}
849
850//static char *twitter_set_display_name( set_t *set, char *value )
851//{
[5983eca]852//      return value;
[1b221e0]853//}
[7b87539]854
[5983eca]855static void twitter_buddy_data_add(struct bee_user *bu)
[203a2d2]856{
[5983eca]857        bu->data = g_new0(struct twitter_user_data, 1);
[203a2d2]858}
859
[5983eca]860static void twitter_buddy_data_free(struct bee_user *bu)
[203a2d2]861{
[5983eca]862        g_free(bu->data);
[203a2d2]863}
864
[2ca933c]865bee_user_t twitter_log_local_user;
866
[b61c74c]867/** Convert the given bitlbee tweet ID, bitlbee username, or twitter tweet ID
868 *  into a twitter tweet ID.
869 *
870 *  Returns 0 if the user provides garbage.
871 */
[5ebff60]872static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_)
873{
[3ca001b]874        struct twitter_data *td = ic->proto_data;
[b61c74c]875        struct twitter_user_data *tud;
[3ca001b]876        bee_user_t *bu = NULL;
[b61c74c]877        guint64 id = 0;
[5ebff60]878
879        if (bu_) {
[3ca001b]880                *bu_ = NULL;
[5ebff60]881        }
882        if (!arg || !arg[0]) {
[3ca001b]883                return 0;
[5ebff60]884        }
885
[3ca001b]886        if (arg[0] != '#' && (bu = bee_user_by_handle(ic->bee, ic, arg))) {
[5ebff60]887                if ((tud = bu->data)) {
[3ca001b]888                        id = tud->last_id;
[5ebff60]889                }
[3ca001b]890        } else {
[5ebff60]891                if (arg[0] == '#') {
[3ca001b]892                        arg++;
[5ebff60]893                }
[73f0a01]894                if (parse_int64(arg, 16, &id) && id < TWITTER_LOG_LENGTH) {
[3ca001b]895                        bu = td->log[id].bu;
[b61c74c]896                        id = td->log[id].id;
[73f0a01]897                } else if (parse_int64(arg, 10, &id)) {
[3ca001b]898                        /* Allow normal tweet IDs as well; not a very useful
899                           feature but it's always been there. Just ignore
900                           very low IDs to avoid accidents. */
[5ebff60]901                        if (id < 1000000) {
[3ca001b]902                                id = 0;
[5ebff60]903                        }
[3ca001b]904                }
[b61c74c]905        }
[5ebff60]906        if (bu_) {
[c43146d]907                if (bu == &twitter_log_local_user) {
908                        /* HACK alert. There's no bee_user object for the local
909                         * user so just fake one for the few cmds that need it. */
910                        twitter_log_local_user.handle = td->user;
911                } else {
912                        /* Beware of dangling pointers! */
913                        if (!g_slist_find(ic->bee->users, bu)) {
914                                bu = NULL;
915                        }
916                }
[3ca001b]917                *bu_ = bu;
[5ebff60]918        }
[b61c74c]919        return id;
920}
921
[5983eca]922static void twitter_handle_command(struct im_connection *ic, char *message)
[7b87539]923{
924        struct twitter_data *td = ic->proto_data;
[15bc063]925        char *cmds, **cmd, *new = NULL;
[3ca001b]926        guint64 in_reply_to = 0, id;
927        gboolean allow_post =
[5ebff60]928                g_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0;
[3ca001b]929        bee_user_t *bu = NULL;
[5983eca]930
931        cmds = g_strdup(message);
[269580c]932        cmd = split_command_parts(cmds, 2);
[5983eca]933
934        if (cmd[0] == NULL) {
[3ca001b]935                goto eof;
936        } else if (!set_getbool(&ic->acc->set, "commands") && allow_post) {
937                /* Not supporting commands if "commands" is set to true/strict. */
[5983eca]938        } else if (g_strcasecmp(cmd[0], "undo") == 0) {
[5ebff60]939                if (cmd[1] == NULL) {
[aa2f575]940                        twitter_status_destroy(ic, td->last_status_id);
[5ebff60]941                } else if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) {
[5983eca]942                        twitter_status_destroy(ic, id);
[5ebff60]943                } else {
[96dd574]944                        twitter_log(ic, "Could not undo last action");
[5ebff60]945                }
[5983eca]946
[3ca001b]947                goto eof;
[c203533]948        } else if ((g_strcasecmp(cmd[0], "favourite") == 0 ||
[5ebff60]949                    g_strcasecmp(cmd[0], "favorite") == 0 ||
950                    g_strcasecmp(cmd[0], "fav") == 0) && cmd[1]) {
[3ca001b]951                if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) {
[b61c74c]952                        twitter_favourite_tweet(ic, id);
953                } else {
[96dd574]954                        twitter_log(ic, "Please provide a message ID or username.");
[b61c74c]955                }
[3ca001b]956                goto eof;
[5983eca]957        } else if (g_strcasecmp(cmd[0], "follow") == 0 && cmd[1]) {
958                twitter_add_buddy(ic, cmd[1], NULL);
[3ca001b]959                goto eof;
[5983eca]960        } else if (g_strcasecmp(cmd[0], "unfollow") == 0 && cmd[1]) {
961                twitter_remove_buddy(ic, cmd[1], NULL);
[3ca001b]962                goto eof;
[d18dee42]963        } else if ((g_strcasecmp(cmd[0], "report") == 0 ||
964                    g_strcasecmp(cmd[0], "spam") == 0) && cmd[1]) {
[3ca001b]965                char *screen_name;
[5ebff60]966
[d18dee42]967                /* Report nominally works on users but look up the user who
968                   posted the given ID if the user wants to do it that way */
[3ca001b]969                twitter_message_id_from_command_arg(ic, cmd[1], &bu);
[5ebff60]970                if (bu) {
[3ca001b]971                        screen_name = bu->handle;
[5ebff60]972                } else {
[3ca001b]973                        screen_name = cmd[1];
[5ebff60]974                }
975
[d18dee42]976                twitter_report_spam(ic, screen_name);
[3ca001b]977                goto eof;
[5983eca]978        } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) {
[3ca001b]979                id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
[5983eca]980
[b890626]981                td->last_status_id = 0;
[5ebff60]982                if (id) {
[5983eca]983                        twitter_status_retweet(ic, id);
[5ebff60]984                } else {
[96dd574]985                        twitter_log(ic, "User `%s' does not exist or didn't "
[5ebff60]986                                    "post any statuses recently", cmd[1]);
987                }
[5983eca]988
[3ca001b]989                goto eof;
[5983eca]990        } else if (g_strcasecmp(cmd[0], "reply") == 0 && cmd[1] && cmd[2]) {
[3ca001b]991                id = twitter_message_id_from_command_arg(ic, cmd[1], &bu);
[5983eca]992                if (!id || !bu) {
[96dd574]993                        twitter_log(ic, "User `%s' does not exist or didn't "
[5ebff60]994                                    "post any statuses recently", cmd[1]);
[3ca001b]995                        goto eof;
[15bc063]996                }
[269580c]997                message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]);
[15bc063]998                in_reply_to = id;
[3ca001b]999                allow_post = TRUE;
[dfaa46d]1000        } else if (g_strcasecmp(cmd[0], "rawreply") == 0 && cmd[1] && cmd[2]) {
1001                id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
1002                if (!id) {
1003                        twitter_log(ic, "Tweet `%s' does not exist", cmd[1]);
1004                        goto eof;
1005                }
1006                message = cmd[2];
1007                in_reply_to = id;
1008                allow_post = TRUE;
[9ed8175]1009        } else if (g_strcasecmp(cmd[0], "url") == 0) {
1010                id = twitter_message_id_from_command_arg(ic, cmd[1], &bu);
1011                if (!id) {
1012                        twitter_log(ic, "Tweet `%s' does not exist", cmd[1]);
1013                } else {
[1201fcb]1014                        twitter_status_show_url(ic, id);
[9ed8175]1015                }
1016                goto eof;
1017
[5983eca]1018        } else if (g_strcasecmp(cmd[0], "post") == 0) {
[7b87539]1019                message += 5;
[3ca001b]1020                allow_post = TRUE;
[7b87539]1021        }
[5983eca]1022
[3ca001b]1023        if (allow_post) {
[15bc063]1024                char *s;
[5983eca]1025
[5ebff60]1026                if (!twitter_length_check(ic, message)) {
[3ca001b]1027                        goto eof;
[5ebff60]1028                }
[5983eca]1029
1030                s = cmd[0] + strlen(cmd[0]) - 1;
1031                if (!new && s > cmd[0] && (*s == ':' || *s == ',')) {
[7b87539]1032                        *s = '\0';
[5983eca]1033
1034                        if ((bu = bee_user_by_handle(ic->bee, ic, cmd[0]))) {
[b890626]1035                                struct twitter_user_data *tud = bu->data;
[5983eca]1036
1037                                new = g_strdup_printf("@%s %s", bu->handle,
[5ebff60]1038                                                      message + (s - cmd[0]) + 2);
[7b87539]1039                                message = new;
[5983eca]1040
1041                                if (time(NULL) < tud->last_time +
[5ebff60]1042                                    set_getint(&ic->acc->set, "auto_reply_timeout")) {
[b890626]1043                                        in_reply_to = tud->last_id;
[5ebff60]1044                                }
[7b87539]1045                        }
1046                }
[5983eca]1047
[b890626]1048                /* If the user runs undo between this request and its response
1049                   this would delete the second-last Tweet. Prevent that. */
1050                td->last_status_id = 0;
[5983eca]1051                twitter_post_status(ic, message, in_reply_to);
[3ca001b]1052        } else {
1053                twitter_log(ic, "Unknown command: %s", cmd[0]);
[7b87539]1054        }
[3ca001b]1055eof:
1056        g_free(new);
[5983eca]1057        g_free(cmds);
[7b87539]1058}
[1b221e0]1059
[5ebff60]1060void twitter_log(struct im_connection *ic, char *format, ...)
[96dd574]1061{
1062        struct twitter_data *td = ic->proto_data;
1063        va_list params;
1064        char *text;
[5ebff60]1065
[96dd574]1066        va_start(params, format);
1067        text = g_strdup_vprintf(format, params);
1068        va_end(params);
[5ebff60]1069
1070        if (td->timeline_gc) {
[96dd574]1071                imcb_chat_log(td->timeline_gc, "%s", text);
[5ebff60]1072        } else {
[96dd574]1073                imcb_log(ic, "%s", text);
[5ebff60]1074        }
1075
[96dd574]1076        g_free(text);
1077}
1078
1079
[1b221e0]1080void twitter_initmodule()
1081{
1082        struct prpl *ret = g_new0(struct prpl, 1);
[5983eca]1083
[1dd3470]1084        ret->options = OPT_NOOTR;
[1b221e0]1085        ret->name = "twitter";
1086        ret->login = twitter_login;
1087        ret->init = twitter_init;
1088        ret->logout = twitter_logout;
1089        ret->buddy_msg = twitter_buddy_msg;
1090        ret->get_info = twitter_get_info;
1091        ret->add_buddy = twitter_add_buddy;
1092        ret->remove_buddy = twitter_remove_buddy;
1093        ret->chat_msg = twitter_chat_msg;
1094        ret->chat_invite = twitter_chat_invite;
[73ee390]1095        ret->chat_join = twitter_chat_join;
[1b221e0]1096        ret->chat_leave = twitter_chat_leave;
1097        ret->keepalive = twitter_keepalive;
1098        ret->add_permit = twitter_add_permit;
1099        ret->rem_permit = twitter_rem_permit;
1100        ret->add_deny = twitter_add_deny;
1101        ret->rem_deny = twitter_rem_deny;
[203a2d2]1102        ret->buddy_data_add = twitter_buddy_data_add;
1103        ret->buddy_data_free = twitter_buddy_data_free;
[1b221e0]1104        ret->handle_cmp = g_strcasecmp;
[5983eca]1105
[ffcdf13]1106        register_protocol(ret);
[1b221e0]1107
[ffcdf13]1108        /* And an identi.ca variant: */
1109        ret = g_memdup(ret, sizeof(struct prpl));
1110        ret->name = "identica";
[1b221e0]1111        register_protocol(ret);
1112}
Note: See TracBrowser for help on using the repository browser.