comparison pidgin/plugins/cap/cap.c @ 15374:5fe8042783c1

Rename gtk/ and libgaim/ to pidgin/ and libpurple/
author Sean Egan <seanegan@gmail.com>
date Sat, 20 Jan 2007 02:32:10 +0000
parents
children 7865f302c5fb
comparison
equal deleted inserted replaced
15373:f79e0f4df793 15374:5fe8042783c1
1 /*
2 * Contact Availability Prediction plugin for Gaim
3 *
4 * Copyright (C) 2006 Geoffrey Foster.
5 *
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.
10 *
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.
15 *
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., 59 Temple Place - Suite 330, Boston, MA
19 * 02111-1307, USA.
20 */
21
22 #include "cap.h"
23
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);
30 }
31 }
32
33 static double generate_prediction_for(GaimBuddy *buddy) {
34 double prediction = 1.0f;
35 gboolean generated = FALSE;
36 gchar *buddy_name = buddy->name;
37 const gchar *protocol_id = gaim_account_get_protocol_id(buddy->account);
38 const gchar *account_id = gaim_account_get_username(buddy->account);
39 const gchar *status_id = gaim_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 = gaim_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;
47 sqlite3_stmt *stmt = NULL;
48 const char *tail = NULL;
49 int rc;
50
51
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);
57 if(rc == SQLITE_OK) {
58 int successes = 0;
59 int failures = 0;
60 if(stmt != NULL) {
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)));
66 generated = TRUE;
67 }
68 }
69 sqlite3_finalize(stmt);
70 }
71 }
72 sqlite3_free(sql);
73
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);
79 if(rc == SQLITE_OK) {
80 int successes = 0;
81 int failures = 0;
82 if(stmt != NULL) {
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)));
88 generated = TRUE;
89 }
90 }
91 sqlite3_finalize(stmt);
92 }
93 }
94 sqlite3_free(sql);
95
96
97 if(strcmp(gaim_status_get_id(get_status_for(buddy)), "offline") == 0) {
98 /* This is kind of stupid, change it. */
99 if(prediction == 1.0f)
100 prediction = 0.0f;
101 }
102
103 if(generated)
104 return prediction;
105 else
106 return -1;
107 }
108
109 static CapStatistics * get_stats_for(GaimBuddy *buddy) {
110 gchar *buddy_name;
111 CapStatistics *stats;
112
113 g_return_val_if_fail(buddy != NULL, NULL);
114
115 buddy_name = g_strdup(buddy->name);
116 stats = g_hash_table_lookup(_buddy_stats, buddy_name);
117 if(!stats) {
118 stats = g_malloc(sizeof(CapStatistics));
119 stats->last_message = -1;
120 stats->last_message_status_id = NULL;
121 stats->last_status_id = NULL;
122 stats->prediction = NULL;
123 g_hash_table_insert(_buddy_stats, buddy_name, stats);
124 stats->buddy = buddy;
125 stats->last_seen = -1;
126 stats->last_status_id = "";
127 } else {
128 g_free(buddy_name);
129 }
130 generate_prediction(stats);
131 return stats;
132 }
133
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 g_free(stats);
140 }
141
142 static void
143 insert_cap_msg_count_success(const char *buddy_name, const char *account, const char *protocol, int minute) {
144 int rc;
145 sqlite3_stmt *stmt;
146 const char *tail;
147 char *sql_select = sqlite3_mprintf("SELECT * FROM cap_msg_count WHERE "
148 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
149 buddy_name, account, protocol, minute);
150 char *sql_ins_up = NULL;
151
152 gaim_debug_info("cap", "%s\n", sql_select);
153
154 sqlite3_prepare(_db, sql_select, -1, &stmt, &tail);
155
156 rc = sqlite3_step(stmt);
157
158 if(rc == SQLITE_DONE) {
159 sql_ins_up = sqlite3_mprintf("INSERT INTO cap_msg_count VALUES (%Q, %Q, %Q, %d, %d, %d);",
160 buddy_name, account, protocol, minute, 1, 0);
161 } else if(rc == SQLITE_ROW) {
162 sql_ins_up = sqlite3_mprintf("UPDATE cap_msg_count SET success_count=success_count+1 WHERE "
163 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
164 buddy_name, account, protocol, minute);
165 } else {
166 gaim_debug_info("cap", "%d\n", rc);
167 sqlite3_finalize(stmt);
168 sqlite3_free(sql_select);
169 return;
170 }
171
172 sqlite3_finalize(stmt);
173 sqlite3_free(sql_select);
174
175 sqlite3_exec(_db, sql_ins_up, NULL, NULL, NULL);
176 sqlite3_free(sql_ins_up);
177 }
178
179 static void
180 insert_cap_status_count_success(const char *buddy_name, const char *account, const char *protocol, const char *status_id) {
181 int rc;
182 sqlite3_stmt *stmt;
183 const char *tail;
184 char *sql_select = sqlite3_mprintf("SELECT * FROM cap_status_count WHERE "
185 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
186 buddy_name, account, protocol, status_id);
187 char *sql_ins_up = NULL;
188
189 gaim_debug_info("cap", "%s\n", sql_select);
190
191 sqlite3_prepare(_db, sql_select, -1, &stmt, &tail);
192
193 rc = sqlite3_step(stmt);
194
195 if(rc == SQLITE_DONE) {
196 sql_ins_up = sqlite3_mprintf("INSERT INTO cap_status_count VALUES (%Q, %Q, %Q, %Q, %d, %d);",
197 buddy_name, account, protocol, status_id, 1, 0);
198 } else if(rc == SQLITE_ROW) {
199 sql_ins_up = sqlite3_mprintf("UPDATE cap_status_count SET success_count=success_count+1 WHERE "
200 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
201 buddy_name, account, protocol, status_id);
202 } else {
203 gaim_debug_info("cap", "%d\n", rc);
204 sqlite3_finalize(stmt);
205 sqlite3_free(sql_select);
206 return;
207 }
208
209 sqlite3_finalize(stmt);
210 sqlite3_free(sql_select);
211
212 sqlite3_exec(_db, sql_ins_up, NULL, NULL, NULL);
213 sqlite3_free(sql_ins_up);
214 }
215
216 static void
217 insert_cap_msg_count_failed(const char *buddy_name, const char *account, const char *protocol, int minute) {
218 int rc;
219 sqlite3_stmt *stmt;
220 const char *tail;
221 char *sql_select = sqlite3_mprintf("SELECT * FROM cap_msg_count WHERE "
222 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
223 buddy_name, account, protocol, minute);
224 char *sql_ins_up = NULL;
225
226 gaim_debug_info("cap", "%s\n", sql_select);
227
228 sqlite3_prepare(_db, sql_select, -1, &stmt, &tail);
229
230 rc = sqlite3_step(stmt);
231
232 if(rc == SQLITE_DONE) {
233 sql_ins_up = sqlite3_mprintf("INSERT INTO cap_msg_count VALUES (%Q, %Q, %Q, %d, %d, %d);",
234 buddy_name, account, protocol, minute, 0, 1);
235 } else if(rc == SQLITE_ROW) {
236 sql_ins_up = sqlite3_mprintf("UPDATE cap_msg_count SET failed_count=failed_count+1 WHERE "
237 "buddy=%Q AND account=%Q AND protocol=%Q AND minute_val=%d;",
238 buddy_name, account, protocol, minute);
239 } else {
240 gaim_debug_info("cap", "%d\n", rc);
241 sqlite3_finalize(stmt);
242 sqlite3_free(sql_select);
243 return;
244 }
245
246 sqlite3_finalize(stmt);
247 sqlite3_free(sql_select);
248
249 sqlite3_exec(_db, sql_ins_up, NULL, NULL, NULL);
250 sqlite3_free(sql_ins_up);
251 }
252
253 static void
254 insert_cap_status_count_failed(const char *buddy_name, const char *account, const char *protocol, const char *status_id) {
255 int rc;
256 sqlite3_stmt *stmt;
257 const char *tail;
258 char *sql_select = sqlite3_mprintf("SELECT * FROM cap_status_count WHERE "
259 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
260 buddy_name, account, protocol, status_id);
261 char *sql_ins_up = NULL;
262
263 gaim_debug_info("cap", "%s\n", sql_select);
264
265 sqlite3_prepare(_db, sql_select, -1, &stmt, &tail);
266
267 rc = sqlite3_step(stmt);
268
269 if(rc == SQLITE_DONE) {
270 sql_ins_up = sqlite3_mprintf("INSERT INTO cap_status_count VALUES (%Q, %Q, %Q, %Q, %d, %d);",
271 buddy_name, account, protocol, status_id, 0, 1);
272 } else if(rc == SQLITE_ROW) {
273 sql_ins_up = sqlite3_mprintf("UPDATE cap_status_count SET failed_count=failed_count+1 WHERE "
274 "buddy=%Q AND account=%Q AND protocol=%Q AND status=%Q;",
275 buddy_name, account, protocol, status_id);
276 } else {
277 gaim_debug_info("cap", "%d\n", rc);
278 sqlite3_finalize(stmt);
279 sqlite3_free(sql_select);
280 return;
281 }
282
283 sqlite3_finalize(stmt);
284 sqlite3_free(sql_select);
285
286 sqlite3_exec(_db, sql_ins_up, NULL, NULL, NULL);
287 sqlite3_free(sql_ins_up);
288 }
289
290 static void insert_cap_success(CapStatistics *stats) {
291 gchar *buddy_name = stats->buddy->name;
292 const gchar *protocol_id = gaim_account_get_protocol_id(stats->buddy->account);
293 const gchar *account_id = gaim_account_get_username(stats->buddy->account);
294 const gchar *status_id = (stats->last_message_status_id) ?
295 stats->last_message_status_id :
296 gaim_status_get_id(get_status_for(stats->buddy));
297 struct tm *current_time;
298 int minute;
299
300 if(stats->last_message == -1) {
301 time_t now = time(NULL);
302 current_time = localtime(&now);
303 } else {
304 current_time = localtime(&stats->last_message);
305 }
306 minute = current_time->tm_min + current_time->tm_hour * 60;
307
308 insert_cap_msg_count_success(buddy_name, account_id, protocol_id, minute);
309
310 insert_cap_status_count_success(buddy_name, account_id, protocol_id, status_id);
311
312 stats->last_message = -1;
313 stats->last_message_status_id = NULL;
314 }
315
316 static void insert_cap_failure(CapStatistics *stats) {
317 gchar *buddy_name = stats->buddy->name;
318 const gchar *protocol_id = gaim_account_get_protocol_id(stats->buddy->account);
319 const gchar *account_id = gaim_account_get_username(stats->buddy->account);
320 const gchar *status_id = (stats->last_message_status_id) ?
321 stats->last_message_status_id :
322 gaim_status_get_id(get_status_for(stats->buddy));
323 struct tm *current_time = localtime(&stats->last_message);
324 int minute = current_time->tm_min + current_time->tm_hour * 60;
325
326 insert_cap_msg_count_failed(buddy_name, account_id, protocol_id, minute);
327
328 insert_cap_status_count_failed(buddy_name, account_id, protocol_id, status_id);
329
330 stats->last_message = -1;
331 stats->last_message_status_id = NULL;
332 }
333
334 static gboolean max_message_difference_cb(gpointer data) {
335 CapStatistics *stats = data;
336 gaim_debug_info("cap", "Max Message Difference timeout occured\n");
337 insert_cap_failure(stats);
338 stats->timeout_source_id = 0;
339 return FALSE;
340 }
341
342 /* Gaim Signal Handlers */
343
344 /* sent-im-msg */
345 static void sent_im_msg(GaimAccount *account, const char *receiver, const char *message) {
346 GaimBuddy *buddy;
347 guint interval, words;
348
349 buddy = gaim_find_buddy(account, receiver);
350
351 if (buddy == NULL)
352 return;
353
354 interval = gaim_prefs_get_int("/plugins/gtk/cap/max_msg_difference") * 1000 * 60;
355 words = word_count(message);
356
357 CapStatistics *stats = get_stats_for(buddy);
358
359 insert_word_count(gaim_account_get_username(account), receiver, words);
360 stats->last_message = time(NULL);
361 stats->last_message_status_id = gaim_status_get_id(get_status_for(buddy));
362 if(stats->timeout_source_id != 0)
363 g_source_remove(stats->timeout_source_id);
364
365 stats->timeout_source_id = g_timeout_add(interval, max_message_difference_cb, stats);
366 }
367
368 /* received-im-msg */
369 static void
370 received_im_msg(GaimAccount *account, char *sender, char *message, GaimConversation *conv, GaimMessageFlags flags) {
371 GaimBuddy *buddy;
372 CapStatistics *stats;
373 /* guint words = word_count(message); */
374
375 buddy = gaim_find_buddy(account, sender);
376
377 if (buddy == NULL)
378 return;
379
380 stats = get_stats_for(buddy);
381
382 /* insert_word_count(sender, buddy_name, words); */
383
384 /* If we are waiting for a response from a prior message
385 * then cancel the timeout callback. */
386 if(stats->timeout_source_id != 0) {
387 gaim_debug_info("cap", "Cancelling timeout callback\n");
388 g_source_remove(stats->timeout_source_id);
389 stats->timeout_source_id = 0;
390 }
391
392 insert_cap_success(stats);
393
394 /* Reset the last_message value */
395 stats->last_message = -1;
396 /* Reset the last status id value */
397 stats->last_message_status_id = NULL;
398 }
399
400 /* buddy-status-changed */
401 static void buddy_status_changed(GaimBuddy *buddy, GaimStatus *old_status, GaimStatus *status) {
402 CapStatistics *stats = get_stats_for(buddy);
403 insert_status_change_from_gaim_status(stats, status);
404 }
405
406 /* buddy-signed-on */
407 static void buddy_signed_on(GaimBuddy *buddy) {
408 CapStatistics *stats = get_stats_for(buddy);
409
410 /* If the statistic object existed but doesn't have a buddy pointer associated
411 * with it then reassociate one with it. The pointer being null is a result
412 * of a buddy with existing stats signing off and Gaim sticking around. */
413 if(!stats->buddy) {
414 stats->buddy = buddy;
415 }
416
417 insert_status_change(stats);
418 }
419
420 /* buddy-signed-off */
421 static void buddy_signed_off(GaimBuddy *buddy) {
422 CapStatistics *stats = get_stats_for(buddy);
423
424 /* We don't necessarily want to delete a buddies generated statistics every time they go offline.
425 * Instead we just set the buddy pointer to null so that when they come back online we can look
426 * them up again and continue using their statistics.
427 */
428 insert_status_change(stats);
429 /* stats->buddy = NULL; */
430 stats->last_seen = time(NULL);
431 }
432
433 static void buddy_idle(GaimBuddy *buddy, gboolean old_idle, gboolean idle) {
434 }
435
436 static void blist_node_extended_menu(GaimBlistNode *node, GList **menu) {
437 GaimBuddy *buddy;
438 GaimMenuAction *menu_action;
439 gaim_debug_info("cap", "got extended blist menu\n");
440 gaim_debug_info("cap", "is buddy: %d\n", GAIM_BLIST_NODE_IS_BUDDY(node));
441 gaim_debug_info("cap", "is contact: %d\n", GAIM_BLIST_NODE_IS_CONTACT(node));
442 gaim_debug_info("cap", "is group: %d\n", GAIM_BLIST_NODE_IS_GROUP(node));
443 /* Probably only concerned with buddy/contact types. Contacts = meta-buddies (grouped msn/jabber/etc.) */
444 g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));
445 buddy = (GaimBuddy *)node;
446 menu_action = gaim_menu_action_new(_("Display Statistics"),
447 GAIM_CALLBACK(display_statistics_action_cb), NULL, NULL);
448 *menu = g_list_append(*menu, menu_action);
449 }
450
451 /* drawing-tooltip */
452 static void drawing_tooltip(GaimBlistNode *node, GString *text, gboolean full) {
453 if(node->type == GAIM_BLIST_BUDDY_NODE) {
454 GaimBuddy *buddy = (GaimBuddy *)node;
455 CapStatistics *stats = get_stats_for(buddy);
456 /* get the probability that this buddy will respond and add to the tooltip */
457 if(stats->prediction->probability >= 0.0) {
458 g_string_append_printf(text, "\n<b>%s</b> %3.0f %%", _("Response Probability:"),
459 100 * stats->prediction->probability);
460 } else {
461 g_string_append_printf(text, "\n<b>%s</b> ???", _("Response Probability:"));
462 }
463 }
464 }
465
466 /* signed-on */
467 static void signed_on(GaimConnection *gc) {
468 GaimAccount *account = gaim_connection_get_account(gc);
469 const char *my_gaim_name = gaim_account_get_username(account);
470 gchar *my_name = g_strdup(my_gaim_name);
471 time_t *last_offline = g_hash_table_lookup(_my_offline_times, my_name);
472
473 const gchar *account_id = gaim_account_get_username(account);
474 const gchar *protocol_id = gaim_account_get_protocol_id(account);
475 char *sql;
476
477 sql = sqlite3_mprintf("insert into cap_my_usage values(%Q, %Q, %d, now());", account_id, protocol_id, 1);
478 sqlite3_exec(_db, sql, NULL, NULL, NULL);
479 sqlite3_free(sql);
480
481 if(last_offline) {
482 if(difftime(*last_offline, time(NULL)) > gaim_prefs_get_int("/plugins/gtk/cap/max_seen_difference") * 60) {
483 /* reset all of the last_message times to -1 */
484 g_hash_table_foreach(_my_offline_times, reset_all_last_message_times, NULL);
485 }
486 g_hash_table_remove(_my_offline_times, my_name);
487 }
488 g_free(my_name);
489 }
490
491 /* signed-off */
492 static void signed_off(GaimConnection *gc) {
493 /* Here we record the time you (the user) sign off of an account.
494 * The account username is the key in the hashtable and the sign off time_t
495 * (equal to the sign off time) is the value. */
496 GaimAccount *account = gaim_connection_get_account(gc);
497 const char *my_gaim_name = gaim_account_get_username(account);
498 gchar *my_name = g_strdup(my_gaim_name);
499 time_t *offline_time = g_malloc(sizeof(time_t));
500 const gchar *account_id = gaim_account_get_username(account);
501 const gchar *protocol_id = gaim_account_get_protocol_id(account);
502 char *sql;
503
504 sql = sqlite3_mprintf("insert into cap_my_usage values(%Q, %Q, %d, now());", account_id, protocol_id, 0);
505 sqlite3_exec(_db, sql, NULL, NULL, NULL);
506 sqlite3_free(sql);
507
508 time(offline_time);
509 g_hash_table_insert(_my_offline_times, my_name, offline_time);
510 }
511
512 static void reset_all_last_message_times(gpointer key, gpointer value, gpointer user_data) {
513 CapStatistics *stats = value;
514 stats->last_message = -1;
515 }
516
517 static GaimStatus * get_status_for(GaimBuddy *buddy) {
518 GaimPresence *presence = gaim_buddy_get_presence(buddy);
519 GaimStatus *status = gaim_presence_get_active_status(presence);
520 return status;
521 }
522
523 static void create_tables() {
524 int rc;
525 rc = sqlite3_exec(_db,
526 "CREATE TABLE IF NOT EXISTS cap_status ("
527 " buddy varchar(60) not null,"
528 " account varchar(60) not null,"
529 " protocol varchar(60) not null,"
530 " status varchar(60) not null,"
531 " event_time datetime not null,"
532 " primary key (buddy, account, protocol, event_time)"
533 ");",
534 NULL, NULL, NULL);
535
536 rc = sqlite3_exec(_db,
537 "create table if not exists cap_message ("
538 " sender varchar(60) not null,"
539 " receiver varchar(60) not null,"
540 " account varchar(60) not null,"
541 " protocol varchar(60) not null,"
542 " word_count integer not null,"
543 " event_time datetime not null,"
544 " primary key (sender, account, protocol, receiver, event_time)"
545 ");",
546 NULL, NULL, NULL);
547
548 rc = sqlite3_exec(_db,
549 "create table if not exists cap_msg_count ("
550 " buddy varchar(60) not null,"
551 " account varchar(60) not null,"
552 " protocol varchar(60) not null,"
553 " minute_val int not null,"
554 " success_count int not null,"
555 " failed_count int not null,"
556 " primary key (buddy, account, protocol, minute_val)"
557 ");",
558 NULL, NULL, NULL);
559
560 rc = sqlite3_exec(_db,
561 "create table if not exists cap_status_count ("
562 " buddy varchar(60) not null,"
563 " account varchar(60) not null,"
564 " protocol varchar(60) not null,"
565 " status varchar(60) not null,"
566 " success_count int not null,"
567 " failed_count int not null,"
568 " primary key (buddy, account, protocol, status)"
569 ");",
570 NULL, NULL, NULL);
571
572 rc = sqlite3_exec(_db,
573 "create table if not exists cap_my_usage ("
574 " account varchar(60) not null,"
575 " protocol varchar(60) not null,"
576 " online tinyint not null,"
577 " event_time datetime not null,"
578 " primary key(account, protocol, online, event_time)"
579 ");",
580 NULL, NULL, NULL);
581 }
582
583 static gboolean create_database_connection() {
584 gchar *path;
585 int rc;
586
587 if(_db)
588 return TRUE;
589
590 /* build the path */
591 path = g_build_filename(gaim_user_dir(), "cap.db", (gchar *)NULL);
592
593 /* make database connection here */
594 rc = sqlite3_open(path, &_db);
595 g_free(path);
596 if(rc != SQLITE_OK)
597 return FALSE;
598
599 /* Add tables here */
600 create_tables();
601 gaim_debug_info("cap", "Database connection successfully made.\n");
602 return TRUE;
603 }
604 static void destroy_database_connection() {
605 if(_db)
606 sqlite3_close(_db);
607
608 _db = NULL;
609 }
610
611 static guint word_count(const gchar *string) {
612 /*TODO: doesn't really work, should use regex instead (#include <regex.h>)*/
613 gchar **result = g_strsplit_set(string, " ", -1);
614 guint count = g_strv_length(result);
615
616 g_strfreev(result);
617
618 return count;
619 }
620
621 static void insert_status_change(CapStatistics *statistics) {
622 insert_status_change_from_gaim_status(statistics, get_status_for(statistics->buddy));
623 }
624
625 static void insert_status_change_from_gaim_status(CapStatistics *statistics, GaimStatus *status) {
626 char *sql;
627 int rc;
628 const gchar *status_id;
629 const gchar *buddy_name;
630 const gchar *protocol_id;
631 const gchar *account_id;
632
633 /* It would seem that some protocols receive periodic updates of the buddies status.
634 * Check to make sure the last status is not the same as current status to prevent
635 * to many duplicated useless database entries. */
636 if(strcmp(statistics->last_status_id, gaim_status_get_id(status)) == 0)
637 return;
638
639 status_id = gaim_status_get_id(status);
640 buddy_name = statistics->buddy->name;
641 protocol_id = gaim_account_get_protocol_id(statistics->buddy->account);
642 account_id = gaim_account_get_username(statistics->buddy->account);
643
644 statistics->last_status_id = gaim_status_get_id(status);
645
646 gaim_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);
647
648 sql = sqlite3_mprintf("insert into cap_status values (%Q, %Q, %Q, %Q, now());", buddy_name, account_id, protocol_id, status_id);
649 rc = sqlite3_exec(_db, sql, NULL, NULL, NULL);
650 sqlite3_free(sql);
651 }
652
653 static void insert_word_count(const char *sender, const char *receiver, guint count) {
654 /* TODO! */
655 /* dbi_result result; */
656 /* result = dbi_conn_queryf(_conn, "insert into cap_message values(\'%s\', \'%s\', %d, now());", sender, receiver, count); */
657 }
658
659 /* Callbacks */
660 void display_statistics_action_cb(GaimBlistNode *node, gpointer data) {
661 GaimBuddy *buddy;
662
663 g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));
664 buddy = (GaimBuddy *)node;
665 gaim_debug_info("cap", "Statistics for %s requested.\n", buddy->name);
666 }
667
668 /* Gaim plugin specific code */
669
670 static gboolean plugin_load(GaimPlugin *plugin) {
671 _plugin_pointer = plugin;
672 _signals_connected = FALSE;
673
674 /* buddy_stats is a hashtable where strings are keys
675 * and the keys are a buddies account id (GaimBuddy.name).
676 * keys/values are automatically deleted */
677 _buddy_stats = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, destroy_stats);
678
679 /* ? - Can't remember at the moment
680 */
681 _my_offline_times = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
682
683 if(create_database_connection()) {
684 add_plugin_functionality(plugin);
685 }
686 return TRUE;
687 }
688
689 static void add_plugin_functionality(GaimPlugin *plugin) {
690 if(_signals_connected)
691 return;
692
693 gaim_debug_info("cap", "Adding plugin functionality.\n");
694
695 /* Connect all the signals */
696 gaim_signal_connect(gaim_conversations_get_handle(), "sent-im-msg", plugin,
697 GAIM_CALLBACK(sent_im_msg), NULL);
698
699 gaim_signal_connect(gaim_conversations_get_handle(), "received-im-msg", plugin,
700 GAIM_CALLBACK(received_im_msg), NULL);
701
702 gaim_signal_connect(gaim_blist_get_handle(), "buddy-status-changed", plugin,
703 GAIM_CALLBACK(buddy_status_changed), NULL);
704
705 gaim_signal_connect(gaim_blist_get_handle(), "buddy-signed-on", plugin,
706 GAIM_CALLBACK(buddy_signed_on), NULL);
707
708 gaim_signal_connect(gaim_blist_get_handle(), "buddy-signed-off", plugin,
709 GAIM_CALLBACK(buddy_signed_off), NULL);
710
711 /*gaim_signal_connect(gaim_blist_get_handle(), "blist-node-extended-menu", plugin,
712 GAIM_CALLBACK(blist_node_extended_menu), NULL);*/
713
714 gaim_signal_connect(gaim_gtk_blist_get_handle(), "drawing-tooltip", plugin,
715 GAIM_CALLBACK(drawing_tooltip), NULL);
716
717 gaim_signal_connect(gaim_connections_get_handle(), "signed-on", plugin,
718 GAIM_CALLBACK(signed_on), NULL);
719
720 gaim_signal_connect(gaim_connections_get_handle(), "signed-off", plugin,
721 GAIM_CALLBACK(signed_off), NULL);
722
723 gaim_signal_connect(gaim_blist_get_handle(), "buddy-idle-changed", plugin,
724 GAIM_CALLBACK(buddy_idle), NULL);
725
726 _signals_connected = TRUE;
727 }
728
729 static void cancel_conversation_timeouts(gpointer key, gpointer value, gpointer user_data) {
730 CapStatistics *stats = value;
731 if(stats->timeout_source_id != 0) {
732 g_source_remove(stats->timeout_source_id);
733 stats->timeout_source_id = 0;
734 }
735 }
736
737 static void remove_plugin_functionality(GaimPlugin *plugin) {
738 if(!_signals_connected)
739 return;
740
741 gaim_debug_info("cap", "Removing plugin functionality.\n");
742
743 /* If there are any timeouts waiting to be processed then cancel them */
744 g_hash_table_foreach(_buddy_stats, cancel_conversation_timeouts, NULL);
745
746 /* Connect all the signals */
747 gaim_signal_disconnect(gaim_conversations_get_handle(), "sent-im-msg", plugin,
748 GAIM_CALLBACK(sent_im_msg));
749
750 gaim_signal_disconnect(gaim_conversations_get_handle(), "received-im-msg", plugin,
751 GAIM_CALLBACK(received_im_msg));
752
753 gaim_signal_disconnect(gaim_blist_get_handle(), "buddy-status-changed", plugin,
754 GAIM_CALLBACK(buddy_status_changed));
755
756 gaim_signal_disconnect(gaim_blist_get_handle(), "buddy-signed-on", plugin,
757 GAIM_CALLBACK(buddy_signed_on));
758
759 gaim_signal_disconnect(gaim_blist_get_handle(), "buddy-signed-off", plugin,
760 GAIM_CALLBACK(buddy_signed_off));
761
762 /*gaim_signal_disconnect(gaim_blist_get_handle(), "blist-node-extended-menu", plugin,
763 GAIM_CALLBACK(blist_node_extended_menu));*/
764
765 gaim_signal_disconnect(gaim_gtk_blist_get_handle(), "drawing-tooltip", plugin,
766 GAIM_CALLBACK(drawing_tooltip));
767
768 gaim_signal_disconnect(gaim_connections_get_handle(), "signed-on", plugin,
769 GAIM_CALLBACK(signed_on));
770
771 gaim_signal_disconnect(gaim_connections_get_handle(), "signed-off", plugin,
772 GAIM_CALLBACK(signed_off));
773
774 gaim_signal_disconnect(gaim_blist_get_handle(), "buddy-idle-changed", plugin,
775 GAIM_CALLBACK(buddy_idle));
776
777 _signals_connected = FALSE;
778 }
779
780 static void write_stats_on_unload(gpointer key, gpointer value, gpointer user_data) {
781 CapStatistics *stats = value;
782 if(stats->last_message != -1 && stats->buddy != NULL) {
783 insert_cap_failure(stats);
784 }
785 }
786
787 static gboolean plugin_unload(GaimPlugin *plugin) {
788 gaim_debug_info("cap", "CAP plugin unloading\n");
789
790 /* clean up memory allocations */
791 if(_buddy_stats) {
792 g_hash_table_foreach(_buddy_stats, write_stats_on_unload, NULL);
793 g_hash_table_destroy(_buddy_stats);
794 }
795
796 /* close database connection */
797 destroy_database_connection();
798
799 return TRUE;
800 }
801
802 static CapPrefsUI * create_cap_prefs_ui() {
803 CapPrefsUI *ui = g_malloc(sizeof(CapPrefsUI));
804
805 ui->ret = gtk_vbox_new(FALSE, 18);
806 gtk_container_set_border_width(GTK_CONTAINER(ui->ret), 10);
807 ui->cap_vbox = gaim_gtk_make_frame(ui->ret, _("Statistics Configuration"));
808
809 /* msg_difference spinner */
810 ui->msg_difference_label = gtk_label_new(_("Maximum response timeout:"));
811 gtk_misc_set_alignment(GTK_MISC(ui->msg_difference_label), 0, 0.5);
812 ui->msg_difference_input = gtk_spin_button_new_with_range(1, 1440, 1);
813 ui->msg_difference_minutes_label = gtk_label_new(_("minutes"));
814 gtk_misc_set_alignment(GTK_MISC(ui->msg_difference_minutes_label), 0, 0.5);
815
816 /* last_seen spinner */
817 ui->last_seen_label = gtk_label_new(_("Maximum last-seen difference:"));
818 gtk_misc_set_alignment(GTK_MISC(ui->last_seen_label), 0, 0.5);
819 ui->last_seen_input = gtk_spin_button_new_with_range(1, 1440, 1);
820 ui->last_seen_minutes_label = gtk_label_new(_("minutes"));
821 gtk_misc_set_alignment(GTK_MISC(ui->last_seen_minutes_label), 0, 0.5);
822
823 /* threshold spinner */
824 ui->threshold_label = gtk_label_new(_("Threshold:"));
825 gtk_misc_set_alignment(GTK_MISC(ui->threshold_label), 0, 0.5);
826 ui->threshold_input = gtk_spin_button_new_with_range(1, 1440, 1);
827 ui->threshold_minutes_label = gtk_label_new(_("minutes"));
828 gtk_misc_set_alignment(GTK_MISC(ui->threshold_minutes_label), 0, 0.5);
829
830 /* Layout threshold/last-seen/response-timeout input items */
831 ui->table_layout = gtk_table_new(3, 3, FALSE);
832 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->threshold_label, 0, 1, 0, 1,
833 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
834 (GtkAttachOptions) (0), 0, 0);
835
836 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->threshold_input, 1, 2, 0, 1,
837 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
838 (GtkAttachOptions) (0), 0, 0);
839
840 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->threshold_minutes_label, 2, 3, 0, 1,
841 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
842 (GtkAttachOptions) (0), 0, 0);
843
844 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->msg_difference_label, 0, 1, 1, 2,
845 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
846 (GtkAttachOptions) (0), 0, 0);
847
848 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->msg_difference_input, 1, 2, 1, 2,
849 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
850 (GtkAttachOptions) (0), 0, 0);
851
852 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->msg_difference_minutes_label, 2, 3, 1, 2,
853 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
854 (GtkAttachOptions) (0), 0, 0);
855
856 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->last_seen_label, 0, 1, 2,3,
857 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
858 (GtkAttachOptions) (0), 0, 0);
859
860 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->last_seen_input, 1, 2, 2, 3,
861 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
862 (GtkAttachOptions) (0), 0, 0);
863
864 gtk_table_attach(GTK_TABLE(ui->table_layout), ui->last_seen_minutes_label, 2, 3, 2, 3,
865 (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
866 (GtkAttachOptions) (0), 0, 0);
867
868
869 /* Config window - lay it out */
870 gtk_box_pack_start(GTK_BOX(ui->cap_vbox), ui->table_layout, FALSE, FALSE, 0);
871
872 /* Set the input areas to contain the configuration values from
873 * gaim prefs.
874 */
875 if(gaim_prefs_exists("/plugins/gtk/cap/max_msg_difference")) {
876 int max_msg_diff = gaim_prefs_get_int("/plugins/gtk/cap/max_msg_difference");
877 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->msg_difference_input), max_msg_diff);
878 }
879 if(gaim_prefs_exists("/plugins/gtk/cap/max_seen_difference")) {
880 int max_seen_diff = gaim_prefs_get_int("/plugins/gtk/cap/max_seen_difference");
881 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->last_seen_input), max_seen_diff);
882 }
883 if(gaim_prefs_exists("/plugins/gtk/cap/threshold")) {
884 int threshold = gaim_prefs_get_int("/plugins/gtk/cap/threshold");
885 gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->threshold_input), threshold);
886 }
887
888 /* Add the signals */
889 g_signal_connect(G_OBJECT(ui->ret), "destroy",
890 G_CALLBACK(cap_prefs_ui_destroy_cb), ui);
891
892 g_signal_connect(G_OBJECT(ui->msg_difference_input), "value-changed",
893 G_CALLBACK(numeric_spinner_prefs_cb), "/plugins/gtk/cap/max_msg_difference");
894
895 g_signal_connect(G_OBJECT(ui->last_seen_input), "value-changed",
896 G_CALLBACK(numeric_spinner_prefs_cb), "/plugins/gtk/cap/max_seen_difference");
897
898 g_signal_connect(G_OBJECT(ui->threshold_input), "value-changed",
899 G_CALLBACK(numeric_spinner_prefs_cb), "/plugins/gtk/cap/threshold");
900
901 return ui;
902 }
903
904 static void cap_prefs_ui_destroy_cb(GtkObject *object, gpointer user_data) {
905 CapPrefsUI *ui = user_data;
906 if(_db) {
907 add_plugin_functionality(_plugin_pointer);
908 }
909 g_free(ui);
910 }
911
912 static void numeric_spinner_prefs_cb(GtkSpinButton *spinbutton, gpointer user_data) {
913 gaim_prefs_set_int(user_data, gtk_spin_button_get_value_as_int(spinbutton));
914 }
915
916 static GaimGtkPluginUiInfo ui_info = {
917 get_config_frame,
918 0 /* page_num (reserved) */
919 };
920
921 static GaimPluginInfo info = {
922 GAIM_PLUGIN_MAGIC,
923 GAIM_MAJOR_VERSION,
924 GAIM_MINOR_VERSION,
925 GAIM_PLUGIN_STANDARD, /**< type */
926 GAIM_GTK_PLUGIN_TYPE, /**< ui_requirement */
927 0, /**< flags */
928 NULL, /**< dependencies */
929 GAIM_PRIORITY_DEFAULT, /**< priority */
930 CAP_PLUGIN_ID, /**< id */
931 N_("Contact Availability Prediction"), /**< name */
932 VERSION, /**< version */
933 N_("Contact Availability Prediction plugin."), /** summary */
934 N_("The contact availability plugin (cap) is used to display statistical information about buddies in a users contact list."),
935 /** description */
936 "Geoffrey Foster <geoffrey.foster@gmail.com>", /**< author */
937 GAIM_WEBSITE, /**< homepage */
938 plugin_load, /**< load */
939 plugin_unload, /**< unload */
940 NULL, /**< destroy */
941 &ui_info, /**< ui_info */
942 NULL, /**< extra_info */
943 NULL, /**< prefs_info */
944 NULL
945 };
946
947 static GtkWidget * get_config_frame(GaimPlugin *plugin) {
948 CapPrefsUI *ui = create_cap_prefs_ui();
949
950 /*
951 * Prevent database stuff from occuring since we are editing values
952 */
953 remove_plugin_functionality(_plugin_pointer);
954
955 return ui->ret;
956 }
957
958 static void init_plugin(GaimPlugin *plugin) {
959 gaim_prefs_add_none("/plugins/gtk/cap");
960 gaim_prefs_add_int("/plugins/gtk/cap/max_seen_difference", 1);
961 gaim_prefs_add_int("/plugins/gtk/cap/max_msg_difference", 10);
962 gaim_prefs_add_int("/plugins/gtk/cap/threshold", 5);
963 }
964
965 GAIM_INIT_PLUGIN(cap, init_plugin, info);