1 /*
2  * Pidgin-libnotify - Provides a libnotify interface for Pidgin
3  * Copyright (C) 2005-2007 Duarte Henriques
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19 
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23 
24 #include "gln_intl.h"
25 
26 #ifndef PURPLE_PLUGINS
27 #define PURPLE_PLUGINS
28 #endif
29 
30 #include <pidgin.h>
31 #include <version.h>
32 #include <debug.h>
33 #include <util.h>
34 #include <privacy.h>
35 
36 /* for pidgin_create_prpl_icon */
37 #include <gtkutils.h>
38 
39 #include <libnotify/notify.h>
40 
41 #include <string.h>
42 
43 #define PLUGIN_ID "pidgin-libnotify"
44 
45 static GHashTable *buddy_hash;
46 
47 static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin * plugin)48 get_plugin_pref_frame (PurplePlugin *plugin)
49 {
50 	PurplePluginPrefFrame *frame;
51 	PurplePluginPref *ppref;
52 
53 	frame = purple_plugin_pref_frame_new ();
54 
55 	ppref = purple_plugin_pref_new_with_name_and_label (
56                             "/plugins/gtk/libnotify/newmsg",
57                             _("New messages"));
58 	purple_plugin_pref_frame_add (frame, ppref);
59 
60 	ppref = purple_plugin_pref_new_with_name_and_label (
61                             "/plugins/gtk/libnotify/newconvonly",
62                             _("Only new conversations"));
63 	purple_plugin_pref_frame_add (frame, ppref);
64 
65 	ppref = purple_plugin_pref_new_with_name_and_label (
66                             "/plugins/gtk/libnotify/blocked",
67                             _("Ignore events from blocked users"));
68 	purple_plugin_pref_frame_add (frame, ppref);
69 
70 	ppref = purple_plugin_pref_new_with_name_and_label (
71                             "/plugins/gtk/libnotify/signon",
72                             _("Buddy signs on"));
73 	purple_plugin_pref_frame_add (frame, ppref);
74 
75 	ppref = purple_plugin_pref_new_with_name_and_label (
76                             "/plugins/gtk/libnotify/signoff",
77                             _("Buddy signs off"));
78 	purple_plugin_pref_frame_add (frame, ppref);
79 
80 	ppref = purple_plugin_pref_new_with_name_and_label (
81                             "/plugins/gtk/libnotify/only_available",
82                             _("Only when available"));
83 	purple_plugin_pref_frame_add (frame, ppref);
84 
85 	return frame;
86 }
87 
88 /* Signon flood be gone! - thanks to the guifications devs */
89 static GList *just_signed_on_accounts = NULL;
90 
91 static gboolean
event_connection_throttle_cb(gpointer data)92 event_connection_throttle_cb (gpointer data)
93 {
94 	PurpleAccount *account;
95 
96 	account = (PurpleAccount *)data;
97 
98 	if (!account)
99 		return FALSE;
100 
101 	if (!purple_account_get_connection (account)) {
102 		just_signed_on_accounts = g_list_remove (just_signed_on_accounts, account);
103 		return FALSE;
104 	}
105 
106 	if (!purple_account_is_connected (account))
107 		return TRUE;
108 
109 	just_signed_on_accounts = g_list_remove (just_signed_on_accounts, account);
110 	return FALSE;
111 }
112 
113 static void
event_connection_throttle(PurpleConnection * conn,gpointer data)114 event_connection_throttle (PurpleConnection *conn, gpointer data)
115 {
116 	PurpleAccount *account;
117 
118 	/* TODO: this function gets called after buddy signs on for GTalk
119 	   users who have themselves as a buddy */
120 	purple_debug_info (PLUGIN_ID, "event_connection_throttle() called\n");
121 
122 	if (!conn)
123 		return;
124 
125 	account = purple_connection_get_account(conn);
126 	if (!account)
127 		return;
128 
129 	just_signed_on_accounts = g_list_prepend (just_signed_on_accounts, account);
130 	g_timeout_add (5000, event_connection_throttle_cb, (gpointer)account);
131 }
132 
133 /* do NOT g_free() the string returned by this function */
134 static gchar *
best_name(PurpleBuddy * buddy)135 best_name (PurpleBuddy *buddy)
136 {
137 	if (buddy->alias) {
138 		return buddy->alias;
139 	} else if (buddy->server_alias) {
140 		return buddy->server_alias;
141 	} else {
142 		return buddy->name;
143 	}
144 }
145 
146 static GdkPixbuf *
pixbuf_from_buddy_icon(PurpleBuddyIcon * buddy_icon)147 pixbuf_from_buddy_icon (PurpleBuddyIcon *buddy_icon)
148 {
149 	GdkPixbuf *icon;
150 	const guchar *data;
151 	size_t len;
152 	GdkPixbufLoader *loader;
153 
154 	data = purple_buddy_icon_get_data (buddy_icon, &len);
155 
156 	loader = gdk_pixbuf_loader_new ();
157 	gdk_pixbuf_loader_set_size (loader, 48, 48);
158 	gdk_pixbuf_loader_write (loader, data, len, NULL);
159 	gdk_pixbuf_loader_close (loader, NULL);
160 
161 	icon = gdk_pixbuf_loader_get_pixbuf (loader);
162 
163 	if (icon) {
164 		g_object_ref (icon);
165 	}
166 
167 	g_object_unref (loader);
168 
169 	return icon;
170 }
171 
172 static void
action_cb(NotifyNotification * notification,gchar * action,gpointer user_data)173 action_cb (NotifyNotification *notification,
174 		   gchar *action, gpointer user_data)
175 {
176 	PurpleBuddy *buddy = NULL;
177 	PurpleConversation *conv = NULL;
178 
179 	purple_debug_info (PLUGIN_ID, "action_cb(), "
180 					"notification: 0x%x, action: '%s'", notification, action);
181 
182 	buddy = (PurpleBuddy *)g_object_get_data (G_OBJECT(notification), "buddy");
183 
184 	if (!buddy) {
185 		purple_debug_warning (PLUGIN_ID, "Got no buddy!");
186 		return;
187 	}
188 
189 	conv = purple_find_conversation_with_account (PURPLE_CONV_TYPE_ANY, buddy->name, buddy->account);
190 
191 	if (!conv) {
192 		conv = purple_conversation_new (PURPLE_CONV_TYPE_IM,
193 									  buddy->account,
194 									  buddy->name);
195 	}
196 	conv->ui_ops->present (conv);
197 
198 	notify_notification_close (notification, NULL);
199 }
200 
201 static gboolean
closed_cb(NotifyNotification * notification)202 closed_cb (NotifyNotification *notification)
203 {
204 	PurpleContact *contact;
205 
206 	purple_debug_info (PLUGIN_ID, "closed_cb(), notification: 0x%x\n", notification);
207 
208 	contact = (PurpleContact *)g_object_get_data (G_OBJECT(notification), "contact");
209 	if (contact)
210 		g_hash_table_remove (buddy_hash, contact);
211 
212 	g_object_unref (G_OBJECT(notification));
213 
214 	return FALSE;
215 }
216 
217 /* you must g_free the returned string
218  * num_chars is utf-8 characters */
219 static gchar *
truncate_escape_string(const gchar * str,int num_chars)220 truncate_escape_string (const gchar *str,
221 						int num_chars)
222 {
223 	gchar *escaped_str;
224 
225 	if (g_utf8_strlen (str, num_chars*2+1) > num_chars) {
226 		gchar *truncated_str;
227 		gchar *str2;
228 
229 		/* allocate number of bytes and not number of utf-8 chars */
230 		str2 = g_malloc ((num_chars-1) * 2 * sizeof(gchar));
231 
232 		g_utf8_strncpy (str2, str, num_chars-2);
233 		truncated_str = g_strdup_printf ("%s..", str2);
234 		escaped_str = g_markup_escape_text (truncated_str, strlen (truncated_str));
235 		g_free (str2);
236 		g_free (truncated_str);
237 	} else {
238 		escaped_str = g_markup_escape_text (str, strlen (str));
239 	}
240 
241 	return escaped_str;
242 }
243 
244 static gboolean
should_notify_unavailable(PurpleAccount * account)245 should_notify_unavailable (PurpleAccount *account)
246 {
247 	PurpleStatus *status;
248 
249 	if (!purple_prefs_get_bool ("/plugins/gtk/libnotify/only_available"))
250 		return TRUE;
251 
252 	status = purple_account_get_active_status (account);
253 
254 	return purple_status_is_online (status) && purple_status_is_available (status);
255 }
256 
257 static void
notify(const gchar * title,const gchar * body,PurpleBuddy * buddy)258 notify (const gchar *title,
259 		const gchar *body,
260 		PurpleBuddy *buddy)
261 {
262 	NotifyNotification *notification = NULL;
263 	GdkPixbuf *icon;
264 	PurpleBuddyIcon *buddy_icon;
265 	gchar *tr_body;
266 	PurpleContact *contact;
267 
268 	contact = purple_buddy_get_contact (buddy);
269 
270 	if (body)
271 		tr_body = truncate_escape_string (body, 60);
272 	else
273 		tr_body = NULL;
274 
275 	notification = g_hash_table_lookup (buddy_hash, contact);
276 
277 	if (notification != NULL) {
278 		notify_notification_update (notification, title, tr_body, NULL);
279 		/* this shouldn't be necessary, file a bug */
280 		notify_notification_show (notification, NULL);
281 
282 		purple_debug_info (PLUGIN_ID, "notify(), update: "
283 						 "title: '%s', body: '%s', buddy: '%s'\n",
284 						 title, tr_body, best_name (buddy));
285 
286 		g_free (tr_body);
287 		return;
288 	}
289 	notification = notify_notification_new (title, tr_body, NULL);
290 	purple_debug_info (PLUGIN_ID, "notify(), new: "
291 					 "title: '%s', body: '%s', buddy: '%s'\n",
292 					 title, tr_body, best_name (buddy));
293 
294 	g_free (tr_body);
295 
296 	buddy_icon = purple_buddy_get_icon (buddy);
297 	if (buddy_icon) {
298 		icon = pixbuf_from_buddy_icon (buddy_icon);
299 		purple_debug_info (PLUGIN_ID, "notify(), has a buddy icon.\n");
300 	} else {
301 		icon = pidgin_create_prpl_icon (buddy->account, 1);
302 		purple_debug_info (PLUGIN_ID, "notify(), has a prpl icon.\n");
303 	}
304 
305 	if (icon) {
306 		notify_notification_set_icon_from_pixbuf (notification, icon);
307 		g_object_unref (icon);
308 	} else {
309 		purple_debug_warning (PLUGIN_ID, "notify(), couldn't find any icon!\n");
310 	}
311 
312 	g_hash_table_insert (buddy_hash, contact, notification);
313 
314 	g_object_set_data (G_OBJECT(notification), "contact", contact);
315 
316 	g_signal_connect (notification, "closed", G_CALLBACK(closed_cb), NULL);
317 
318 	notify_notification_set_urgency (notification, NOTIFY_URGENCY_NORMAL);
319 
320 	notify_notification_add_action (notification, "show", _("Show"), action_cb, NULL, NULL);
321 
322 	if (!notify_notification_show (notification, NULL)) {
323 		purple_debug_error (PLUGIN_ID, "notify(), failed to send notification\n");
324 	}
325 
326 }
327 
328 static void
notify_buddy_signon_cb(PurpleBuddy * buddy,gpointer data)329 notify_buddy_signon_cb (PurpleBuddy *buddy,
330 						gpointer data)
331 {
332 	gchar *tr_name, *title;
333 	gboolean blocked;
334 
335 	g_return_if_fail (buddy);
336 
337 	if (!purple_prefs_get_bool ("/plugins/gtk/libnotify/signon"))
338 		return;
339 
340 	if (g_list_find (just_signed_on_accounts, buddy->account))
341 		return;
342 
343 	blocked = purple_prefs_get_bool ("/plugins/gtk/libnotify/blocked");
344 	if (!purple_privacy_check (buddy->account, buddy->name) && blocked)
345 		return;
346 
347 	if (!should_notify_unavailable (purple_buddy_get_account (buddy)))
348 		return;
349 
350 	tr_name = truncate_escape_string (best_name (buddy), 25);
351 
352 	title = g_strdup_printf (_("%s signed on"), tr_name);
353 
354 	notify (title, NULL, buddy);
355 
356 	g_free (tr_name);
357 	g_free (title);
358 }
359 
360 static void
notify_buddy_signoff_cb(PurpleBuddy * buddy,gpointer data)361 notify_buddy_signoff_cb (PurpleBuddy *buddy,
362 						 gpointer data)
363 {
364 	gchar *tr_name, *title;
365 	gboolean blocked;
366 
367 	g_return_if_fail (buddy);
368 
369 	if (!purple_prefs_get_bool ("/plugins/gtk/libnotify/signoff"))
370 		return;
371 
372 	if (g_list_find (just_signed_on_accounts, buddy->account))
373 		return;
374 
375 	blocked = purple_prefs_get_bool ("/plugins/gtk/libnotify/blocked");
376 	if (!purple_privacy_check (buddy->account, buddy->name) && blocked)
377 		return;
378 
379 	if (!should_notify_unavailable (purple_buddy_get_account (buddy)))
380 		return;
381 
382 	tr_name = truncate_escape_string (best_name (buddy), 25);
383 
384 	title = g_strdup_printf (_("%s signed off"), tr_name);
385 
386 	notify (title, NULL, buddy);
387 
388 	g_free (tr_name);
389 	g_free (title);
390 }
391 
392 static void
notify_msg_sent(PurpleAccount * account,const gchar * sender,const gchar * message)393 notify_msg_sent (PurpleAccount *account,
394 				 const gchar *sender,
395 				 const gchar *message)
396 {
397 	PurpleBuddy *buddy;
398 	gchar *title, *body, *tr_name;
399 	gboolean blocked;
400 
401 	buddy = purple_find_buddy (account, sender);
402 	if (!buddy)
403 		return;
404 
405 	blocked = purple_prefs_get_bool ("/plugins/gtk/libnotify/blocked");
406 	if (!purple_privacy_check(account, sender) && blocked)
407 		return;
408 
409 	tr_name = truncate_escape_string (best_name (buddy), 25);
410 
411 	title = g_strdup_printf (_("%s says:"), tr_name);
412 	body = purple_markup_strip_html (message);
413 
414 	notify (title, body, buddy);
415 
416 	g_free (tr_name);
417 	g_free (title);
418 	g_free (body);
419 }
420 
421 static void
notify_new_message_cb(PurpleAccount * account,const gchar * sender,const gchar * message,int flags,gpointer data)422 notify_new_message_cb (PurpleAccount *account,
423 					   const gchar *sender,
424 					   const gchar *message,
425 					   int flags,
426 					   gpointer data)
427 {
428 	PurpleConversation *conv;
429 
430 	if (!purple_prefs_get_bool ("/plugins/gtk/libnotify/newmsg"))
431 		return;
432 
433 	conv = purple_find_conversation_with_account (PURPLE_CONV_TYPE_IM, sender, account);
434 
435 #ifndef DEBUG /* in debug mode, always show notifications */
436 	if (conv && purple_conversation_has_focus (conv)) {
437 		purple_debug_info (PLUGIN_ID, "Conversation has focus 0x%x\n", conv);
438 		return;
439 	}
440 #endif
441 
442 	if (conv && purple_prefs_get_bool ("/plugins/gtk/libnotify/newconvonly")) {
443 		purple_debug_info (PLUGIN_ID, "Conversation is not new 0x%x\n", conv);
444 		return;
445 	}
446 
447 	if (!should_notify_unavailable (account))
448 		return;
449 
450 	notify_msg_sent (account, sender, message);
451 }
452 
453 static void
notify_chat_nick(PurpleAccount * account,const gchar * sender,const gchar * message,PurpleConversation * conv,gpointer data)454 notify_chat_nick (PurpleAccount *account,
455 				  const gchar *sender,
456 				  const gchar *message,
457 				  PurpleConversation *conv,
458 				  gpointer data)
459 {
460 	gchar *nick;
461 
462 	nick = (gchar *)purple_conv_chat_get_nick (PURPLE_CONV_CHAT(conv));
463 	if (nick && !strcmp (sender, nick))
464 		return;
465 
466 	if (!g_strstr_len (message, strlen(message), nick))
467 		return;
468 
469 	notify_msg_sent (account, sender, message);
470 }
471 
472 static gboolean
plugin_load(PurplePlugin * plugin)473 plugin_load (PurplePlugin *plugin)
474 {
475 	void *conv_handle, *blist_handle, *conn_handle;
476 
477 	if (!notify_is_initted () && !notify_init ("Pidgin")) {
478 		purple_debug_error (PLUGIN_ID, "libnotify not running!\n");
479 		return FALSE;
480 	}
481 
482 	conv_handle = purple_conversations_get_handle ();
483 	blist_handle = purple_blist_get_handle ();
484 	conn_handle = purple_connections_get_handle();
485 
486 	buddy_hash = g_hash_table_new (NULL, NULL);
487 
488 	purple_signal_connect (blist_handle, "buddy-signed-on", plugin,
489 						PURPLE_CALLBACK(notify_buddy_signon_cb), NULL);
490 
491 	purple_signal_connect (blist_handle, "buddy-signed-off", plugin,
492 						PURPLE_CALLBACK(notify_buddy_signoff_cb), NULL);
493 
494 	purple_signal_connect (conv_handle, "received-im-msg", plugin,
495 						PURPLE_CALLBACK(notify_new_message_cb), NULL);
496 
497 	purple_signal_connect (conv_handle, "received-chat-msg", plugin,
498 						PURPLE_CALLBACK(notify_chat_nick), NULL);
499 
500 	/* used just to not display the flood of guifications we'd get */
501 	purple_signal_connect (conn_handle, "signed-on", plugin,
502 						PURPLE_CALLBACK(event_connection_throttle), NULL);
503 
504 	return TRUE;
505 }
506 
507 static gboolean
plugin_unload(PurplePlugin * plugin)508 plugin_unload (PurplePlugin *plugin)
509 {
510 	void *conv_handle, *blist_handle, *conn_handle;
511 
512 	conv_handle = purple_conversations_get_handle ();
513 	blist_handle = purple_blist_get_handle ();
514 	conn_handle = purple_connections_get_handle();
515 
516 	purple_signal_disconnect (blist_handle, "buddy-signed-on", plugin,
517 							PURPLE_CALLBACK(notify_buddy_signon_cb));
518 
519 	purple_signal_disconnect (blist_handle, "buddy-signed-off", plugin,
520 							PURPLE_CALLBACK(notify_buddy_signoff_cb));
521 
522 	purple_signal_disconnect (conv_handle, "received-im-msg", plugin,
523 							PURPLE_CALLBACK(notify_new_message_cb));
524 
525 	purple_signal_disconnect (conv_handle, "received-chat-msg", plugin,
526 							PURPLE_CALLBACK(notify_chat_nick));
527 
528 	purple_signal_disconnect (conn_handle, "signed-on", plugin,
529 							PURPLE_CALLBACK(event_connection_throttle));
530 
531 	g_hash_table_destroy (buddy_hash);
532 
533 	notify_uninit ();
534 
535 	return TRUE;
536 }
537 
538 static PurplePluginUiInfo prefs_info = {
539     get_plugin_pref_frame,
540     0,						/* page num (Reserved) */
541     NULL					/* frame (Reserved) */
542 };
543 
544 static PurplePluginInfo info = {
545     PURPLE_PLUGIN_MAGIC,										/* api version */
546     PURPLE_MAJOR_VERSION,
547     PURPLE_MINOR_VERSION,
548     PURPLE_PLUGIN_STANDARD,									/* type */
549     0,														/* ui requirement */
550     0,														/* flags */
551     NULL,													/* dependencies */
552     PURPLE_PRIORITY_DEFAULT,									/* priority */
553 
554     PLUGIN_ID,												/* id */
555     NULL,													/* name */
556     VERSION,												/* version */
557     NULL,													/* summary */
558     NULL,													/* description */
559 
560     "Duarte Henriques <duarte.henriques@gmail.com>",		/* author */
561     "http://sourceforge.net/projects/gaim-libnotify/",		/* homepage */
562 
563     plugin_load,			/* load */
564     plugin_unload,			/* unload */
565     NULL,					/* destroy */
566     NULL,					/* ui info */
567     NULL,					/* extra info */
568     &prefs_info				/* prefs info */
569 };
570 
571 static void
init_plugin(PurplePlugin * plugin)572 init_plugin (PurplePlugin *plugin)
573 {
574 	bindtextdomain (PACKAGE, LOCALEDIR);
575 	bind_textdomain_codeset (PACKAGE, "UTF-8");
576 
577 	info.name = _("Libnotify Popups");
578 	info.summary = _("Displays popups via libnotify.");
579 	info.description = _("Pidgin-libnotify:\nDisplays popups via libnotify.");
580 
581 	purple_prefs_add_none ("/plugins/gtk/libnotify");
582 	purple_prefs_add_bool ("/plugins/gtk/libnotify/newmsg", TRUE);
583 	purple_prefs_add_bool ("/plugins/gtk/libnotify/blocked", TRUE);
584 	purple_prefs_add_bool ("/plugins/gtk/libnotify/newconvonly", FALSE);
585 	purple_prefs_add_bool ("/plugins/gtk/libnotify/signon", TRUE);
586 	purple_prefs_add_bool ("/plugins/gtk/libnotify/signoff", FALSE);
587 	purple_prefs_add_bool ("/plugins/gtk/libnotify/only_available", FALSE);
588 }
589 
590 PURPLE_INIT_PLUGIN(notify, init_plugin, info)
591 
592