finch/plugins/gnttinyurl/gnttinyurl.c

changeset 42678
0b9b81b6ff18
parent 42677
66b49e545c53
child 42679
192a8112562f
equal deleted inserted replaced
42677:66b49e545c53 42678:0b9b81b6ff18
1 /**
2 * Copyright (C) 2009 Richard Nelson <wabz@whatsbeef.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
17 */
18
19 #include <glib/gi18n-lib.h>
20
21 #include <glib.h>
22 #include <libsoup/soup.h>
23
24 #define PREFS_SCHEMA "im.pidgin.Finch.plugin.TinyURL"
25 #define PREF_LENGTH "length"
26 #define PREF_URL "url"
27
28 #include <purple.h>
29
30 #include <gnt.h>
31
32 #include <finch.h>
33
34
35 static int tag_num = 0;
36 static SoupSession *session = NULL;
37 static GHashTable *tinyurl_cache = NULL;
38
39 typedef struct {
40 SoupMessage *msg;
41 gchar *original_url;
42 PurpleConversation *conv;
43 gchar *tag;
44 int num;
45 } CbInfo;
46
47 static void process_urls(PurpleConversation *conv, GList *urls);
48
49 /* 3 functions from util.c */
50 static gboolean
51 badchar(char c)
52 {
53 switch (c) {
54 case ' ':
55 case ',':
56 case '\0':
57 case '\n':
58 case '\r':
59 case '<':
60 case '>':
61 case '"':
62 case '\'':
63 return TRUE;
64 default:
65 return FALSE;
66 }
67 }
68
69 static gboolean
70 badentity(const char *c)
71 {
72 if (!g_ascii_strncasecmp(c, "&lt;", 4) ||
73 !g_ascii_strncasecmp(c, "&gt;", 4) ||
74 !g_ascii_strncasecmp(c, "&quot;", 6)) {
75 return TRUE;
76 }
77 return FALSE;
78 }
79
80 static GList *extract_urls(const char *text)
81 {
82 const char *t, *c, *q = NULL;
83 char *url_buf;
84 GList *ret = NULL;
85 gboolean inside_html = FALSE;
86 int inside_paren = 0;
87 c = text;
88 while (*c) {
89 if (*c == '(' && !inside_html) {
90 inside_paren++;
91 c++;
92 }
93 if (inside_html) {
94 if (*c == '>') {
95 inside_html = FALSE;
96 } else if (!q && (*c == '\"' || *c == '\'')) {
97 q = c;
98 } else if(q) {
99 if(*c == *q)
100 q = NULL;
101 }
102 } else if (*c == '<') {
103 inside_html = TRUE;
104 if (!g_ascii_strncasecmp(c, "<A", 2)) {
105 while (1) {
106 if (*c == '>') {
107 inside_html = FALSE;
108 break;
109 }
110 c++;
111 if (!(*c))
112 break;
113 }
114 }
115 } else if ((*c=='h') && (!g_ascii_strncasecmp(c, "http://", 7) ||
116 (!g_ascii_strncasecmp(c, "https://", 8)))) {
117 t = c;
118 while (1) {
119 if (badchar(*t) || badentity(t)) {
120
121 if ((!g_ascii_strncasecmp(c, "http://", 7) && (t - c == 7)) ||
122 (!g_ascii_strncasecmp(c, "https://", 8) && (t - c == 8))) {
123 break;
124 }
125
126 if (*(t) == ',' && (*(t + 1) != ' ')) {
127 t++;
128 continue;
129 }
130
131 if (*(t - 1) == '.')
132 t--;
133 if ((*(t - 1) == ')' && (inside_paren > 0))) {
134 t--;
135 }
136
137 url_buf = g_strndup(c, t - c);
138 if (!g_list_find_custom(ret, url_buf, (GCompareFunc)strcmp)) {
139 purple_debug_info("TinyURL", "Added URL %s\n", url_buf);
140 ret = g_list_append(ret, url_buf);
141 } else {
142 g_free(url_buf);
143 }
144 c = t;
145 break;
146 }
147 t++;
148
149 }
150 } else if (!g_ascii_strncasecmp(c, "www.", 4) && (c == text || badchar(c[-1]) || badentity(c-1))) {
151 if (c[4] != '.') {
152 t = c;
153 while (1) {
154 if (badchar(*t) || badentity(t)) {
155 if (t - c == 4) {
156 break;
157 }
158
159 if (*(t) == ',' && (*(t + 1) != ' ')) {
160 t++;
161 continue;
162 }
163
164 if (*(t - 1) == '.')
165 t--;
166 if ((*(t - 1) == ')' && (inside_paren > 0))) {
167 t--;
168 }
169 url_buf = g_strndup(c, t - c);
170 if (!g_list_find_custom(ret, url_buf, (GCompareFunc)strcmp)) {
171 purple_debug_info("TinyURL", "Added URL %s\n", url_buf);
172 ret = g_list_append(ret, url_buf);
173 } else {
174 g_free(url_buf);
175 }
176 c = t;
177 break;
178 }
179 t++;
180 }
181 }
182 }
183 if (*c == ')' && !inside_html) {
184 inside_paren--;
185 c++;
186 }
187 if (*c == 0)
188 break;
189 c++;
190 }
191 return ret;
192 }
193
194 static void
195 url_fetched(GObject *source, GAsyncResult *result, gpointer user_data) {
196 CbInfo *data = (CbInfo *)user_data;
197 PurpleConversation *conv = data->conv;
198 PurpleConversationManager *manager;
199 GList *convs;
200 GBytes *response_body = NULL;
201 const gchar *url;
202
203 manager = purple_conversation_manager_get_default();
204 convs = purple_conversation_manager_get_all(manager);
205
206 if(SOUP_STATUS_IS_SUCCESSFUL(soup_message_get_status(data->msg))) {
207 response_body = soup_session_send_and_read_finish(SOUP_SESSION(source),
208 result, NULL);
209 }
210 if (response_body != NULL) {
211 gchar *tmp = NULL;
212 gsize size = 0;
213
214 /* Ensure URL is NUL-terminated. */
215 url = g_bytes_get_data(response_body, &size);
216 tmp = g_strndup(url, size);
217 url = tmp;
218
219 g_hash_table_insert(tinyurl_cache, data->original_url, tmp);
220 g_bytes_unref(response_body);
221 } else {
222 url = _("Error while querying TinyURL");
223 g_free(data->original_url);
224 }
225
226 /* ensure the conversation still exists */
227 if (g_list_find(convs, conv)) {
228 FinchConv *fconv = FINCH_CONV(conv);
229 gchar *str = g_strdup_printf("[%d] %s", data->num, url);
230 GntTextView *tv = GNT_TEXT_VIEW(fconv->tv);
231 gnt_text_view_tag_change(tv, data->tag, str, FALSE);
232 g_free(str);
233 g_free(data->tag);
234 g_object_unref(data->msg);
235 g_free(data);
236 return;
237 }
238 g_free(data->tag);
239 g_object_unref(data->msg);
240 g_free(data);
241 purple_debug_info("TinyURL", "Conversation no longer exists... :(\n");
242 }
243
244 static gboolean
245 writing_msg(PurpleConversation *conv, PurpleMessage *msg,
246 G_GNUC_UNUSED gpointer data)
247 {
248 GString *t;
249 GList *iter, *urls, *next;
250 int c = 0;
251 GSettings *settings = NULL;
252 gint min_url_length;
253
254 if (purple_message_get_flags(msg) & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_INVISIBLE))
255 return FALSE;
256
257 urls = g_object_get_data(G_OBJECT(conv), "TinyURLs");
258 g_list_free_full(urls, g_free);
259 urls = extract_urls(purple_message_get_contents(msg));
260 if (!urls)
261 return FALSE;
262
263 settings = g_settings_new_with_backend(PREFS_SCHEMA,
264 purple_core_get_settings_backend());
265 min_url_length = g_settings_get_int(settings, PREF_LENGTH);
266 g_object_unref(settings);
267
268 t = g_string_new(g_strdup(purple_message_get_contents(msg)));
269 for (iter = urls; iter; iter = next) {
270 next = iter->next;
271 if (g_utf8_strlen((char *)iter->data, -1) >= min_url_length) {
272 int pos, x = 0;
273 gchar *j, *s, *str, *orig;
274 glong len = g_utf8_strlen(iter->data, -1);
275 s = g_strdup(t->str);
276 orig = s;
277 str = g_strdup_printf("[%d]", ++c);
278 while ((j = strstr(s, iter->data))) { /* replace all occurrences */
279 pos = j - orig + (x++ * 3);
280 s = j + len;
281 t = g_string_insert(t, pos + len, str);
282 if (*s == '\0') break;
283 }
284 g_free(orig);
285 g_free(str);
286 continue;
287 } else {
288 g_free(iter->data);
289 urls = g_list_delete_link(urls, iter);
290 }
291 }
292 purple_message_set_contents(msg, t->str);
293 g_string_free(t, TRUE);
294 if (conv != NULL)
295 g_object_set_data(G_OBJECT(conv), "TinyURLs", urls);
296 return FALSE;
297 }
298
299 static void
300 wrote_msg(PurpleConversation *conv, PurpleMessage *pmsg,
301 G_GNUC_UNUSED gpointer _unused)
302 {
303 GList *urls;
304
305 if (purple_message_get_flags(pmsg) & PURPLE_MESSAGE_SEND)
306 return;
307
308 urls = g_object_get_data(G_OBJECT(conv), "TinyURLs");
309 if (urls == NULL)
310 return;
311
312 process_urls(conv, urls);
313 g_object_set_data(G_OBJECT(conv), "TinyURLs", NULL);
314 }
315
316 /* Frees 'urls' */
317 static void
318 process_urls(PurpleConversation *conv, GList *urls)
319 {
320 GList *iter;
321 int c;
322 FinchConv *fconv = FINCH_CONV(conv);
323 GntTextView *tv = GNT_TEXT_VIEW(fconv->tv);
324 GSettings *settings = NULL;
325 gchar *tinyurl_prefix = NULL;
326
327 settings = g_settings_new_with_backend(PREFS_SCHEMA,
328 purple_core_get_settings_backend());
329 tinyurl_prefix = g_settings_get_string(settings, PREF_URL);
330 g_object_unref(settings);
331
332 for (iter = urls, c = 1; iter; iter = iter->next, c++) {
333 int i;
334 SoupMessage *msg;
335 CbInfo *cbdata;
336 gchar *url;
337 gchar *original_url;
338 const gchar *tiny_url;
339
340 i = gnt_text_view_get_lines_below(tv);
341
342 original_url = purple_unescape_html((char *)iter->data);
343 tiny_url = g_hash_table_lookup(tinyurl_cache, original_url);
344 if (tiny_url) {
345 gchar *str = g_strdup_printf("\n[%d] %s", c, tiny_url);
346
347 g_free(original_url);
348 gnt_text_view_append_text_with_flags(tv, str, GNT_TEXT_FLAG_DIM);
349 if (i == 0)
350 gnt_text_view_scroll(tv, 0);
351 g_free(str);
352 continue;
353 }
354 cbdata = g_new(CbInfo, 1);
355 cbdata->num = c;
356 cbdata->original_url = original_url;
357 cbdata->tag = g_strdup_printf("%s%d", "tiny_", tag_num++);
358 cbdata->conv = conv;
359 if (g_ascii_strncasecmp(original_url, "http://", 7) && g_ascii_strncasecmp(original_url, "https://", 8)) {
360 url = g_strdup_printf("%shttp%%3A%%2F%%2F%s", tinyurl_prefix,
361 purple_url_encode(original_url));
362 } else {
363 url = g_strdup_printf("%s%s", tinyurl_prefix,
364 purple_url_encode(original_url));
365 }
366 cbdata->msg = msg = soup_message_new("GET", url);
367 soup_session_send_and_read_async(session, msg, G_PRIORITY_DEFAULT,
368 NULL, url_fetched, cbdata);
369 gnt_text_view_append_text_with_tag((tv), _("\nFetching TinyURL..."),
370 GNT_TEXT_FLAG_DIM, cbdata->tag);
371 if (i == 0)
372 gnt_text_view_scroll(tv, 0);
373 g_free(iter->data);
374 g_free(url);
375 }
376 g_list_free(urls);
377 g_free(tinyurl_prefix);
378 }
379
380 static void
381 free_conv_urls(PurpleConversation *conv)
382 {
383 GList *urls = g_object_get_data(G_OBJECT(conv), "TinyURLs");
384 g_list_free_full(urls, g_free);
385 }
386
387 static void
388 tinyurl_notify_tinyuri(GntWidget *win, const gchar *url)
389 {
390 gchar *message;
391 GntWidget *label = g_object_get_data(G_OBJECT(win), "info-widget");
392
393 message = g_strdup_printf(_("TinyURL for above: %s"), url);
394 gnt_label_set_text(GNT_LABEL(label), message);
395 g_free(message);
396 }
397
398 static void
399 tinyurl_notify_fetch_cb(GObject *source, GAsyncResult *result, gpointer data)
400 {
401 SoupMessage *msg = data;
402 GntWidget *win = NULL;
403 GBytes *response_body = NULL;
404 const gchar *tmp = NULL;
405 gsize size = 0;
406 gchar *url = NULL;
407 const gchar *original_url = NULL;
408
409 if(SOUP_STATUS_IS_SUCCESSFUL(soup_message_get_status(msg))) {
410 response_body = soup_session_send_and_read_finish(SOUP_SESSION(source),
411 result, NULL);
412 }
413
414 if (response_body == NULL) {
415 g_object_unref(msg);
416 return;
417 }
418
419 win = g_object_get_data(G_OBJECT(msg), "gnttinyurl-window");
420 original_url = g_object_get_data(G_OBJECT(msg), "gnttinyurl-original");
421 tmp = g_bytes_get_data(response_body, &size);
422 url = g_strndup(tmp, size);
423 g_hash_table_insert(tinyurl_cache, g_strdup(original_url), url);
424
425 tinyurl_notify_tinyuri(win, url);
426
427 g_bytes_unref(response_body);
428 g_object_unref(msg);
429 }
430
431 static void *
432 tinyurl_notify_uri(const char *uri)
433 {
434 char *fullurl = NULL;
435 GntWidget *win;
436 GCancellable *cancellable = NULL;
437 SoupMessage *msg;
438 const gchar *tiny_url;
439 GSettings *settings = NULL;
440 gchar *tinyurl_prefix = NULL;
441
442 /* XXX: The following expects that finch_notify_message gets called. This
443 * may not always happen, e.g. when another plugin sets its own
444 * notify_message. So tread carefully. */
445 win = purple_notify_message(NULL, PURPLE_NOTIFY_MSG_INFO, _("URI"), uri,
446 _("Please wait while TinyURL fetches a shorter URL ..."), NULL, NULL, NULL);
447 if (!GNT_IS_WINDOW(win) || !g_object_get_data(G_OBJECT(win), "info-widget"))
448 return win;
449
450 tiny_url = g_hash_table_lookup(tinyurl_cache, uri);
451 if (tiny_url) {
452 tinyurl_notify_tinyuri(win, tiny_url);
453 return win;
454 }
455
456 settings = g_settings_new_with_backend(PREFS_SCHEMA,
457 purple_core_get_settings_backend());
458 tinyurl_prefix = g_settings_get_string(settings, PREF_URL);
459 g_object_unref(settings);
460
461 if (g_ascii_strncasecmp(uri, "http://", 7) && g_ascii_strncasecmp(uri, "https://", 8)) {
462 fullurl = g_strdup_printf("%shttp%%3A%%2F%%2F%s", tinyurl_prefix,
463 purple_url_encode(uri));
464 } else {
465 fullurl = g_strdup_printf("%s%s", tinyurl_prefix,
466 purple_url_encode(uri));
467 }
468
469 g_free(tinyurl_prefix);
470
471 /* Make a cancellable and cancel it when the window is destroyed, so that
472 * the callback does not try to use a non-existent window.
473 */
474 cancellable = g_cancellable_new();
475 msg = soup_message_new("GET", fullurl);
476 g_object_set_data(G_OBJECT(msg), "gnttinyurl-window", win);
477 g_object_set_data_full(G_OBJECT(msg), "gnttinyurl-original", g_strdup(uri),
478 g_free);
479
480 soup_session_send_and_read_async(session, msg, G_PRIORITY_DEFAULT,
481 cancellable, tinyurl_notify_fetch_cb, msg);
482 g_signal_connect_object(win, "destroy", G_CALLBACK(g_cancellable_cancel),
483 cancellable, G_CONNECT_SWAPPED);
484 g_free(fullurl);
485 g_object_unref(cancellable);
486
487 return win;
488 }
489
490 static GPluginPluginInfo *
491 tiny_url_query(G_GNUC_UNUSED GError **error) {
492 const gchar * const authors[] = {
493 "Richard Nelson <wabz@whatsbeef.net>",
494 NULL
495 };
496
497 return finch_plugin_info_new(
498 "id", "TinyURL",
499 "name", N_("TinyURL"),
500 "version", DISPLAY_VERSION,
501 "category", N_("Utility"),
502 "summary", N_("TinyURL plugin"),
503 "description", N_("When receiving a message with URL(s), "
504 "use TinyURL for easier copying"),
505 "authors", authors,
506 "website", PURPLE_WEBSITE,
507 "abi-version", PURPLE_ABI_VERSION,
508 "settings-schema", PREFS_SCHEMA,
509 NULL
510 );
511 }
512
513 static gboolean
514 tiny_url_load(GPluginPlugin *plugin, G_GNUC_UNUSED GError **error) {
515 PurpleNotifyUiOps *ops = purple_notify_get_ui_ops();
516
517 session = soup_session_new();
518
519 g_object_set_data(G_OBJECT(plugin), "notify-uri", ops->notify_uri);
520 ops->notify_uri = tinyurl_notify_uri;
521
522 tinyurl_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
523 g_free, g_free);
524
525 purple_signal_connect(purple_conversations_get_handle(),
526 "wrote-im-msg",
527 plugin, G_CALLBACK(wrote_msg), NULL);
528 purple_signal_connect(purple_conversations_get_handle(),
529 "wrote-chat-msg",
530 plugin, G_CALLBACK(wrote_msg), NULL);
531 purple_signal_connect(purple_conversations_get_handle(),
532 "writing-im-msg",
533 plugin, G_CALLBACK(writing_msg), NULL);
534 purple_signal_connect(purple_conversations_get_handle(),
535 "writing-chat-msg",
536 plugin, G_CALLBACK(writing_msg), NULL);
537 purple_signal_connect(purple_conversations_get_handle(),
538 "deleting-conversation",
539 plugin, G_CALLBACK(free_conv_urls), NULL);
540
541 return TRUE;
542 }
543
544 static gboolean
545 tiny_url_unload(GPluginPlugin *plugin, G_GNUC_UNUSED gboolean shutdown,
546 G_GNUC_UNUSED GError **error)
547 {
548 PurpleNotifyUiOps *ops = purple_notify_get_ui_ops();
549 if (ops->notify_uri == tinyurl_notify_uri)
550 ops->notify_uri = g_object_get_data(G_OBJECT(plugin), "notify-uri");
551
552 soup_session_abort(session);
553 g_clear_object(&session);
554
555 g_clear_pointer(&tinyurl_cache, g_hash_table_destroy);
556
557 return TRUE;
558 }
559
560 GPLUGIN_NATIVE_PLUGIN_DECLARE(tiny_url)

mercurial