1 /**
2  * @file savedstatuses.c Saved Status API
3  * @ingroup core
4  */
5 
6 /* purple
7  *
8  * Purple is the legal property of its developers, whose names are too numerous
9  * to list here.  Please refer to the COPYRIGHT file distributed with this
10  * source distribution.
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
25  */
26 #include "internal.h"
27 
28 #include "debug.h"
29 #include "idle.h"
30 #include "notify.h"
31 #include "savedstatuses.h"
32 #include "dbus-maybe.h"
33 #include "request.h"
34 #include "status.h"
35 #include "util.h"
36 #include "xmlnode.h"
37 
38 /**
39  * The maximum number of transient statuses to save.  This
40  * is used during the shutdown process to clean out old
41  * transient statuses.
42  */
43 #define MAX_TRANSIENTS 5
44 
45 /**
46  * The default message to use when the user becomes auto-away.
47  */
48 #define DEFAULT_AUTOAWAY_MESSAGE _("I'm not here right now")
49 
50 /**
51  * The information stores a snap-shot of the statuses of all
52  * your accounts.  Basically these are your saved away messages.
53  * There is an overall status and message that applies to
54  * all your accounts, and then each individual account can
55  * optionally have a different custom status and message.
56  *
57  * The changes to status.xml caused by the new status API
58  * are fully backward compatible.  The new status API just
59  * adds the optional sub-statuses to the XML file.
60  */
61 struct _PurpleSavedStatus
62 {
63 	char *title;
64 	PurpleStatusPrimitive type;
65 	char *message;
66 
67 	/** The timestamp when this saved status was created. This must be unique. */
68 	time_t creation_time;
69 
70 	time_t lastused;
71 
72 	unsigned int usage_count;
73 
74 	GList *substatuses;      /**< A list of PurpleSavedStatusSub's. */
75 };
76 
77 /*
78  * TODO: If a PurpleStatusType is deleted, need to also delete any
79  *       associated PurpleSavedStatusSub's?
80  */
81 struct _PurpleSavedStatusSub
82 {
83 	PurpleAccount *account;
84 	const PurpleStatusType *type;
85 	char *message;
86 };
87 
88 static GList      *saved_statuses = NULL;
89 static guint       save_timer = 0;
90 static gboolean    statuses_loaded = FALSE;
91 
92 /*
93  * This hash table keeps track of which timestamps we've
94  * used so that we don't have two saved statuses with the
95  * same 'creation_time' timestamp.  The 'created' timestamp
96  * is used as a unique identifier.
97  *
98  * So the key in this hash table is the creation_time and
99  * the value is a pointer to the PurpleSavedStatus.
100  */
101 static GHashTable *creation_times;
102 
103 static void schedule_save(void);
104 
105 /*********************************************************************
106  * Private utility functions                                         *
107  *********************************************************************/
108 
109 static void
free_saved_status_sub(PurpleSavedStatusSub * substatus)110 free_saved_status_sub(PurpleSavedStatusSub *substatus)
111 {
112 	g_return_if_fail(substatus != NULL);
113 
114 	g_free(substatus->message);
115 	purple_request_close_with_handle(substatus);
116 	PURPLE_DBUS_UNREGISTER_POINTER(substatus);
117 	g_free(substatus);
118 }
119 
120 static void
free_saved_status(PurpleSavedStatus * status)121 free_saved_status(PurpleSavedStatus *status)
122 {
123 	g_return_if_fail(status != NULL);
124 
125 	g_free(status->title);
126 	g_free(status->message);
127 
128 	while (status->substatuses != NULL)
129 	{
130 		PurpleSavedStatusSub *substatus = status->substatuses->data;
131 		status->substatuses = g_list_remove(status->substatuses, substatus);
132 		free_saved_status_sub(substatus);
133 	}
134 	purple_request_close_with_handle(status);
135 	PURPLE_DBUS_UNREGISTER_POINTER(status);
136 	g_free(status);
137 }
138 
139 /*
140  * Set the timestamp for when this saved status was created, and
141  * make sure it is unique.
142  */
143 static void
set_creation_time(PurpleSavedStatus * status,time_t creation_time)144 set_creation_time(PurpleSavedStatus *status, time_t creation_time)
145 {
146 	g_return_if_fail(status != NULL);
147 
148 	/* Avoid using 0 because it's an invalid hash key */
149 	status->creation_time = creation_time != 0 ? creation_time : 1;
150 
151 	while (g_hash_table_lookup(creation_times, (gconstpointer)status->creation_time) != NULL)
152 		status->creation_time++;
153 
154 	g_hash_table_insert(creation_times,
155 						(gpointer)status->creation_time,
156 						status);
157 }
158 
159 /**
160  * A magic number is calculated for each status, and then the
161  * statuses are ordered by the magic number.  The magic number
162  * is the date the status was last used offset by one day for
163  * each time the status has been used (but only by 10 days at
164  * the most).
165  *
166  * The goal is to have recently used statuses at the top of
167  * the list, but to also keep frequently used statuses near
168  * the top.
169  */
170 static gint
saved_statuses_sort_func(gconstpointer a,gconstpointer b)171 saved_statuses_sort_func(gconstpointer a, gconstpointer b)
172 {
173 	const PurpleSavedStatus *saved_status_a = a;
174 	const PurpleSavedStatus *saved_status_b = b;
175 	time_t time_a = saved_status_a->lastused +
176 						(MIN(saved_status_a->usage_count, 10) * 86400);
177 	time_t time_b = saved_status_b->lastused +
178 						(MIN(saved_status_b->usage_count, 10) * 86400);
179 	if (time_a > time_b)
180 		return -1;
181 	if (time_a < time_b)
182 		return 1;
183 	return 0;
184 }
185 
186 /**
187  * Transient statuses are added and removed automatically by
188  * Purple.  If they're not used for a certain length of time then
189  * they'll expire and be automatically removed.  This function
190  * does the expiration.
191  */
192 static void
remove_old_transient_statuses(void)193 remove_old_transient_statuses(void)
194 {
195 	GList *l, *next;
196 	PurpleSavedStatus *saved_status, *startup_status, *current_status;
197 	int count;
198 	time_t creation_time;
199 
200 	startup_status = purple_savedstatus_get_startup();
201 	current_status = purple_savedstatus_get_current();
202 
203 	/*
204 	 * Iterate through the list of saved statuses.  Delete all
205 	 * transient statuses except for the first MAX_TRANSIENTS
206 	 * (remember, the saved statuses are already sorted by popularity).
207 	 * We should also keep the startup status, if any is set.
208 	 */
209 	count = 0;
210 	for (l = saved_statuses; l != NULL; l = next)
211 	{
212 		next = l->next;
213 		saved_status = l->data;
214 		if (purple_savedstatus_is_transient(saved_status))
215 		{
216 			if (count == MAX_TRANSIENTS)
217 			{
218 				if (saved_status != current_status && saved_status != startup_status)
219 				{
220 					saved_statuses = g_list_remove(saved_statuses, saved_status);
221 					creation_time = purple_savedstatus_get_creation_time(saved_status);
222 					g_hash_table_remove(creation_times, (gconstpointer)creation_time);
223 					free_saved_status(saved_status);
224 				}
225 			}
226 			else
227 				count++;
228 		}
229 	}
230 
231 	if (count == MAX_TRANSIENTS)
232 		schedule_save();
233 }
234 
235 /*********************************************************************
236  * Writing to disk                                                   *
237  *********************************************************************/
238 
239 static xmlnode *
substatus_to_xmlnode(PurpleSavedStatusSub * substatus)240 substatus_to_xmlnode(PurpleSavedStatusSub *substatus)
241 {
242 	xmlnode *node, *child;
243 
244 	node = xmlnode_new("substatus");
245 
246 	child = xmlnode_new_child(node, "account");
247 	xmlnode_set_attrib(child, "protocol", purple_account_get_protocol_id(substatus->account));
248 	xmlnode_insert_data(child,
249 			purple_normalize(substatus->account,
250 				purple_account_get_username(substatus->account)), -1);
251 
252 	child = xmlnode_new_child(node, "state");
253 	xmlnode_insert_data(child, purple_status_type_get_id(substatus->type), -1);
254 
255 	if (substatus->message != NULL)
256 	{
257 		child = xmlnode_new_child(node, "message");
258 		xmlnode_insert_data(child, substatus->message, -1);
259 	}
260 
261 	return node;
262 }
263 
264 static xmlnode *
status_to_xmlnode(PurpleSavedStatus * status)265 status_to_xmlnode(PurpleSavedStatus *status)
266 {
267 	xmlnode *node, *child;
268 	char buf[21];
269 	GList *cur;
270 
271 	node = xmlnode_new("status");
272 	if (status->title != NULL)
273 	{
274 		xmlnode_set_attrib(node, "name", status->title);
275 	}
276 	else
277 	{
278 		/*
279 		 * Purple 1.5.0 and earlier require a name to be set, so we
280 		 * do this little hack to maintain backward compatability
281 		 * in the status.xml file.  Eventually this should be removed
282 		 * and we should determine if a status is transient by
283 		 * whether the "name" attribute is set to something or if
284 		 * it does not exist at all.
285 		 */
286 		xmlnode_set_attrib(node, "name", "Auto-Cached");
287 		xmlnode_set_attrib(node, "transient", "true");
288 	}
289 
290 	g_snprintf(buf, sizeof(buf), "%lu", status->creation_time);
291 	xmlnode_set_attrib(node, "created", buf);
292 
293 	g_snprintf(buf, sizeof(buf), "%lu", status->lastused);
294 	xmlnode_set_attrib(node, "lastused", buf);
295 
296 	g_snprintf(buf, sizeof(buf), "%u", status->usage_count);
297 	xmlnode_set_attrib(node, "usage_count", buf);
298 
299 	child = xmlnode_new_child(node, "state");
300 	xmlnode_insert_data(child, purple_primitive_get_id_from_type(status->type), -1);
301 
302 	if (status->message != NULL)
303 	{
304 		child = xmlnode_new_child(node, "message");
305 		xmlnode_insert_data(child, status->message, -1);
306 	}
307 
308 	for (cur = status->substatuses; cur != NULL; cur = cur->next)
309 	{
310 		child = substatus_to_xmlnode(cur->data);
311 		xmlnode_insert_child(node, child);
312 	}
313 
314 	return node;
315 }
316 
317 static xmlnode *
statuses_to_xmlnode(void)318 statuses_to_xmlnode(void)
319 {
320 	xmlnode *node, *child;
321 	GList *cur;
322 
323 	node = xmlnode_new("statuses");
324 	xmlnode_set_attrib(node, "version", "1.0");
325 
326 	for (cur = saved_statuses; cur != NULL; cur = cur->next)
327 	{
328 		child = status_to_xmlnode(cur->data);
329 		xmlnode_insert_child(node, child);
330 	}
331 
332 	return node;
333 }
334 
335 static void
sync_statuses(void)336 sync_statuses(void)
337 {
338 	xmlnode *node;
339 	char *data;
340 
341 	if (!statuses_loaded)
342 	{
343 		purple_debug_error("status", "Attempted to save statuses before they "
344 						 "were read!\n");
345 		return;
346 	}
347 
348 	node = statuses_to_xmlnode();
349 	data = xmlnode_to_formatted_str(node, NULL);
350 	purple_util_write_data_to_file("status.xml", data, -1);
351 	g_free(data);
352 	xmlnode_free(node);
353 }
354 
355 static gboolean
save_cb(gpointer data)356 save_cb(gpointer data)
357 {
358 	sync_statuses();
359 	save_timer = 0;
360 	return FALSE;
361 }
362 
363 static void
schedule_save(void)364 schedule_save(void)
365 {
366 	if (save_timer == 0)
367 		save_timer = purple_timeout_add_seconds(5, save_cb, NULL);
368 }
369 
370 
371 /*********************************************************************
372  * Reading from disk                                                 *
373  *********************************************************************/
374 
375 static PurpleSavedStatusSub *
parse_substatus(xmlnode * substatus)376 parse_substatus(xmlnode *substatus)
377 {
378 	PurpleSavedStatusSub *ret;
379 	xmlnode *node;
380 	char *data;
381 
382 	ret = g_new0(PurpleSavedStatusSub, 1);
383 
384 	/* Read the account */
385 	node = xmlnode_get_child(substatus, "account");
386 	if (node != NULL)
387 	{
388 		char *acct_name;
389 		const char *protocol;
390 		acct_name = xmlnode_get_data(node);
391 		protocol = xmlnode_get_attrib(node, "protocol");
392 		protocol = _purple_oscar_convert(acct_name, protocol); /* XXX: Remove */
393 		if ((acct_name != NULL) && (protocol != NULL))
394 			ret->account = purple_accounts_find(acct_name, protocol);
395 		g_free(acct_name);
396 	}
397 
398 	if (ret->account == NULL)
399 	{
400 		g_free(ret);
401 		return NULL;
402 	}
403 
404 	/* Read the state */
405 	node = xmlnode_get_child(substatus, "state");
406 	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
407 	{
408 		ret->type = purple_status_type_find_with_id(
409 							ret->account->status_types, data);
410 		g_free(data);
411 	}
412 
413 	if (ret->type == NULL)
414 	{
415 		g_free(ret);
416 		return NULL;
417 	}
418 
419 	/* Read the message */
420 	node = xmlnode_get_child(substatus, "message");
421 	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
422 	{
423 		ret->message = data;
424 	}
425 
426 	PURPLE_DBUS_REGISTER_POINTER(ret, PurpleSavedStatusSub);
427 	return ret;
428 }
429 
430 /**
431  * Parse a saved status and add it to the saved_statuses linked list.
432  *
433  * Here's an example of the XML for a saved status:
434  *   <status name="Girls">
435  *       <state>away</state>
436  *       <message>I like the way that they walk
437  *   And it's chill to hear them talk
438  *   And I can always make them smile
439  *   From White Castle to the Nile</message>
440  *       <substatus>
441  *           <account protocol='prpl-aim'>markdoliner</account>
442  *           <state>available</state>
443  *           <message>The ladies man is here to answer your queries.</message>
444  *       </substatus>
445  *       <substatus>
446  *           <account protocol='prpl-aim'>giantgraypanda</account>
447  *           <state>away</state>
448  *           <message>A.C. ain't in charge no more.</message>
449  *       </substatus>
450  *   </status>
451  *
452  * I know.  Moving, huh?
453  */
454 static PurpleSavedStatus *
parse_status(xmlnode * status)455 parse_status(xmlnode *status)
456 {
457 	PurpleSavedStatus *ret;
458 	xmlnode *node;
459 	const char *attrib;
460 	char *data;
461 	int i;
462 
463 	ret = g_new0(PurpleSavedStatus, 1);
464 
465 	attrib = xmlnode_get_attrib(status, "transient");
466 	if (!purple_strequal(attrib, "true"))
467 	{
468 		/* Read the title */
469 		attrib = xmlnode_get_attrib(status, "name");
470 		ret->title = g_strdup(attrib);
471 	}
472 
473 	if (ret->title != NULL)
474 	{
475 		/* Ensure the title is unique */
476 		i = 2;
477 		while (purple_savedstatus_find(ret->title) != NULL)
478 		{
479 			g_free(ret->title);
480 			ret->title = g_strdup_printf("%s %d", attrib, i);
481 			i++;
482 		}
483 	}
484 
485 	/* Read the creation time */
486 	attrib = xmlnode_get_attrib(status, "created");
487 	set_creation_time(ret, (attrib != NULL ? atol(attrib) : 0));
488 
489 	/* Read the last used time */
490 	attrib = xmlnode_get_attrib(status, "lastused");
491 	ret->lastused = (attrib != NULL ? atol(attrib) : 0);
492 
493 	/* Read the usage count */
494 	attrib = xmlnode_get_attrib(status, "usage_count");
495 	ret->usage_count = (attrib != NULL ? atol(attrib) : 0);
496 
497 	/* Read the primitive status type */
498 	node = xmlnode_get_child(status, "state");
499 	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
500 	{
501 		ret->type = purple_primitive_get_type_from_id(data);
502 		g_free(data);
503 	}
504 
505 	/* Read the message */
506 	node = xmlnode_get_child(status, "message");
507 	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
508 	{
509 		ret->message = data;
510 	}
511 
512 	/* Read substatuses */
513 	for (node = xmlnode_get_child(status, "substatus"); node != NULL;
514 			node = xmlnode_get_next_twin(node))
515 	{
516 		PurpleSavedStatusSub *new;
517 		new = parse_substatus(node);
518 		if (new != NULL)
519 			ret->substatuses = g_list_prepend(ret->substatuses, new);
520 	}
521 
522 	PURPLE_DBUS_REGISTER_POINTER(ret, PurpleSavedStatus);
523 	return ret;
524 }
525 
526 /**
527  * Read the saved statuses from a file in the Purple user dir.
528  *
529  * @return TRUE on success, FALSE on failure (if the file can not
530  *         be opened, or if it contains invalid XML).
531  */
532 static void
load_statuses(void)533 load_statuses(void)
534 {
535 	xmlnode *statuses, *status;
536 
537 	statuses_loaded = TRUE;
538 
539 	statuses = purple_util_read_xml_from_file("status.xml", _("saved statuses"));
540 
541 	if (statuses == NULL)
542 		return;
543 
544 	for (status = xmlnode_get_child(statuses, "status"); status != NULL;
545 			status = xmlnode_get_next_twin(status))
546 	{
547 		PurpleSavedStatus *new;
548 		new = parse_status(status);
549 		saved_statuses = g_list_prepend(saved_statuses, new);
550 	}
551 	saved_statuses = g_list_sort(saved_statuses, saved_statuses_sort_func);
552 
553 	xmlnode_free(statuses);
554 }
555 
556 
557 /**************************************************************************
558 * Saved status API
559 **************************************************************************/
560 PurpleSavedStatus *
purple_savedstatus_new(const char * title,PurpleStatusPrimitive type)561 purple_savedstatus_new(const char *title, PurpleStatusPrimitive type)
562 {
563 	PurpleSavedStatus *status;
564 
565 	/* Make sure we don't already have a saved status with this title. */
566 	if (title != NULL)
567 		g_return_val_if_fail(purple_savedstatus_find(title) == NULL, NULL);
568 
569 	status = g_new0(PurpleSavedStatus, 1);
570 	PURPLE_DBUS_REGISTER_POINTER(status, PurpleSavedStatus);
571 	status->title = g_strdup(title);
572 	status->type = type;
573 	set_creation_time(status, time(NULL));
574 
575 	saved_statuses = g_list_insert_sorted(saved_statuses, status, saved_statuses_sort_func);
576 
577 	schedule_save();
578 
579 	purple_signal_emit(purple_savedstatuses_get_handle(), "savedstatus-added",
580 		status);
581 
582 	return status;
583 }
584 
585 void
purple_savedstatus_set_title(PurpleSavedStatus * status,const char * title)586 purple_savedstatus_set_title(PurpleSavedStatus *status, const char *title)
587 {
588 	g_return_if_fail(status != NULL);
589 
590 	/* Make sure we don't already have a saved status with this title. */
591 	g_return_if_fail(purple_savedstatus_find(title) == NULL);
592 
593 	g_free(status->title);
594 	status->title = g_strdup(title);
595 
596 	schedule_save();
597 
598 	purple_signal_emit(purple_savedstatuses_get_handle(),
599 			"savedstatus-modified", status);
600 }
601 
602 void
purple_savedstatus_set_type(PurpleSavedStatus * status,PurpleStatusPrimitive type)603 purple_savedstatus_set_type(PurpleSavedStatus *status, PurpleStatusPrimitive type)
604 {
605 	g_return_if_fail(status != NULL);
606 
607 	status->type = type;
608 
609 	schedule_save();
610 	purple_signal_emit(purple_savedstatuses_get_handle(),
611 			"savedstatus-modified", status);
612 }
613 
614 void
purple_savedstatus_set_message(PurpleSavedStatus * status,const char * message)615 purple_savedstatus_set_message(PurpleSavedStatus *status, const char *message)
616 {
617 	g_return_if_fail(status != NULL);
618 
619 	g_free(status->message);
620 	if ((message != NULL) && (*message == '\0'))
621 		status->message = NULL;
622 	else
623 		status->message = g_strdup(message);
624 
625 	schedule_save();
626 
627 	purple_signal_emit(purple_savedstatuses_get_handle(),
628 			"savedstatus-modified", status);
629 }
630 
631 void
purple_savedstatus_set_substatus(PurpleSavedStatus * saved_status,const PurpleAccount * account,const PurpleStatusType * type,const char * message)632 purple_savedstatus_set_substatus(PurpleSavedStatus *saved_status,
633 							   const PurpleAccount *account,
634 							   const PurpleStatusType *type,
635 							   const char *message)
636 {
637 	PurpleSavedStatusSub *substatus;
638 
639 	g_return_if_fail(saved_status != NULL);
640 	g_return_if_fail(account      != NULL);
641 	g_return_if_fail(type         != NULL);
642 
643 	/* Find an existing substatus or create a new one */
644 	substatus = purple_savedstatus_get_substatus(saved_status, account);
645 	if (substatus == NULL)
646 	{
647 		substatus = g_new0(PurpleSavedStatusSub, 1);
648 		PURPLE_DBUS_REGISTER_POINTER(substatus, PurpleSavedStatusSub);
649 		substatus->account = (PurpleAccount *)account;
650 		saved_status->substatuses = g_list_prepend(saved_status->substatuses, substatus);
651 	}
652 
653 	substatus->type = type;
654 	g_free(substatus->message);
655 	substatus->message = g_strdup(message);
656 
657 	schedule_save();
658 	purple_signal_emit(purple_savedstatuses_get_handle(),
659 			"savedstatus-modified", saved_status);
660 }
661 
662 void
purple_savedstatus_unset_substatus(PurpleSavedStatus * saved_status,const PurpleAccount * account)663 purple_savedstatus_unset_substatus(PurpleSavedStatus *saved_status,
664 								 const PurpleAccount *account)
665 {
666 	GList *iter;
667 	PurpleSavedStatusSub *substatus;
668 
669 	g_return_if_fail(saved_status != NULL);
670 	g_return_if_fail(account      != NULL);
671 
672 	for (iter = saved_status->substatuses; iter != NULL; iter = iter->next)
673 	{
674 		substatus = iter->data;
675 		if (substatus->account == account)
676 		{
677 			saved_status->substatuses = g_list_delete_link(saved_status->substatuses, iter);
678 			g_free(substatus->message);
679 			g_free(substatus);
680 			return;
681 		}
682 	}
683 
684 	purple_signal_emit(purple_savedstatuses_get_handle(),
685 			"savedstatus-modified", saved_status);
686 }
687 
688 /*
689  * This gets called when an account is deleted.  We iterate through
690  * all of our saved statuses and delete any substatuses that may
691  * exist for this account.
692  */
693 static void
purple_savedstatus_unset_all_substatuses(const PurpleAccount * account,gpointer user_data)694 purple_savedstatus_unset_all_substatuses(const PurpleAccount *account,
695 		gpointer user_data)
696 {
697 	GList *iter;
698 	PurpleSavedStatus *status;
699 
700 	g_return_if_fail(account != NULL);
701 
702 	for (iter = saved_statuses; iter != NULL; iter = iter->next)
703 	{
704 		status = (PurpleSavedStatus *)iter->data;
705 		purple_savedstatus_unset_substatus(status, account);
706 	}
707 }
708 
709 void
purple_savedstatus_delete_by_status(PurpleSavedStatus * status)710 purple_savedstatus_delete_by_status(PurpleSavedStatus *status)
711 {
712 	time_t creation_time, current, idleaway;
713 
714 	g_return_if_fail(status != NULL);
715 
716 	saved_statuses = g_list_remove(saved_statuses, status);
717 	creation_time = purple_savedstatus_get_creation_time(status);
718 	g_hash_table_remove(creation_times, (gconstpointer)creation_time);
719 	free_saved_status(status);
720 
721 	schedule_save();
722 
723 	/*
724 	 * If we just deleted our current status or our idleaway status,
725 	 * then set the appropriate pref back to 0.
726 	 */
727 	current = purple_prefs_get_int("/purple/savedstatus/default");
728 	if (current == creation_time)
729 		purple_prefs_set_int("/purple/savedstatus/default", 0);
730 
731 	idleaway = purple_prefs_get_int("/purple/savedstatus/idleaway");
732 	if (idleaway == creation_time)
733 		purple_prefs_set_int("/purple/savedstatus/idleaway", 0);
734 
735 	purple_signal_emit(purple_savedstatuses_get_handle(),
736 			"savedstatus-deleted", status);
737 }
738 
739 gboolean
purple_savedstatus_delete(const char * title)740 purple_savedstatus_delete(const char *title)
741 {
742 	PurpleSavedStatus *status;
743 
744 	status = purple_savedstatus_find(title);
745 
746 	if (status == NULL)
747 		return FALSE;
748 
749 	if (purple_savedstatus_get_current() == status)
750 		return FALSE;
751 
752 	purple_savedstatus_delete_by_status(status);
753 
754 	return TRUE;
755 }
756 
757 GList *
purple_savedstatuses_get_all(void)758 purple_savedstatuses_get_all(void)
759 {
760 	return saved_statuses;
761 }
762 
763 GList *
purple_savedstatuses_get_popular(unsigned int how_many)764 purple_savedstatuses_get_popular(unsigned int how_many)
765 {
766 	GList *popular = NULL;
767 	GList *cur;
768 	unsigned int i;
769 	PurpleSavedStatus *next;
770 
771 	/* Copy 'how_many' elements to a new list. If 'how_many' is 0, then copy all of 'em. */
772 	if (how_many == 0)
773 		how_many = (unsigned int) -1;
774 
775 	i = 0;
776 	cur = saved_statuses;
777 	while ((i < how_many) && (cur != NULL))
778 	{
779 		next = cur->data;
780 		if ((!purple_savedstatus_is_transient(next)
781 			|| purple_savedstatus_get_message(next) != NULL))
782 		{
783 			popular = g_list_prepend(popular, next);
784 			i++;
785 		}
786 		cur = cur->next;
787 	}
788 
789 	popular = g_list_reverse(popular);
790 
791 	return popular;
792 }
793 
794 PurpleSavedStatus *
purple_savedstatus_get_current(void)795 purple_savedstatus_get_current(void)
796 {
797 	if (purple_savedstatus_is_idleaway())
798 		return purple_savedstatus_get_idleaway();
799 	else
800 		return purple_savedstatus_get_default();
801 }
802 
803 PurpleSavedStatus *
purple_savedstatus_get_default()804 purple_savedstatus_get_default()
805 {
806 	time_t creation_time;
807 	PurpleSavedStatus *saved_status = NULL;
808 
809 	creation_time = purple_prefs_get_int("/purple/savedstatus/default");
810 
811 	if (creation_time != 0)
812 		saved_status = g_hash_table_lookup(creation_times, (gconstpointer)creation_time);
813 
814 	if (saved_status == NULL)
815 	{
816 		/*
817 		 * We don't have a current saved status!  This is either a new
818 		 * Purple user or someone upgrading from Purple 1.5.0 or older, or
819 		 * possibly someone who deleted the status they were currently
820 		 * using?  In any case, add a default status.
821 		 */
822 		saved_status = purple_savedstatus_new(NULL, PURPLE_STATUS_AVAILABLE);
823 		purple_prefs_set_int("/purple/savedstatus/default",
824 						   purple_savedstatus_get_creation_time(saved_status));
825 	}
826 
827 	return saved_status;
828 }
829 
830 PurpleSavedStatus *
purple_savedstatus_get_idleaway()831 purple_savedstatus_get_idleaway()
832 {
833 	time_t creation_time;
834 	PurpleSavedStatus *saved_status = NULL;
835 
836 	creation_time = purple_prefs_get_int("/purple/savedstatus/idleaway");
837 
838 	if (creation_time != 0)
839 		saved_status = g_hash_table_lookup(creation_times, (gconstpointer)creation_time);
840 
841 	if (saved_status == NULL)
842 	{
843 		/* We don't have a specified "idle" status!  Weird. */
844 		saved_status = purple_savedstatus_find_transient_by_type_and_message(
845 				PURPLE_STATUS_AWAY, DEFAULT_AUTOAWAY_MESSAGE);
846 
847 		if (saved_status == NULL)
848 		{
849 			saved_status = purple_savedstatus_new(NULL, PURPLE_STATUS_AWAY);
850 			purple_savedstatus_set_message(saved_status, DEFAULT_AUTOAWAY_MESSAGE);
851 			purple_prefs_set_int("/purple/savedstatus/idleaway",
852 							   purple_savedstatus_get_creation_time(saved_status));
853 		}
854 	}
855 
856 	return saved_status;
857 }
858 
859 gboolean
purple_savedstatus_is_idleaway()860 purple_savedstatus_is_idleaway()
861 {
862 	return purple_prefs_get_bool("/purple/savedstatus/isidleaway");
863 }
864 
865 void
purple_savedstatus_set_idleaway(gboolean idleaway)866 purple_savedstatus_set_idleaway(gboolean idleaway)
867 {
868 	GList *accounts, *node;
869 	PurpleSavedStatus *old, *saved_status;
870 
871 	if (purple_savedstatus_is_idleaway() == idleaway)
872 		/* Don't need to do anything */
873 		return;
874 
875 	old = purple_savedstatus_get_current();
876 	saved_status = idleaway ? purple_savedstatus_get_idleaway()
877 			: purple_savedstatus_get_default();
878 	purple_prefs_set_bool("/purple/savedstatus/isidleaway", idleaway);
879 
880 	/* Changing our status makes us un-idle */
881 	if (!idleaway)
882 		purple_idle_touch();
883 
884 	if (idleaway && (purple_savedstatus_get_type(old) != PURPLE_STATUS_AVAILABLE))
885 		/* Our global status is already "away," so don't change anything */
886 		return;
887 
888 	accounts = purple_accounts_get_all_active();
889 	for (node = accounts; node != NULL; node = node->next)
890 	{
891 		PurpleAccount *account;
892 		PurplePresence *presence;
893 		PurpleStatus *status;
894 
895 		account = node->data;
896 		presence = purple_account_get_presence(account);
897 		status = purple_presence_get_active_status(presence);
898 
899 		if (!idleaway || purple_status_is_available(status))
900 			purple_savedstatus_activate_for_account(saved_status, account);
901 	}
902 
903 	g_list_free(accounts);
904 
905 	purple_signal_emit(purple_savedstatuses_get_handle(), "savedstatus-changed",
906 					 saved_status, old);
907 }
908 
909 PurpleSavedStatus *
purple_savedstatus_get_startup()910 purple_savedstatus_get_startup()
911 {
912 	time_t creation_time;
913 	PurpleSavedStatus *saved_status = NULL;
914 
915 	creation_time = purple_prefs_get_int("/purple/savedstatus/startup");
916 
917 	if (creation_time != 0)
918 		saved_status = g_hash_table_lookup(creation_times, (gconstpointer)creation_time);
919 
920 	if (saved_status == NULL)
921 	{
922 		/*
923 		 * We don't have a status to apply.
924 		 * This may be the first login, or the user wants to
925 		 * restore the "current" status.
926 		 */
927 		saved_status = purple_savedstatus_get_current();
928 	}
929 
930 	return saved_status;
931 }
932 
933 
934 PurpleSavedStatus *
purple_savedstatus_find(const char * title)935 purple_savedstatus_find(const char *title)
936 {
937 	GList *iter;
938 	PurpleSavedStatus *status;
939 
940 	g_return_val_if_fail(title != NULL, NULL);
941 
942 	for (iter = saved_statuses; iter != NULL; iter = iter->next)
943 	{
944 		status = (PurpleSavedStatus *)iter->data;
945 		if (purple_strequal(status->title, title))
946 			return status;
947 	}
948 
949 	return NULL;
950 }
951 
952 PurpleSavedStatus *
purple_savedstatus_find_by_creation_time(time_t creation_time)953 purple_savedstatus_find_by_creation_time(time_t creation_time)
954 {
955 	GList *iter;
956 	PurpleSavedStatus *status;
957 
958 	for (iter = saved_statuses; iter != NULL; iter = iter->next)
959 	{
960 		status = (PurpleSavedStatus *)iter->data;
961 		if (status->creation_time == creation_time)
962 			return status;
963 	}
964 
965 	return NULL;
966 }
967 
968 PurpleSavedStatus *
purple_savedstatus_find_transient_by_type_and_message(PurpleStatusPrimitive type,const char * message)969 purple_savedstatus_find_transient_by_type_and_message(PurpleStatusPrimitive type,
970 													const char *message)
971 {
972 	GList *iter;
973 	PurpleSavedStatus *status;
974 
975 	for (iter = saved_statuses; iter != NULL; iter = iter->next)
976 	{
977 		status = (PurpleSavedStatus *)iter->data;
978 		if ((status->type == type) && purple_savedstatus_is_transient(status) &&
979 			!purple_savedstatus_has_substatuses(status) &&
980 			purple_strequal(status->message, message))
981 		{
982 			return status;
983 		}
984 	}
985 
986 	return NULL;
987 }
988 
989 gboolean
purple_savedstatus_is_transient(const PurpleSavedStatus * saved_status)990 purple_savedstatus_is_transient(const PurpleSavedStatus *saved_status)
991 {
992 	g_return_val_if_fail(saved_status != NULL, TRUE);
993 
994 	return (saved_status->title == NULL);
995 }
996 
997 const char *
purple_savedstatus_get_title(const PurpleSavedStatus * saved_status)998 purple_savedstatus_get_title(const PurpleSavedStatus *saved_status)
999 {
1000 	const char *message;
1001 
1002 	g_return_val_if_fail(saved_status != NULL, NULL);
1003 
1004 	/* If we have a title then return it */
1005 	if (saved_status->title != NULL)
1006 		return saved_status->title;
1007 
1008 	/* Otherwise, this is a transient status and we make up a title on the fly */
1009 	message = purple_savedstatus_get_message(saved_status);
1010 
1011 	if ((message == NULL) || (*message == '\0'))
1012 	{
1013 		PurpleStatusPrimitive primitive;
1014 		primitive = purple_savedstatus_get_type(saved_status);
1015 		return purple_primitive_get_name_from_type(primitive);
1016 	}
1017 	else
1018 	{
1019 		char *stripped;
1020 		static char buf[64];
1021 		stripped = purple_markup_strip_html(message);
1022 		purple_util_chrreplace(stripped, '\n', ' ');
1023 		strncpy(buf, stripped, sizeof(buf));
1024 		buf[sizeof(buf) - 1] = '\0';
1025 		if ((strlen(stripped) + 1) > sizeof(buf))
1026 		{
1027 			/* Truncate and ellipsize */
1028 			char *tmp = g_utf8_find_prev_char(buf, &buf[sizeof(buf) - 4]);
1029 			strcpy(tmp, "...");
1030 		}
1031 		g_free(stripped);
1032 		return buf;
1033 	}
1034 }
1035 
1036 PurpleStatusPrimitive
purple_savedstatus_get_type(const PurpleSavedStatus * saved_status)1037 purple_savedstatus_get_type(const PurpleSavedStatus *saved_status)
1038 {
1039 	g_return_val_if_fail(saved_status != NULL, PURPLE_STATUS_OFFLINE);
1040 
1041 	return saved_status->type;
1042 }
1043 
1044 const char *
purple_savedstatus_get_message(const PurpleSavedStatus * saved_status)1045 purple_savedstatus_get_message(const PurpleSavedStatus *saved_status)
1046 {
1047 	g_return_val_if_fail(saved_status != NULL, NULL);
1048 
1049 	return saved_status->message;
1050 }
1051 
1052 time_t
purple_savedstatus_get_creation_time(const PurpleSavedStatus * saved_status)1053 purple_savedstatus_get_creation_time(const PurpleSavedStatus *saved_status)
1054 {
1055 	g_return_val_if_fail(saved_status != NULL, 0);
1056 
1057 	return saved_status->creation_time;
1058 }
1059 
1060 gboolean
purple_savedstatus_has_substatuses(const PurpleSavedStatus * saved_status)1061 purple_savedstatus_has_substatuses(const PurpleSavedStatus *saved_status)
1062 {
1063 	g_return_val_if_fail(saved_status != NULL, FALSE);
1064 
1065 	return (saved_status->substatuses != NULL);
1066 }
1067 
1068 PurpleSavedStatusSub *
purple_savedstatus_get_substatus(const PurpleSavedStatus * saved_status,const PurpleAccount * account)1069 purple_savedstatus_get_substatus(const PurpleSavedStatus *saved_status,
1070 							   const PurpleAccount *account)
1071 {
1072 	GList *iter;
1073 	PurpleSavedStatusSub *substatus;
1074 
1075 	g_return_val_if_fail(saved_status != NULL, NULL);
1076 	g_return_val_if_fail(account      != NULL, NULL);
1077 
1078 	for (iter = saved_status->substatuses; iter != NULL; iter = iter->next)
1079 	{
1080 		substatus = iter->data;
1081 		if (substatus->account == account)
1082 			return substatus;
1083 	}
1084 
1085 	return NULL;
1086 }
1087 
1088 const PurpleStatusType *
purple_savedstatus_substatus_get_type(const PurpleSavedStatusSub * substatus)1089 purple_savedstatus_substatus_get_type(const PurpleSavedStatusSub *substatus)
1090 {
1091 	g_return_val_if_fail(substatus != NULL, NULL);
1092 
1093 	return substatus->type;
1094 }
1095 
1096 const char *
purple_savedstatus_substatus_get_message(const PurpleSavedStatusSub * substatus)1097 purple_savedstatus_substatus_get_message(const PurpleSavedStatusSub *substatus)
1098 {
1099 	g_return_val_if_fail(substatus != NULL, NULL);
1100 
1101 	return substatus->message;
1102 }
1103 
1104 void
purple_savedstatus_activate(PurpleSavedStatus * saved_status)1105 purple_savedstatus_activate(PurpleSavedStatus *saved_status)
1106 {
1107 	GList *accounts, *node;
1108 	PurpleSavedStatus *old = purple_savedstatus_get_current();
1109 
1110 	g_return_if_fail(saved_status != NULL);
1111 
1112 	/* Make sure our list of saved statuses remains sorted */
1113 	saved_status->lastused = time(NULL);
1114 	saved_status->usage_count++;
1115 	saved_statuses = g_list_remove(saved_statuses, saved_status);
1116 	saved_statuses = g_list_insert_sorted(saved_statuses, saved_status, saved_statuses_sort_func);
1117 	purple_prefs_set_int("/purple/savedstatus/default",
1118 					   purple_savedstatus_get_creation_time(saved_status));
1119 
1120 	accounts = purple_accounts_get_all_active();
1121 	for (node = accounts; node != NULL; node = node->next)
1122 	{
1123 		PurpleAccount *account;
1124 
1125 		account = node->data;
1126 
1127 		purple_savedstatus_activate_for_account(saved_status, account);
1128 	}
1129 
1130 	g_list_free(accounts);
1131 
1132 	if (purple_savedstatus_is_idleaway()) {
1133 		purple_savedstatus_set_idleaway(FALSE);
1134 	} else {
1135 		purple_signal_emit(purple_savedstatuses_get_handle(), "savedstatus-changed",
1136 					 	   saved_status, old);
1137 	}
1138 }
1139 
1140 void
purple_savedstatus_activate_for_account(const PurpleSavedStatus * saved_status,PurpleAccount * account)1141 purple_savedstatus_activate_for_account(const PurpleSavedStatus *saved_status,
1142 									  PurpleAccount *account)
1143 {
1144 	const PurpleStatusType *status_type;
1145 	const PurpleSavedStatusSub *substatus;
1146 	const char *message = NULL;
1147 
1148 	g_return_if_fail(saved_status != NULL);
1149 	g_return_if_fail(account != NULL);
1150 
1151 	substatus = purple_savedstatus_get_substatus(saved_status, account);
1152 	if (substatus != NULL)
1153 	{
1154 		status_type = substatus->type;
1155 		message = substatus->message;
1156 	}
1157 	else
1158 	{
1159 		status_type = purple_account_get_status_type_with_primitive(account, saved_status->type);
1160 		if (status_type == NULL)
1161 			return;
1162 		message = saved_status->message;
1163 	}
1164 
1165 	if ((message != NULL) &&
1166 		(purple_status_type_get_attr(status_type, "message")))
1167 	{
1168 		purple_account_set_status(account, purple_status_type_get_id(status_type),
1169 								TRUE, "message", message, NULL);
1170 	}
1171 	else
1172 	{
1173 		purple_account_set_status(account, purple_status_type_get_id(status_type),
1174 								TRUE, NULL);
1175 	}
1176 }
1177 
1178 void *
purple_savedstatuses_get_handle(void)1179 purple_savedstatuses_get_handle(void)
1180 {
1181 	static int handle;
1182 
1183 	return &handle;
1184 }
1185 
1186 void
purple_savedstatuses_init(void)1187 purple_savedstatuses_init(void)
1188 {
1189 	void *handle = purple_savedstatuses_get_handle();
1190 
1191 	creation_times = g_hash_table_new(g_direct_hash, g_direct_equal);
1192 
1193 	/*
1194 	 * Using 0 as the creation_time is a special case.
1195 	 * If someone calls purple_savedstatus_get_current() or
1196 	 * purple_savedstatus_get_idleaway() and either of those functions
1197 	 * sees a creation_time of 0, then it will create a default
1198 	 * saved status and return that to the user.
1199 	 */
1200 	purple_prefs_add_none("/purple/savedstatus");
1201 	purple_prefs_add_int("/purple/savedstatus/default", 0);
1202 	purple_prefs_add_int("/purple/savedstatus/startup", 0);
1203 	purple_prefs_add_bool("/purple/savedstatus/startup_current_status", TRUE);
1204 	purple_prefs_add_int("/purple/savedstatus/idleaway", 0);
1205 	purple_prefs_add_bool("/purple/savedstatus/isidleaway", FALSE);
1206 
1207 	load_statuses();
1208 
1209 	purple_signal_register(handle, "savedstatus-changed",
1210 					 purple_marshal_VOID__POINTER_POINTER, NULL, 2,
1211 					 purple_value_new(PURPLE_TYPE_SUBTYPE,
1212 									PURPLE_SUBTYPE_SAVEDSTATUS),
1213 					 purple_value_new(PURPLE_TYPE_SUBTYPE,
1214 									PURPLE_SUBTYPE_SAVEDSTATUS));
1215 
1216 	purple_signal_register(handle, "savedstatus-added",
1217 		purple_marshal_VOID__POINTER, NULL, 1,
1218 		purple_value_new(PURPLE_TYPE_SUBTYPE,
1219 			PURPLE_SUBTYPE_SAVEDSTATUS));
1220 
1221 	purple_signal_register(handle, "savedstatus-deleted",
1222 		purple_marshal_VOID__POINTER, NULL, 1,
1223 		purple_value_new(PURPLE_TYPE_SUBTYPE,
1224 			PURPLE_SUBTYPE_SAVEDSTATUS));
1225 
1226 	purple_signal_register(handle, "savedstatus-modified",
1227 		purple_marshal_VOID__POINTER, NULL, 1,
1228 		purple_value_new(PURPLE_TYPE_SUBTYPE,
1229 			PURPLE_SUBTYPE_SAVEDSTATUS));
1230 
1231 	purple_signal_connect(purple_accounts_get_handle(), "account-removed",
1232 			handle,
1233 			PURPLE_CALLBACK(purple_savedstatus_unset_all_substatuses),
1234 			NULL);
1235 }
1236 
1237 void
purple_savedstatuses_uninit(void)1238 purple_savedstatuses_uninit(void)
1239 {
1240 	gpointer handle = purple_savedstatuses_get_handle();
1241 
1242 	remove_old_transient_statuses();
1243 
1244 	if (save_timer != 0)
1245 	{
1246 		purple_timeout_remove(save_timer);
1247 		save_timer = 0;
1248 		sync_statuses();
1249 	}
1250 
1251 	while (saved_statuses != NULL) {
1252 		PurpleSavedStatus *saved_status = saved_statuses->data;
1253 		saved_statuses = g_list_remove(saved_statuses, saved_status);
1254 		free_saved_status(saved_status);
1255 	}
1256 
1257 	g_hash_table_destroy(creation_times);
1258 	creation_times = NULL;
1259 
1260 	purple_signals_unregister_by_instance(handle);
1261 	purple_signals_disconnect_by_handle(handle);
1262 }
1263 
1264