2 * Contact Availability Prediction plugin for Purple
4 * Copyright (C) 2006 Geoffrey Foster.
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of the
9 * License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 static void generate_prediction(CapStatistics
*statistics
) {
25 if(statistics
->buddy
) {
26 if(statistics
->prediction
== NULL
)
27 statistics
->prediction
= g_malloc(sizeof(CapPrediction
));
28 statistics
->prediction
->probability
= generate_prediction_for(statistics
->buddy
);
29 statistics
->prediction
->generated_at
= time(NULL
);
33 static double generate_prediction_for(PurpleBuddy
*buddy
) {
34 double prediction
= 1.0f
;
35 gboolean generated
= FALSE
;
36 gchar
*buddy_name
= buddy
->name
;
37 const gchar
*protocol_id
= purple_account_get_protocol_id(buddy
->account
);
38 const gchar
*account_id
= purple_account_get_username(buddy
->account
);
39 const gchar
*status_id
= purple_status_get_id(get_status_for(buddy
));
40 time_t t
= time(NULL
);
41 struct tm
*current_time
= localtime(&t
);
42 int current_minute
= current_time
->tm_min
+ current_time
->tm_hour
* 60;
43 int threshold
= purple_prefs_get_int("/plugins/gtk/cap/threshold");
44 int min_minute
= (current_minute
- threshold
) % 1440;
45 int max_minute
= (current_minute
+ threshold
) % 1440;
46 char *sql
, *sta_id
= NULL
;
47 sqlite3_stmt
*stmt
= NULL
;
48 const char *tail
= NULL
;
52 sql
= sqlite3_mprintf("select sum(success_count) as successes, sum(failed_count) as failures "
53 "from cap_msg_count where "
54 "buddy=%Q and account=%Q and protocol=%Q and minute_val>=%d and minute_val<=%d;",
55 buddy_name
, account_id
, protocol_id
, min_minute
, max_minute
);
56 rc
= sqlite3_prepare(_db
, sql
, -1, &stmt
, &tail
);
61 if(sqlite3_step(stmt
) == SQLITE_ROW
) {
62 successes
= sqlite3_column_int(stmt
, 0);
63 failures
= sqlite3_column_int(stmt
, 1);
64 if(failures
+ successes
> 0) {
65 prediction
*= ((double)successes
/((double)(successes
+failures
)));
69 sqlite3_finalize(stmt
);
74 sql
= sqlite3_mprintf("select sum(success_count) as successes, sum(failed_count) as failures "
75 "from cap_status_count where "
76 "buddy=%Q and account=%Q and protocol=%Q and status=%Q;",
77 buddy_name
, account_id
, protocol_id
, status_id
);
78 rc
= sqlite3_prepare(_db
, sql
, -1, &stmt
, &tail
);
83 if(sqlite3_step(stmt
) == SQLITE_ROW
) {
84 successes
= sqlite3_column_int(stmt
, 0);
85 failures
= sqlite3_column_int(stmt
, 1);
86 if(failures
+ successes
> 0) {
87 prediction
*= ((double)successes
/((double)(successes
+failures
)));
91 sqlite3_finalize(stmt
);
97 sta_id
= purple_status_get_id(get_status_for(buddy
));
99 if(purple_strequal(sta_id
, "offline")) {
100 /* This is kind of stupid, change it. */
101 if(prediction
== 1.0f
)
111 static CapStatistics
* get_stats_for(PurpleBuddy
*buddy
) {
112 CapStatistics
*stats
;
114 g_return_val_if_fail(buddy
!= NULL
, NULL
);
116 stats
= g_hash_table_lookup(_buddy_stats
, buddy
->name
);
118 stats
= g_malloc0(sizeof(CapStatistics
));
119 stats
->last_message
= -1;
120 stats
->buddy
= buddy
;
121 stats
->last_seen
= -1;
122 stats
->last_status_id
= "";
124 g_hash_table_insert(_buddy_stats
, g_strdup(buddy
->name
), stats
);
126 /* This may actually be a different PurpleBuddy than what is in stats.
127 * We replace stats->buddy to make sure we're looking at a valid pointer. */
128 stats
->buddy
= buddy
;
130 generate_prediction(stats
);
134 static void destroy_stats(gpointer data
) {
135 CapStatistics
*stats
= data
;
136 g_free(stats
->prediction
);
137 /* g_free(stats->hourly_usage); */
138 /* g_free(stats->daily_usage); */
139 if (stats
->timeout_source_id
!= 0)
140 purple_timeout_remove(stats
->timeout_source_id
);
145 insert_cap_msg_count_success(const char *buddy_name
, const char *account
, const char *protocol
, int minute
) {
149 char *sql_select
= sqlite3_mprintf("SELECT * FROM cap_msg_count WHERE "
150 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
151 buddy_name
, account
, protocol
, minute
);
152 char *sql_ins_up
= NULL
;
154 purple_debug_info("cap", "%s\n", sql_select
);
156 sqlite3_prepare(_db
, sql_select
, -1, &stmt
, &tail
);
158 rc
= sqlite3_step(stmt
);
160 if(rc
== SQLITE_DONE
) {
161 sql_ins_up
= sqlite3_mprintf("INSERT INTO cap_msg_count VALUES (%Q, %Q, %Q, %d, %d, %d);",
162 buddy_name
, account
, protocol
, minute
, 1, 0);
163 } else if(rc
== SQLITE_ROW
) {
164 sql_ins_up
= sqlite3_mprintf("UPDATE cap_msg_count SET success_count=success_count+1 WHERE "
165 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
166 buddy_name
, account
, protocol
, minute
);
168 purple_debug_info("cap", "%d\n", rc
);
169 sqlite3_finalize(stmt
);
170 sqlite3_free(sql_select
);
174 sqlite3_finalize(stmt
);
175 sqlite3_free(sql_select
);
177 sqlite3_exec(_db
, sql_ins_up
, NULL
, NULL
, NULL
);
178 sqlite3_free(sql_ins_up
);
182 insert_cap_status_count_success(const char *buddy_name
, const char *account
, const char *protocol
, const char *status_id
) {
186 char *sql_select
= sqlite3_mprintf("SELECT * FROM cap_status_count WHERE "
187 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
188 buddy_name
, account
, protocol
, status_id
);
189 char *sql_ins_up
= NULL
;
191 purple_debug_info("cap", "%s\n", sql_select
);
193 sqlite3_prepare(_db
, sql_select
, -1, &stmt
, &tail
);
195 rc
= sqlite3_step(stmt
);
197 if(rc
== SQLITE_DONE
) {
198 sql_ins_up
= sqlite3_mprintf("INSERT INTO cap_status_count VALUES (%Q, %Q, %Q, %Q, %d, %d);",
199 buddy_name
, account
, protocol
, status_id
, 1, 0);
200 } else if(rc
== SQLITE_ROW
) {
201 sql_ins_up
= sqlite3_mprintf("UPDATE cap_status_count SET success_count=success_count+1 WHERE "
202 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
203 buddy_name
, account
, protocol
, status_id
);
205 purple_debug_info("cap", "%d\n", rc
);
206 sqlite3_finalize(stmt
);
207 sqlite3_free(sql_select
);
211 sqlite3_finalize(stmt
);
212 sqlite3_free(sql_select
);
214 sqlite3_exec(_db
, sql_ins_up
, NULL
, NULL
, NULL
);
215 sqlite3_free(sql_ins_up
);
219 insert_cap_msg_count_failed(const char *buddy_name
, const char *account
, const char *protocol
, int minute
) {
223 char *sql_select
= sqlite3_mprintf("SELECT * FROM cap_msg_count WHERE "
224 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
225 buddy_name
, account
, protocol
, minute
);
226 char *sql_ins_up
= NULL
;
228 purple_debug_info("cap", "%s\n", sql_select
);
230 sqlite3_prepare(_db
, sql_select
, -1, &stmt
, &tail
);
232 rc
= sqlite3_step(stmt
);
234 if(rc
== SQLITE_DONE
) {
235 sql_ins_up
= sqlite3_mprintf("INSERT INTO cap_msg_count VALUES (%Q, %Q, %Q, %d, %d, %d);",
236 buddy_name
, account
, protocol
, minute
, 0, 1);
237 } else if(rc
== SQLITE_ROW
) {
238 sql_ins_up
= sqlite3_mprintf("UPDATE cap_msg_count SET failed_count=failed_count+1 WHERE "
239 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
240 buddy_name
, account
, protocol
, minute
);
242 purple_debug_info("cap", "%d\n", rc
);
243 sqlite3_finalize(stmt
);
244 sqlite3_free(sql_select
);
248 sqlite3_finalize(stmt
);
249 sqlite3_free(sql_select
);
251 sqlite3_exec(_db
, sql_ins_up
, NULL
, NULL
, NULL
);
252 sqlite3_free(sql_ins_up
);
256 insert_cap_status_count_failed(const char *buddy_name
, const char *account
, const char *protocol
, const char *status_id
) {
260 char *sql_select
= sqlite3_mprintf("SELECT * FROM cap_status_count WHERE "
261 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
262 buddy_name
, account
, protocol
, status_id
);
263 char *sql_ins_up
= NULL
;
265 purple_debug_info("cap", "%s\n", sql_select
);
267 sqlite3_prepare(_db
, sql_select
, -1, &stmt
, &tail
);
269 rc
= sqlite3_step(stmt
);
271 if(rc
== SQLITE_DONE
) {
272 sql_ins_up
= sqlite3_mprintf("INSERT INTO cap_status_count VALUES (%Q, %Q, %Q, %Q, %d, %d);",
273 buddy_name
, account
, protocol
, status_id
, 0, 1);
274 } else if(rc
== SQLITE_ROW
) {
275 sql_ins_up
= sqlite3_mprintf("UPDATE cap_status_count SET failed_count=failed_count+1 WHERE "
276 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
277 buddy_name
, account
, protocol
, status_id
);
279 purple_debug_info("cap", "%d\n", rc
);
280 sqlite3_finalize(stmt
);
281 sqlite3_free(sql_select
);
285 sqlite3_finalize(stmt
);
286 sqlite3_free(sql_select
);
288 sqlite3_exec(_db
, sql_ins_up
, NULL
, NULL
, NULL
);
289 sqlite3_free(sql_ins_up
);
292 static void insert_cap_success(CapStatistics
*stats
) {
293 gchar
*buddy_name
= stats
->buddy
->name
;
294 const gchar
*protocol_id
= purple_account_get_protocol_id(stats
->buddy
->account
);
295 const gchar
*account_id
= purple_account_get_username(stats
->buddy
->account
);
296 const gchar
*status_id
= (stats
->last_message_status_id
) ?
297 stats
->last_message_status_id
:
298 purple_status_get_id(get_status_for(stats
->buddy
));
299 struct tm
*current_time
;
302 if(stats
->last_message
== -1) {
303 time_t now
= time(NULL
);
304 current_time
= localtime(&now
);
306 current_time
= localtime(&stats
->last_message
);
308 minute
= current_time
->tm_min
+ current_time
->tm_hour
* 60;
310 insert_cap_msg_count_success(buddy_name
, account_id
, protocol_id
, minute
);
312 insert_cap_status_count_success(buddy_name
, account_id
, protocol_id
, status_id
);
314 stats
->last_message
= -1;
315 stats
->last_message_status_id
= NULL
;
318 static void insert_cap_failure(CapStatistics
*stats
) {
319 gchar
*buddy_name
= stats
->buddy
->name
;
320 const gchar
*protocol_id
= purple_account_get_protocol_id(stats
->buddy
->account
);
321 const gchar
*account_id
= purple_account_get_username(stats
->buddy
->account
);
322 const gchar
*status_id
= (stats
->last_message_status_id
) ?
323 stats
->last_message_status_id
:
324 purple_status_get_id(get_status_for(stats
->buddy
));
325 struct tm
*current_time
= localtime(&stats
->last_message
);
326 int minute
= current_time
->tm_min
+ current_time
->tm_hour
* 60;
328 insert_cap_msg_count_failed(buddy_name
, account_id
, protocol_id
, minute
);
330 insert_cap_status_count_failed(buddy_name
, account_id
, protocol_id
, status_id
);
332 stats
->last_message
= -1;
333 stats
->last_message_status_id
= NULL
;
336 static gboolean
max_message_difference_cb(gpointer data
) {
337 CapStatistics
*stats
= data
;
338 purple_debug_info("cap", "Max Message Difference timeout occurred\n");
339 insert_cap_failure(stats
);
340 stats
->timeout_source_id
= 0;
344 /* Purple Signal Handlers */
347 static void sent_im_msg(PurpleAccount
*account
, const char *receiver
, const char *message
) {
349 guint interval
, words
;
350 CapStatistics
*stats
= NULL
;
352 buddy
= purple_find_buddy(account
, receiver
);
357 interval
= purple_prefs_get_int("/plugins/gtk/cap/max_msg_difference") * 60;
358 words
= word_count(message
);
360 stats
= get_stats_for(buddy
);
362 insert_word_count(purple_account_get_username(account
), receiver
, words
);
363 stats
->last_message
= time(NULL
);
364 stats
->last_message_status_id
= purple_status_get_id(get_status_for(buddy
));
365 if(stats
->timeout_source_id
!= 0)
366 purple_timeout_remove(stats
->timeout_source_id
);
368 stats
->timeout_source_id
= purple_timeout_add_seconds(interval
, max_message_difference_cb
, stats
);
371 /* received-im-msg */
373 received_im_msg(PurpleAccount
*account
, char *sender
, char *message
, PurpleConversation
*conv
, PurpleMessageFlags flags
) {
375 CapStatistics
*stats
;
376 /* guint words = word_count(message); */
378 if (flags
& PURPLE_MESSAGE_AUTO_RESP
)
381 buddy
= purple_find_buddy(account
, sender
);
386 stats
= get_stats_for(buddy
);
388 /* insert_word_count(sender, buddy_name, words); */
390 /* If we are waiting for a response from a prior message
391 * then cancel the timeout callback. */
392 if(stats
->timeout_source_id
!= 0) {
393 purple_debug_info("cap", "Cancelling timeout callback\n");
394 purple_timeout_remove(stats
->timeout_source_id
);
395 stats
->timeout_source_id
= 0;
398 insert_cap_success(stats
);
400 /* Reset the last_message value */
401 stats
->last_message
= -1;
402 /* Reset the last status id value */
403 stats
->last_message_status_id
= NULL
;
406 /* buddy-status-changed */
407 static void buddy_status_changed(PurpleBuddy
*buddy
, PurpleStatus
*old_status
, PurpleStatus
*status
) {
408 CapStatistics
*stats
= get_stats_for(buddy
);
409 insert_status_change_from_purple_status(stats
, status
);
412 /* buddy-signed-on */
413 static void buddy_signed_on(PurpleBuddy
*buddy
) {
414 CapStatistics
*stats
= get_stats_for(buddy
);
416 /* If the statistic object existed but doesn't have a buddy pointer associated
417 * with it then reassociate one with it. The pointer being null is a result
418 * of a buddy with existing stats signing off and Purple sticking around. */
420 stats
->buddy
= buddy
;
423 insert_status_change(stats
);
426 /* buddy-signed-off */
427 static void buddy_signed_off(PurpleBuddy
*buddy
) {
428 CapStatistics
*stats
= get_stats_for(buddy
);
430 /* We don't necessarily want to delete a buddies generated statistics every time they go offline.
431 * Instead we just set the buddy pointer to null so that when they come back online we can look
432 * them up again and continue using their statistics.
434 insert_status_change(stats
);
435 /* stats->buddy = NULL; */
436 stats
->last_seen
= time(NULL
);
439 /* drawing-tooltip */
440 static void drawing_tooltip(PurpleBlistNode
*node
, GString
*text
, gboolean full
) {
441 if(node
->type
== PURPLE_BLIST_BUDDY_NODE
) {
442 PurpleBuddy
*buddy
= (PurpleBuddy
*)node
;
443 CapStatistics
*stats
= get_stats_for(buddy
);
444 /* get the probability that this buddy will respond and add to the tooltip */
445 if(stats
->prediction
->probability
>= 0.0) {
446 g_string_append_printf(text
, "\n<b>%s</b> %3.0f %%", _("Response Probability:"),
447 100 * stats
->prediction
->probability
);
449 g_string_append_printf(text
, "\n<b>%s</b> ???", _("Response Probability:"));
455 static void signed_on(PurpleConnection
*gc
) {
456 PurpleAccount
*account
= purple_connection_get_account(gc
);
457 const char *my_purple_name
= purple_account_get_username(account
);
458 gchar
*my_name
= g_strdup(my_purple_name
);
459 time_t *last_offline
= g_hash_table_lookup(_my_offline_times
, my_name
);
461 const gchar
*account_id
= purple_account_get_username(account
);
462 const gchar
*protocol_id
= purple_account_get_protocol_id(account
);
465 sql
= sqlite3_mprintf("insert into cap_my_usage values(%Q, %Q, %d, now());", account_id
, protocol_id
, 1);
466 sqlite3_exec(_db
, sql
, NULL
, NULL
, NULL
);
470 if(difftime(*last_offline
, time(NULL
)) > purple_prefs_get_int("/plugins/gtk/cap/max_seen_difference") * 60) {
471 /* reset all of the last_message times to -1 */
472 g_hash_table_foreach(_my_offline_times
, reset_all_last_message_times
, NULL
);
474 g_hash_table_remove(_my_offline_times
, my_name
);
480 static void signed_off(PurpleConnection
*gc
) {
481 /* Here we record the time you (the user) sign off of an account.
482 * The account username is the key in the hashtable and the sign off time_t
483 * (equal to the sign off time) is the value. */
484 PurpleAccount
*account
= purple_connection_get_account(gc
);
485 const char *my_purple_name
= purple_account_get_username(account
);
486 gchar
*my_name
= g_strdup(my_purple_name
);
487 time_t *offline_time
= g_malloc(sizeof(time_t));
488 const gchar
*account_id
= purple_account_get_username(account
);
489 const gchar
*protocol_id
= purple_account_get_protocol_id(account
);
492 sql
= sqlite3_mprintf("insert into cap_my_usage values(%Q, %Q, %d, now());", account_id
, protocol_id
, 0);
493 sqlite3_exec(_db
, sql
, NULL
, NULL
, NULL
);
497 g_hash_table_insert(_my_offline_times
, my_name
, offline_time
);
500 static void reset_all_last_message_times(gpointer key
, gpointer value
, gpointer user_data
) {
501 CapStatistics
*stats
= value
;
502 stats
->last_message
= -1;
505 static PurpleStatus
* get_status_for(PurpleBuddy
*buddy
) {
506 PurplePresence
*presence
= purple_buddy_get_presence(buddy
);
507 PurpleStatus
*status
= purple_presence_get_active_status(presence
);
511 static void create_tables() {
513 rc
= sqlite3_exec(_db
,
514 "CREATE TABLE IF NOT EXISTS cap_status ("
515 " buddy varchar(60) not null,"
516 " account varchar(60) not null,"
517 " protocol varchar(60) not null,"
518 " status varchar(60) not null,"
519 " event_time datetime not null,"
520 " primary key (buddy, account, protocol, event_time)"
524 rc
= sqlite3_exec(_db
,
525 "create table if not exists cap_message ("
526 " sender varchar(60) not null,"
527 " receiver varchar(60) not null,"
528 " account varchar(60) not null,"
529 " protocol varchar(60) not null,"
530 " word_count integer not null,"
531 " event_time datetime not null,"
532 " primary key (sender, account, protocol, receiver, event_time)"
536 rc
= sqlite3_exec(_db
,
537 "create table if not exists cap_msg_count ("
538 " buddy varchar(60) not null,"
539 " account varchar(60) not null,"
540 " protocol varchar(60) not null,"
541 " minute_val int not null,"
542 " success_count int not null,"
543 " failed_count int not null,"
544 " primary key (buddy, account, protocol, minute_val)"
548 rc
= sqlite3_exec(_db
,
549 "create table if not exists cap_status_count ("
550 " buddy varchar(60) not null,"
551 " account varchar(60) not null,"
552 " protocol varchar(60) not null,"
553 " status varchar(60) not null,"
554 " success_count int not null,"
555 " failed_count int not null,"
556 " primary key (buddy, account, protocol, status)"
560 rc
= sqlite3_exec(_db
,
561 "create table if not exists cap_my_usage ("
562 " account varchar(60) not null,"
563 " protocol varchar(60) not null,"
564 " online tinyint not null,"
565 " event_time datetime not null,"
566 " primary key(account, protocol, online, event_time)"
571 static gboolean
create_database_connection() {
579 path
= g_build_filename(purple_user_dir(), "cap.db", (gchar
*)NULL
);
581 /* make database connection here */
582 rc
= sqlite3_open(path
, &_db
);
587 /* Add tables here */
589 purple_debug_info("cap", "Database connection successfully made.\n");
592 static void destroy_database_connection() {
599 static guint
word_count(const gchar
*string
) {
600 /*TODO: doesn't really work, should use regex instead (#include <regex.h>)*/
601 gchar
**result
= g_strsplit_set(string
, " ", -1);
602 guint count
= g_strv_length(result
);
609 static void insert_status_change(CapStatistics
*statistics
) {
610 insert_status_change_from_purple_status(statistics
, get_status_for(statistics
->buddy
));
613 static void insert_status_change_from_purple_status(CapStatistics
*statistics
, PurpleStatus
*status
) {
616 const gchar
*status_id
;
617 const gchar
*buddy_name
;
618 const gchar
*protocol_id
;
619 const gchar
*account_id
;
621 /* It would seem that some protocols receive periodic updates of the buddies status.
622 * Check to make sure the last status is not the same as current status to prevent
623 * to many duplicated useless database entries. */
624 if(purple_strequal(statistics
->last_status_id
, purple_status_get_id(status
)))
627 status_id
= purple_status_get_id(status
);
628 buddy_name
= statistics
->buddy
->name
;
629 protocol_id
= purple_account_get_protocol_id(statistics
->buddy
->account
);
630 account_id
= purple_account_get_username(statistics
->buddy
->account
);
632 statistics
->last_status_id
= purple_status_get_id(status
);
634 purple_debug_info("cap", "Executing: insert into cap_status (buddy, account, protocol, status, event_time) values(%s, %s, %s, %s, now());\n", buddy_name
, account_id
, protocol_id
, status_id
);
636 sql
= sqlite3_mprintf("insert into cap_status values (%Q, %Q, %Q, %Q, now());", buddy_name
, account_id
, protocol_id
, status_id
);
637 rc
= sqlite3_exec(_db
, sql
, NULL
, NULL
, NULL
);
641 static void insert_word_count(const char *sender
, const char *receiver
, guint count
) {
643 /* dbi_result result; */
644 /* result = dbi_conn_queryf(_conn, "insert into cap_message values(\'%s\', \'%s\', %d, now());", sender, receiver, count); */
647 /* Purple plugin specific code */
649 static gboolean
plugin_load(PurplePlugin
*plugin
) {
650 _plugin_pointer
= plugin
;
651 _signals_connected
= FALSE
;
653 /* buddy_stats is a hashtable where strings are keys
654 * and the keys are a buddies account id (PurpleBuddy.name).
655 * keys/values are automatically deleted */
656 _buddy_stats
= g_hash_table_new_full(g_str_hash
, g_str_equal
, g_free
, destroy_stats
);
658 /* ? - Can't remember at the moment
660 _my_offline_times
= g_hash_table_new_full(g_str_hash
, g_str_equal
, g_free
, g_free
);
662 if(create_database_connection()) {
663 add_plugin_functionality(plugin
);
668 static void add_plugin_functionality(PurplePlugin
*plugin
) {
669 if(_signals_connected
)
672 purple_debug_info("cap", "Adding plugin functionality.\n");
674 /* Connect all the signals */
675 purple_signal_connect(purple_conversations_get_handle(), "sent-im-msg", plugin
,
676 PURPLE_CALLBACK(sent_im_msg
), NULL
);
678 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg", plugin
,
679 PURPLE_CALLBACK(received_im_msg
), NULL
);
681 purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", plugin
,
682 PURPLE_CALLBACK(buddy_status_changed
), NULL
);
684 purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", plugin
,
685 PURPLE_CALLBACK(buddy_signed_on
), NULL
);
687 purple_signal_connect(purple_blist_get_handle(), "buddy-signed-off", plugin
,
688 PURPLE_CALLBACK(buddy_signed_off
), NULL
);
690 purple_signal_connect(pidgin_blist_get_handle(), "drawing-tooltip", plugin
,
691 PURPLE_CALLBACK(drawing_tooltip
), NULL
);
693 purple_signal_connect(purple_connections_get_handle(), "signed-on", plugin
,
694 PURPLE_CALLBACK(signed_on
), NULL
);
696 purple_signal_connect(purple_connections_get_handle(), "signed-off", plugin
,
697 PURPLE_CALLBACK(signed_off
), NULL
);
699 _signals_connected
= TRUE
;
702 static void cancel_conversation_timeouts(gpointer key
, gpointer value
, gpointer user_data
) {
703 CapStatistics
*stats
= value
;
704 if(stats
->timeout_source_id
!= 0) {
705 purple_timeout_remove(stats
->timeout_source_id
);
706 stats
->timeout_source_id
= 0;
710 static void remove_plugin_functionality(PurplePlugin
*plugin
) {
711 if(!_signals_connected
)
714 purple_debug_info("cap", "Removing plugin functionality.\n");
716 /* If there are any timeouts waiting to be processed then cancel them */
717 g_hash_table_foreach(_buddy_stats
, cancel_conversation_timeouts
, NULL
);
719 /* Connect all the signals */
720 purple_signal_disconnect(purple_conversations_get_handle(), "sent-im-msg", plugin
,
721 PURPLE_CALLBACK(sent_im_msg
));
723 purple_signal_disconnect(purple_conversations_get_handle(), "received-im-msg", plugin
,
724 PURPLE_CALLBACK(received_im_msg
));
726 purple_signal_disconnect(purple_blist_get_handle(), "buddy-status-changed", plugin
,
727 PURPLE_CALLBACK(buddy_status_changed
));
729 purple_signal_disconnect(purple_blist_get_handle(), "buddy-signed-on", plugin
,
730 PURPLE_CALLBACK(buddy_signed_on
));
732 purple_signal_disconnect(purple_blist_get_handle(), "buddy-signed-off", plugin
,
733 PURPLE_CALLBACK(buddy_signed_off
));
735 purple_signal_disconnect(pidgin_blist_get_handle(), "drawing-tooltip", plugin
,
736 PURPLE_CALLBACK(drawing_tooltip
));
738 purple_signal_disconnect(purple_connections_get_handle(), "signed-on", plugin
,
739 PURPLE_CALLBACK(signed_on
));
741 purple_signal_disconnect(purple_connections_get_handle(), "signed-off", plugin
,
742 PURPLE_CALLBACK(signed_off
));
744 _signals_connected
= FALSE
;
747 static void write_stats_on_unload(gpointer key
, gpointer value
, gpointer user_data
) {
748 CapStatistics
*stats
= value
;
749 if(stats
->last_message
!= -1 && stats
->buddy
!= NULL
) {
750 insert_cap_failure(stats
);
754 static gboolean
plugin_unload(PurplePlugin
*plugin
) {
755 purple_debug_info("cap", "CAP plugin unloading\n");
757 /* clean up memory allocations */
759 g_hash_table_foreach(_buddy_stats
, write_stats_on_unload
, NULL
);
760 g_hash_table_destroy(_buddy_stats
);
763 /* close database connection */
764 destroy_database_connection();
769 static CapPrefsUI
* create_cap_prefs_ui() {
770 CapPrefsUI
*ui
= g_malloc(sizeof(CapPrefsUI
));
772 ui
->ret
= gtk_vbox_new(FALSE
, 18);
773 gtk_container_set_border_width(GTK_CONTAINER(ui
->ret
), 10);
774 ui
->cap_vbox
= pidgin_make_frame(ui
->ret
, _("Statistics Configuration"));
776 /* msg_difference spinner */
777 ui
->msg_difference_label
= gtk_label_new(_("Maximum response timeout:"));
778 gtk_misc_set_alignment(GTK_MISC(ui
->msg_difference_label
), 0, 0.5);
779 ui
->msg_difference_input
= gtk_spin_button_new_with_range(1, 1440, 1);
780 ui
->msg_difference_minutes_label
= gtk_label_new(_("minutes"));
781 gtk_misc_set_alignment(GTK_MISC(ui
->msg_difference_minutes_label
), 0, 0.5);
783 /* last_seen spinner */
784 ui
->last_seen_label
= gtk_label_new(_("Maximum last-seen difference:"));
785 gtk_misc_set_alignment(GTK_MISC(ui
->last_seen_label
), 0, 0.5);
786 ui
->last_seen_input
= gtk_spin_button_new_with_range(1, 1440, 1);
787 ui
->last_seen_minutes_label
= gtk_label_new(_("minutes"));
788 gtk_misc_set_alignment(GTK_MISC(ui
->last_seen_minutes_label
), 0, 0.5);
790 /* threshold spinner */
791 ui
->threshold_label
= gtk_label_new(_("Threshold:"));
792 gtk_misc_set_alignment(GTK_MISC(ui
->threshold_label
), 0, 0.5);
793 ui
->threshold_input
= gtk_spin_button_new_with_range(1, 1440, 1);
794 ui
->threshold_minutes_label
= gtk_label_new(_("minutes"));
795 gtk_misc_set_alignment(GTK_MISC(ui
->threshold_minutes_label
), 0, 0.5);
797 /* Layout threshold/last-seen/response-timeout input items */
798 ui
->table_layout
= gtk_table_new(3, 3, FALSE
);
799 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->threshold_label
, 0, 1, 0, 1,
800 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
801 (GtkAttachOptions
) (0), 0, 0);
803 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->threshold_input
, 1, 2, 0, 1,
804 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
805 (GtkAttachOptions
) (0), 0, 0);
807 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->threshold_minutes_label
, 2, 3, 0, 1,
808 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
809 (GtkAttachOptions
) (0), 0, 0);
811 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->msg_difference_label
, 0, 1, 1, 2,
812 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
813 (GtkAttachOptions
) (0), 0, 0);
815 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->msg_difference_input
, 1, 2, 1, 2,
816 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
817 (GtkAttachOptions
) (0), 0, 0);
819 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->msg_difference_minutes_label
, 2, 3, 1, 2,
820 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
821 (GtkAttachOptions
) (0), 0, 0);
823 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->last_seen_label
, 0, 1, 2,3,
824 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
825 (GtkAttachOptions
) (0), 0, 0);
827 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->last_seen_input
, 1, 2, 2, 3,
828 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
829 (GtkAttachOptions
) (0), 0, 0);
831 gtk_table_attach(GTK_TABLE(ui
->table_layout
), ui
->last_seen_minutes_label
, 2, 3, 2, 3,
832 (GtkAttachOptions
) (GTK_EXPAND
| GTK_FILL
),
833 (GtkAttachOptions
) (0), 0, 0);
836 /* Config window - lay it out */
837 gtk_box_pack_start(GTK_BOX(ui
->cap_vbox
), ui
->table_layout
, FALSE
, FALSE
, 0);
839 /* Set the input areas to contain the configuration values from
842 if(purple_prefs_exists("/plugins/gtk/cap/max_msg_difference")) {
843 int max_msg_diff
= purple_prefs_get_int("/plugins/gtk/cap/max_msg_difference");
844 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui
->msg_difference_input
), max_msg_diff
);
846 if(purple_prefs_exists("/plugins/gtk/cap/max_seen_difference")) {
847 int max_seen_diff
= purple_prefs_get_int("/plugins/gtk/cap/max_seen_difference");
848 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui
->last_seen_input
), max_seen_diff
);
850 if(purple_prefs_exists("/plugins/gtk/cap/threshold")) {
851 int threshold
= purple_prefs_get_int("/plugins/gtk/cap/threshold");
852 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui
->threshold_input
), threshold
);
855 /* Add the signals */
856 g_signal_connect(G_OBJECT(ui
->ret
), "destroy",
857 G_CALLBACK(cap_prefs_ui_destroy_cb
), ui
);
859 g_signal_connect(G_OBJECT(ui
->msg_difference_input
), "value-changed",
860 G_CALLBACK(numeric_spinner_prefs_cb
), "/plugins/gtk/cap/max_msg_difference");
862 g_signal_connect(G_OBJECT(ui
->last_seen_input
), "value-changed",
863 G_CALLBACK(numeric_spinner_prefs_cb
), "/plugins/gtk/cap/max_seen_difference");
865 g_signal_connect(G_OBJECT(ui
->threshold_input
), "value-changed",
866 G_CALLBACK(numeric_spinner_prefs_cb
), "/plugins/gtk/cap/threshold");
871 static void cap_prefs_ui_destroy_cb(GtkObject
*object
, gpointer user_data
) {
872 CapPrefsUI
*ui
= user_data
;
874 add_plugin_functionality(_plugin_pointer
);
879 static void numeric_spinner_prefs_cb(GtkSpinButton
*spinbutton
, gpointer user_data
) {
880 purple_prefs_set_int(user_data
, gtk_spin_button_get_value_as_int(spinbutton
));
883 static PidginPluginUiInfo ui_info
= {
885 0 /* page_num (reserved) */,
889 static PurplePluginInfo info
= {
891 PURPLE_MAJOR_VERSION
,
892 PURPLE_MINOR_VERSION
,
893 PURPLE_PLUGIN_STANDARD
, /**< type */
894 PIDGIN_PLUGIN_TYPE
, /**< ui_requirement */
896 NULL
, /**< dependencies */
897 PURPLE_PRIORITY_DEFAULT
, /**< priority */
898 CAP_PLUGIN_ID
, /**< id */
899 N_("Contact Availability Prediction"), /**< name */
900 DISPLAY_VERSION
, /**< version */
901 N_("Contact Availability Prediction plugin."), /** summary */
902 N_("Displays statistical information about your buddies' availability"),
904 "Geoffrey Foster <geoffrey.foster@gmail.com>", /**< author */
905 PURPLE_WEBSITE
, /**< homepage */
906 plugin_load
, /**< load */
907 plugin_unload
, /**< unload */
908 NULL
, /**< destroy */
909 &ui_info
, /**< ui_info */
910 NULL
, /**< extra_info */
911 NULL
, /**< prefs_info */
916 static GtkWidget
* get_config_frame(PurplePlugin
*plugin
) {
917 CapPrefsUI
*ui
= create_cap_prefs_ui();
920 * Prevent database stuff from occuring since we are editing values
922 remove_plugin_functionality(_plugin_pointer
);
927 static void init_plugin(PurplePlugin
*plugin
) {
928 purple_prefs_add_none("/plugins/gtk/cap");
929 purple_prefs_add_int("/plugins/gtk/cap/max_seen_difference", 1);
930 purple_prefs_add_int("/plugins/gtk/cap/max_msg_difference", 10);
931 purple_prefs_add_int("/plugins/gtk/cap/threshold", 5);
934 PURPLE_INIT_PLUGIN(cap
, init_plugin
, info
);