1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483 |
- /*
- * sapphire-backend
- *
- * Copyright (C) 2018 Alyssa Rosenzweig
- * Copyright (C) 2018 libpurple authors
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
- *
- */
- #include <stdint.h>
- #include "purple.h"
- #include <assert.h>
- #include <glib.h>
- #include <signal.h>
- #include <string.h>
- #ifndef _WIN32
- #include <unistd.h>
- #else
- #include "win32/win32dep.h"
- #endif
- #include "core.h"
- #include "websocket.h"
- #include "push.h"
- #include "event-loop.h"
- #include "json_compat.h"
- #define PLUGIN_SAVE_PREF "/purple/sapphire/plugins/saved"
- #define SAPPHIRE_PASSWORD_PREF "/purple/sapphire/password"
- #define UI_ID "sapphire"
- #define purple_serv_send_typing serv_send_typing
- #define purple_serv_join_chat serv_join_chat
- /* List of connected accounts */
- GSList *purple_accounts;
- /* List of SapphireChats, whether they are conversations yet or not */
- GSList *chats;
- /* Hash table of channel IDs to lists of unacked messages ready for replay */
- GHashTable *id_to_unacked_list;
- /* Hash table of chat IDs to PurpleChats */
- GHashTable *id_to_chat;
- /* Account ID to PurpleAccount */
- GHashTable *id_to_account;
- /* Blist Chat ID set */
- GHashTable *id_to_joined;
- /* All known buddies/other users are maintained in a hash table from network
- * serializable identifier to PurpleBuddy, since we can't transmit the buddy
- * object itself each time, this enables pass-by-reference */
- GHashTable *id_to_buddy;
- GHashTable *blist_id_to_conversation;
- /* Our internal tracking for chats, whether they are joined as
- * PurpleConversations or not. Smoothes over PurpleChat, PurpleConversation,
- * and room lists */
- typedef struct {
- const char *id; /* Unique, prpl-agnostic ID */
- const char *account_id; /* Account ID corresponding to account */
- const char *name; /* User visible name */
- const char *group; /* Group name, like from the blist */
- PurpleAccount *account;
- /* Bits needed to join, if roomlist */
- PurpleRoomlist *roomlist;
- PurpleRoomlistRoom *room;
- /* Corresponding conversation, if we have joined */
- PurpleConversation *conv;
- } SapphireChat;
- static gchar *
- sapphire_serialize_account_id(PurpleAccount *account);
- /* Creates a new heap-allocated SapphireChat. Must be freed later. */
- static SapphireChat *
- sapphire_new_chat(PurpleAccount *account, const char *id, const char *name, const char *group)
- {
- SapphireChat *schat = g_new0(SapphireChat, 1);
- schat->id = id;
- schat->account = account;
- schat->account_id = sapphire_serialize_account_id(account);
- schat->name = g_strdup(name);
- schat->group = g_strdup(group);
- return schat;
- }
- static gchar *
- sapphire_id_from_conv(PurpleConversation *chat);
- static SapphireChat *
- sapphire_chat_from_conv(PurpleConversation *conv)
- {
- return sapphire_new_chat(
- purple_conversation_get_account(conv),
- sapphire_id_from_conv(conv),
- purple_conversation_get_name(conv),
- "Chats");
- }
- /* Functions to upload icons to the proxy */
- GHashTable *sent_icons;
- static void
- sapphire_send_icon(const gchar *name, const gchar *ext, gconstpointer data, size_t size, const gchar *hash)
- {
- if (purple_strequal(g_hash_table_lookup(sent_icons, name), hash)) {
- /* Don't duplicate. */
- return;
- }
- /* If there is an active connection, send this icon. Otherwise, save
- * it to be sent later */
- gchar *base64 = g_base64_encode(data, size);
- JsonObject *obj = json_object_new();
- json_object_set_string_member(obj, "op", "icon");
- json_object_set_string_member(obj, "name", name);
- json_object_set_string_member(obj, "ext", ext);
- json_object_set_string_member(obj, "base64", base64);
- gchar *str = json_object_to_string(obj);
- gchar *str_prefixed = g_strdup_printf(">%s", str);
- /* Assume that it needs to save the string. Callee will g_free it itself in the off-chance it doesn't need it anymore */
- sapphire_send_any_or_save(str_prefixed);
- /* Mark that the icon is sent so we don't try later */
- g_hash_table_insert(sent_icons, g_strdup(name), g_strdup(hash));
- g_free(str);
- g_free(base64);
- json_object_unref(obj);
- }
- void
- sapphire_add_buddy_icon(const gchar *name, PurpleBuddyIcon *icon)
- {
- size_t size;
- gconstpointer data = purple_buddy_icon_get_data(icon, &size);
- sapphire_send_icon(name, purple_buddy_icon_get_extension(icon), data, size, purple_buddy_icon_get_checksum(icon));
- }
- void
- sapphire_add_stored_image(const gchar *name, PurpleStoredImage *icon)
- {
- sapphire_send_icon(name, purple_imgstore_get_extension(icon), purple_imgstore_get_data(icon), purple_imgstore_get_size(icon), purple_imgstore_get_filename(icon));
- }
- /* Generic purple related helpers */
- static PurpleStatus *
- sapphire_status_for_buddy(PurpleBuddy *buddy)
- {
- PurplePresence *presence = purple_buddy_get_presence(buddy);
- return purple_presence_get_active_status(presence);
- }
- static PurplePluginProtocolInfo *
- sapphire_info_for_connection(PurpleConnection *connection)
- {
- PurplePlugin *prpl = purple_connection_get_prpl(connection);
- return PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
- }
- static PurpleConvIm *
- sapphire_im_for_name(PurpleAccount *account, const char *name)
- {
- PurpleConversation *conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account);
- if (conv == NULL) {
- /* If not found, create it */
- conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name);
- }
- return purple_conversation_get_im_data(conv);
- }
- /* Search for a PurpleConversation, either as a chat or an IM. Returns NULL if
- * not found */
- static PurpleConversation *
- sapphire_conversation_for_id(const gchar *id)
- {
- PurpleConversation *as_chat = g_hash_table_lookup(blist_id_to_conversation, id);
- if (as_chat)
- return as_chat;
- PurpleBuddy *buddy = g_hash_table_lookup(id_to_buddy, id);
- if (buddy) {
- PurpleAccount *account = purple_buddy_get_account(buddy);
- const gchar *name = purple_buddy_get_name(buddy);
- return purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account);
- }
- return NULL;
- }
- /* Helper to serialize and broadcast */
- static void
- sapphire_broadcast(JsonObject *msg)
- {
- gchar *str = json_object_to_string(msg);
- sapphire_broadcast_raw_packet(str);
- g_free(str);
- }
- static void
- sapphire_send(Connection *conn, JsonObject *msg)
- {
- gchar *str = json_object_to_string(msg);
- sapphire_send_raw_packet(conn, str);
- g_free(str);
- }
- static PurpleTypingState
- sapphire_decode_typing_state(int s_state);
- static PurpleConversation *
- sapphire_find_conversation(const gchar *chat);
- static JsonArray *
- sapphire_serialize_chat_users(SapphireChat *chat);
- static PurpleBuddy *
- sapphire_decode_buddy(JsonObject *data)
- {
- const gchar *buddy_id = json_object_get_string_member(data, "buddy");
- if (!buddy_id)
- return NULL;
- /* Find the associated buddy */
- PurpleBuddy *buddy = g_hash_table_lookup(id_to_buddy, buddy_id);
- if (!buddy) {
- fprintf(stderr, "Bad buddy id %s\n", buddy_id);
- return NULL;
- }
- return buddy;
- }
- static PurpleAccount *
- sapphire_decode_account(JsonObject *data)
- {
- const gchar *account_id = json_object_get_string_member(data, "account");
- return g_hash_table_lookup(id_to_account, account_id);
- }
- void
- sapphire_process_message(Connection *conn, JsonObject *data)
- {
- const gchar *op = json_object_get_string_member(data, "op");
- if (purple_strequal(op, "message")) {
- /* Send an outgoing IM */
- const gchar *content = json_object_get_string_member(data, "content");
- /* Content is HTML, possibly OTR-encrypted so we can't do processing */
- gchar *marked = g_strdup(content);
- if (json_object_has_member(data, "buddy")) {
- PurpleBuddy *buddy = sapphire_decode_buddy(data);
- PurpleAccount *buddy_account = purple_buddy_get_account(buddy);
- const gchar *buddy_name = purple_buddy_get_name(buddy);
- purple_conv_im_send(sapphire_im_for_name(buddy_account, buddy_name), marked);
- } else if (json_object_has_member(data, "chat")) {
- const gchar *chat = json_object_get_string_member(data, "chat");
- PurpleConversation *conv = sapphire_find_conversation(chat);
- PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
- purple_conv_chat_send(conv_chat, marked);
- } else {
- fprintf(stderr, "No recipient specified in message\n");
- return;
- }
- g_free(marked);
- } else if (purple_strequal(op, "typing")) {
- /* Our buddy typing status changed */
- int s_state = json_object_get_int_member(data, "state");
- PurpleTypingState state = sapphire_decode_typing_state(s_state);
- PurpleBuddy *buddy = sapphire_decode_buddy(data);
- if (!buddy) {
- fprintf(stderr, "No buddy\n");
- return;
- }
- PurpleAccount *buddy_account = purple_buddy_get_account(buddy);
- const gchar *buddy_name = purple_buddy_get_name(buddy);
- PurpleConnection *connection = purple_account_get_connection(buddy_account);
- purple_serv_send_typing(connection, buddy_name, state);
- } else if (purple_strequal(op, "joinChat")) {
- /* Join a MUC */
- const gchar *id = json_object_get_string_member(data, "id");
- SapphireChat *chat = g_hash_table_lookup(id_to_chat, id);
- if (!chat) {
- printf("Chat not found %s\n", id);
- return;
- }
- gboolean is_subscribed = g_hash_table_contains(conn->subscribed_ids, id);
- if (g_hash_table_contains(id_to_joined, id)) {
- purple_roomlist_room_join(chat->roomlist, chat->room);
- g_hash_table_add(id_to_joined, g_strdup(id));
- } else if (!is_subscribed) {
- /* If we already joined but not in this connection, just send back details */
- const gchar *topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(chat->conv));
- JsonArray *users = sapphire_serialize_chat_users(chat);
- JsonObject *data = json_object_new();
- json_object_set_string_member(data, "op", "joined");
- json_object_set_string_member(data, "chat", id);
- json_object_set_string_member(data, "topic", topic);
- json_object_set_array_member(data, "members", users);
- sapphire_send(conn, data);
- json_object_unref(data);
- }
- if (!is_subscribed) {
- /* We want to know about this room */
- g_hash_table_add(conn->subscribed_ids, g_strdup(id));
- }
- } else if (purple_strequal(op, "topic")) {
- const gchar *chat = json_object_get_string_member(data, "chat");
- const gchar *topic = json_object_get_string_member(data, "topic");
- PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
- PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
- int id = purple_conv_chat_get_id(conv_chat);
- PurpleAccount *account = purple_conversation_get_account(conv);
- PurpleConnection *connection = purple_account_get_connection(account);
- PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
- if (prpl_info && prpl_info->set_chat_topic)
- prpl_info->set_chat_topic(connection, id, topic);
- else
- printf("Set chat topic unimplemented\n");
- } else if (purple_strequal(op, "markAsRead")) {
- const gchar *id = json_object_get_string_member(data, "id");
- /* Free the unacked list entries */
- GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, id);
- for (GList *it = unacked_list; it != NULL; it = it->next) {
- JsonObject *msg = (JsonObject *) it->data;
- json_object_unref(msg);
- }
- /* Free the list itself */
- g_list_free(unacked_list);
- /* And remove it from the hash table */
- g_hash_table_remove(id_to_unacked_list, id);
- /* Fake a PURPLE_CONV_UPDATE_UNSEEN signal, so that the room gets
- * marked as read.
- */
- PurpleConversation *conv = sapphire_conversation_for_id(id);
- if (!conv) {
- fprintf(stderr, "Conversation not found in markAsRead %s\n", id);
- return;
- }
- purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
- } else if (purple_strequal(op, "requestBuddy")) {
- /* Request to add a buddy */
- const gchar *id = json_object_get_string_member(data, "id");
- const gchar *alias = json_object_get_string_member(data, "alias");
- const gchar *invite = json_object_get_string_member(data, "invite");
- PurpleAccount *account = sapphire_decode_account(data);
- PurpleBuddy *buddy = purple_buddy_new(account, id, alias);
- purple_blist_add_buddy(buddy, NULL, NULL, NULL);
- purple_account_add_buddy_with_invite(account, buddy, invite);
- } else if (purple_strequal(op, "changeAvatar")) {
- /* Request to change our avatar */
- PurpleAccount *account = sapphire_decode_account(data);
- const gchar *base64 = json_object_get_string_member(data, "base64");
- size_t len;
- guchar *l_data = g_base64_decode(base64, &len);
- PurpleStoredImage *icon = purple_buddy_icons_set_account_icon(account, l_data, len);
- /* Update the cache */
- const gchar *raw_acct = json_object_get_string_member(data, "account");
- sapphire_add_stored_image(raw_acct, icon);
- /* Respond that we did it! */
- JsonObject *resp = json_object_new();
- json_object_set_string_member(resp, "op", "changeAvatar");
- json_object_set_string_member(resp, "id", raw_acct);
- sapphire_broadcast(resp);
- json_object_unref(resp);
- } else {
- fprintf(stderr, "Unknown op %s\n", op);
- }
- }
- /*** Conversation uiops ***/
- static gchar *
- sapphire_id_from_parts(PurpleAccount *account, const gchar *id);
- static void
- sapphire_signed_on(PurpleAccount *account, gpointer null)
- {
- PurpleConnection *connection = purple_account_get_connection(account);
- /* Upsert the account ID */
- gchar *acct_id = sapphire_serialize_account_id(account);
- if (g_hash_table_contains(id_to_account, acct_id)) {
- /* Wait. We already did this account. Bail! TODO: Sync */
- g_free(acct_id);
- return;
- }
- g_hash_table_insert(id_to_account, acct_id, account);
- /* For type-1 prpls where the openness of a chat determines whether we
- * receive events (e.g. IRC), open up all chats as early as possible */
- PurpleBlistNode *node;
- for ( node = purple_blist_get_root();
- node != NULL;
- node = purple_blist_node_next(node, TRUE)) {
- if (PURPLE_BLIST_NODE_IS_CHAT(node)) {
- PurpleChat *chat = PURPLE_CHAT(node);
- if (purple_chat_get_account(chat) != account) continue;
- GHashTable *components = purple_chat_get_components(chat);
- purple_serv_join_chat(connection, components);
- }
- }
- /* For type-2 prpls where we fetch from the room list (e.g. Discord),
- * fetch now but do not open yet, since we don't want to spam the
- * servers */
- if (purple_strequal(account->protocol_id, "prpl-eionrobb-discord")) {
- PurpleRoomlist *roomlist = purple_roomlist_get_list(connection);
- /* We're persisting the roomlist until later */
- purple_roomlist_ref(roomlist);
- gboolean in_progress = purple_roomlist_get_in_progress(roomlist);
- if (in_progress) {
- printf("In progress room list, aborting\n");
- return;
- }
- /* Check the field headings to figure out what to display (name) and index by (ID) */
- GList *field_headings = purple_roomlist_get_fields(roomlist);
- int index_id = -1, index_name = -1;
- int field_idx = 0;
- for (; field_headings != NULL; field_headings = field_headings->next, ++field_idx) {
- PurpleRoomlistField *field = (PurpleRoomlistField *) field_headings->data;
- const char *label = purple_roomlist_field_get_label(field);
- gboolean hidden = purple_roomlist_field_get_hidden(field);
- if (index_id == -1 && hidden) {
- index_id = field_idx;
- } else if (index_name == -1 && purple_strequal(label, "Name")) {
- index_name = field_idx;
- } else {
- /* Useless field */
- }
- }
- /* Now, scan the rooms */
- GList *rooms = roomlist->rooms; /* XXX: purple3 */
- for (; rooms != NULL; rooms = rooms->next) {
- PurpleRoomlistRoom *room = (PurpleRoomlistRoom *) rooms->data;
- PurpleRoomlistRoomType type = purple_roomlist_room_get_type(room);
- /* Skip over categories */
- if (type != PURPLE_ROOMLIST_ROOMTYPE_ROOM)
- continue;
- /* ...but do fetch our category name! */
- PurpleRoomlistRoom *parent = purple_roomlist_room_get_parent(room);
- const char *group_name = parent ? purple_roomlist_room_get_name(parent) : "Rooms";
- GList *fields = purple_roomlist_room_get_fields(room);
- const char *id = NULL;
- const char *display_name = NULL;
- for (int idx = 0; fields != NULL; fields = fields->next, ++idx) {
- gchar *value = (gchar *) fields->data;
- if (idx == index_id)
- id = value;
- if (idx == index_name)
- display_name = value;
- }
- /* XXX: Do magic from purple-discord to format ID */
- guint64 gid = g_ascii_strtoull(id, NULL, 10);
- int nid = ABS((gint) gid);
- gchar *snid = g_strdup_printf("%d", nid);
- gchar *sapphic_id = sapphire_id_from_parts(account, snid);
- g_free(snid);
- /* Save the chat */
- SapphireChat *schat = sapphire_new_chat(account, g_strdup(sapphic_id), display_name, group_name);
- schat->roomlist = roomlist;
- schat->room = room;
- printf("Saving Dithcord with %s\n", sapphic_id);
- chats = g_slist_prepend(chats, schat);
- /* No need to g_strdup(sapphic_id) since we already have the exclusive reference */
- g_hash_table_insert(id_to_chat, sapphic_id, schat);
- }
- }
- }
- static void
- sapphire_account_enabled(PurpleAccount *account, gpointer null)
- {
- printf("Account enabled: %s %s\n", account->username, account->protocol_id);
- }
- /* Serializes the actual content of a status */
- static void
- sapphire_serialize_status(JsonObject *data, PurpleStatus *status)
- {
- JsonObject *obj = json_object_new();
- const gchar *id = purple_status_get_id(status);
- const gchar *name = purple_status_get_name(status);
- const gchar *message = purple_status_get_attr_string(status, "message");
- json_object_set_string_member(obj, "id", id);
- json_object_set_string_member(obj, "name", name);
- if (message != NULL)
- json_object_set_string_member(obj, "message", message);
- json_object_set_object_member(data, "status", obj);
- }
- /* Serializes a buddy "by reference", by hashing the buddy. Requires a
- * corresponding `buddy` op to be meaningful for the client. Requires
- * disambiguating by account, prpl, etc as well as just the name.
- * Simultaneously "upserts" the buddy into the global hash table for later
- * access.
- *
- * Result: serialized string, heap allocated. Must be g_free'd later.
- */
- static gchar *
- sapphire_serialize_user_id(PurpleAccount *account, const gchar *name)
- {
- const gchar *prpl = purple_account_get_protocol_id(account);
- const gchar *account_id = purple_account_get_username(account);
- /* Smush together the features into a unique ID. TODO: Hash */
- gchar *smushed = g_strdup_printf("%s|%s|%s", prpl, account_id, name);
- return smushed;
- }
- static gchar *
- sapphire_serialize_buddy_id(PurpleBuddy *buddy)
- {
- /* Get distinguishing features */
- PurpleAccount *p_account = purple_buddy_get_account(buddy);
- const gchar *name = purple_normalize(p_account, purple_buddy_get_name(buddy));
- gchar *smushed = sapphire_serialize_user_id(p_account, name);
- /* Upsert. TODO: Will PurpleBuddy get garbage collected on us? */
- if (!g_hash_table_lookup(id_to_buddy, smushed)) {
- g_hash_table_replace(id_to_buddy, g_strdup(smushed), buddy);
- }
- return smushed;
- }
- /* Resolve from bare nickname who to actual ID */
- static gchar *
- sapphire_serialize_chat_user_id(PurpleConversation *conv, const gchar *who)
- {
- PurpleAccount *account = purple_conversation_get_account(conv);
- PurpleConnection *connection = purple_account_get_connection(account);
- PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
- if (prpl_info && prpl_info->get_cb_real_name) {
- /* Get the user's intra-protocol canonical name */
- PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
- int id = purple_conv_chat_get_id(conv_chat);
- gchar *real_name = prpl_info->get_cb_real_name(connection, id, who);
- gchar *normalized = g_strdup(purple_normalize(account, real_name));
- /* Check if it's, uh, us */
- const char *username = purple_normalize(account, purple_account_get_username(account));
- const char *display_name = purple_connection_get_display_name(connection);
- if (purple_strequal(username, normalized) || purple_strequal(display_name, normalized)) {
- g_free(normalized);
- return sapphire_serialize_account_id(account);
- }
- printf("From %s to %s to %s\n", who, real_name, normalized);
- /* Serialize it formally for protocol independence */
- gchar *out = sapphire_serialize_user_id(account, normalized);
- g_free(normalized);
- g_free(real_name);
- return out;
- } else {
- printf("Bailing on %s\n", who);
- return g_strdup(who);
- }
- }
- static void
- sapphire_serialize_buddy(JsonObject *data, PurpleBuddy *buddy)
- {
- gchar *id = sapphire_serialize_buddy_id(buddy);
- json_object_set_string_member(data, "buddy", id);
- g_free(id);
- }
- static void
- sapphire_serialize_chat_buddy(JsonObject *data, PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags)
- {
- gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
- json_object_set_string_member(data, "id", user_id);
- json_object_set_string_member(data, "alias", who);
- json_object_set_int_member(data, "flags", flags);
- g_free(user_id);
- }
- /* Add missed messages to buddy/chat object if applicable */
- static void
- sapphire_serialize_unacked_messages(JsonObject *obj, const gchar *id)
- {
- GList *lst = g_hash_table_lookup(id_to_unacked_list, id);
- if (!lst)
- return;
- /* Pop missed messages in reverse order */
- GList *it;
- JsonArray *unacked = json_array_new();
- for (it = lst; it != NULL; it = it->next) {
- JsonObject *msg = (JsonObject *) it->data;
- json_array_add_object_element(unacked, msg);
- }
- json_object_set_array_member(obj, "unacked", unacked);
- }
- static JsonArray *
- sapphire_serialize_chat_users(SapphireChat *chat)
- {
- JsonArray *jusers = json_array_new();
- if (chat->conv) {
- PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat->conv);
- for (GList *l = purple_conv_chat_get_users(conv_chat); l != NULL; l = l->next) {
- JsonObject *juser = json_object_new();
- PurpleConvChatBuddy *cb = (PurpleConvChatBuddy *) l->data;
- const gchar *who = purple_conv_chat_cb_get_name(cb);
- PurpleConvChatBuddyFlags flags = purple_conv_chat_user_get_flags(conv_chat, who);
- printf("For %s %s\n", cb->name, cb->alias);
- sapphire_serialize_chat_buddy(juser, chat->conv, who, flags);
- json_array_add_object_element(jusers, juser);
- }
- }
- return jusers;
- }
- /* Serializes the unopened chat pieces, not the conversation bits which have a
- * rather more complex path */
- static JsonObject *
- sapphire_serialize_chat(SapphireChat *chat)
- {
- JsonObject *obj = json_object_new();
- json_object_set_string_member(obj, "id", chat->id);
- json_object_set_string_member(obj, "name", chat->name);
- json_object_set_string_member(obj, "group", chat->group);
- json_object_set_string_member(obj, "account", chat->account_id);
- sapphire_serialize_unacked_messages(obj, chat->id);
- return obj;
- }
- /* Creates pass-by-reference ID for account.
- *
- * Return: ID as a string (must be freed by caller)
- */
- static gchar *
- sapphire_serialize_account_id(PurpleAccount *account)
- {
- /* Get features */
- const gchar *prpl = purple_account_get_protocol_id(account);
- const gchar *username = purple_account_get_username(account);
- /* Smush prpl with username to form an ID */
- return g_strdup_printf("%s|%s", prpl, username);
- }
- /* Serialize actual chat ID */
- static gchar *
- sapphire_id_from_conv(PurpleConversation *chat)
- {
- PurpleAccount *account = purple_conversation_get_account(chat);
- gchar *acct = sapphire_serialize_account_id(account);
- PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat);
- int id = purple_conv_chat_get_id(conv_chat);
- gchar *full_id = g_strdup_printf("%s|%d", acct, id);
- g_free(acct);
- return full_id;
- }
- /* Find a chat by ID */
- static SapphireChat *
- sapphire_find_chat(const gchar *id, gboolean use_id)
- {
- for (GSList *it = chats; it != NULL; it = it->next) {
- SapphireChat *candidate = (SapphireChat *) it->data;
- gboolean match = FALSE;
- if (use_id) {
- match = purple_strequal(candidate->id, id);
- } else if (candidate->conv) {
- /* Ignore the provided ID and compute it ourselves */
- gchar *chat = sapphire_id_from_conv(candidate->conv);
- match = purple_strequal(id, chat);
- g_free(chat);
- } else {
- printf("ERROR: ID ignored but NULL conv\n");
- return NULL;
- }
- if (match)
- return candidate;
- }
- return NULL;
- }
- /* Find conversation by ID, the fast way or the slow way.. */
- static PurpleConversation *
- sapphire_find_conversation(const gchar *chat)
- {
- PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
- if (conv)
- return conv;
- /* Not in the hash table -- so iterate */
- SapphireChat *schat = sapphire_find_chat(chat, FALSE);
- return schat ? schat->conv : NULL;
- }
- static gchar *
- sapphire_id_from_parts(PurpleAccount *account, const gchar *id)
- {
- gchar *acct = sapphire_serialize_account_id(account);
- return g_strdup_printf("%s|%s", acct, id);
- }
- /* By contrast, this routine serializes a buddy by value, including the ID
- * generated by the previous function as well as the actual metadata */
- static JsonObject *
- sapphire_serialize_buddy_object(PurpleBuddy *buddy)
- {
- JsonObject *json = json_object_new();
- PurpleGroup *group = purple_buddy_get_group(buddy);
- const gchar *name = purple_buddy_get_name(buddy);
- const gchar *alias = purple_buddy_get_contact_alias(buddy);
- const gchar *group_name = purple_group_get_name(group);
- gchar *id = sapphire_serialize_buddy_id(buddy);
- /* We might have an icon. If so, get it ready for later access, but do
- * not send it here. Merely record if there is an icon or not */
- PurpleAccount *account = purple_buddy_get_account(buddy);
- PurpleBuddyIcon *icon = purple_buddy_icons_find(account, name);
- if (icon != NULL) {
- purple_buddy_icon_ref(icon);
- sapphire_add_buddy_icon(id, icon);
- }
- json_object_set_boolean_member(json, "hasIcon", icon != NULL);
- json_object_set_string_member(json, "id", id);
- json_object_set_string_member(json, "name", name);
- json_object_set_string_member(json, "alias", alias);
- json_object_set_string_member(json, "group", group_name);
- gchar *accountID = sapphire_serialize_account_id(account);
- if (accountID) {
- json_object_set_string_member(json, "account", accountID);
- g_free(accountID);
- }
- sapphire_serialize_status(json, sapphire_status_for_buddy(buddy));
- /* Include the ID of the buddy itself */
- sapphire_serialize_buddy(json, buddy);
- g_free(id);
- return json;
- }
- /* Serialize the account itself for personal information */
- static JsonObject *
- sapphire_serialize_account(PurpleAccount *account)
- {
- JsonObject *json = json_object_new();
- const gchar *prpl = purple_account_get_protocol_id(account);
- const gchar *prpl_name = purple_account_get_protocol_name(account);
- const gchar *username = purple_account_get_username(account);
- const gchar *alias = purple_account_get_alias(account);
- json_object_set_string_member(json, "prpl", prpl);
- json_object_set_string_member(json, "prplName", prpl_name);
- json_object_set_string_member(json, "name", username);
- json_object_set_string_member(json, "alias", alias);
- gchar *id = sapphire_serialize_account_id(account);
- json_object_set_string_member(json, "id", id);
- /* Add our own icon, if applicable, to the store */
- PurpleStoredImage *icon =
- purple_buddy_icons_find_account_icon(account);
- if (icon)
- sapphire_add_stored_image(id, icon);
- json_object_set_boolean_member(json, "hasIcon", icon != NULL);
- g_free(id);
- return json;
- }
- /* Sends the entire world to a new connection. For this, we need to send:
- *
- * - information about our accounts
- * - the buddy list
- * - rooms we're in
- * - missed messages
- *
- * Essentially, everything needed for the initial client render.
- *
- * We do _not_ need to send anything that's not immediately accessible; for
- * instance, we can avoid sending the users in present rooms that are not on
- * our buddy list, deferring to when we explicitly open the room
- *
- */
- void
- sapphire_send_world(Connection *conn)
- {
- /* Initialize connected state */
- conn->subscribed_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- JsonObject *data = json_object_new();
- json_object_set_string_member(data, "op", "world");
- /* Iterate the buddy list of connected accounts to include buddies */
- JsonArray *jbuddies = json_array_new();
- JsonArray *jaccounts = json_array_new();
- JsonArray *jchats = json_array_new();
- GSList *acct;
- for (acct = purple_accounts; acct != NULL; acct = acct->next) {
- PurpleAccount *account = (PurpleAccount *) acct->data;
- /* Add buddies from account */
- GSList *blist = purple_find_buddies(account, NULL);
- for (GSList *it = blist; it != NULL; it = it->next) {
- PurpleBuddy *buddy = (PurpleBuddy *) it->data;
- JsonObject *bud = sapphire_serialize_buddy_object(buddy);
- const gchar *bid = json_object_get_string_member(bud, "buddy");
- sapphire_serialize_unacked_messages(bud, bid);
- json_array_add_object_element(jbuddies, bud);
- }
- /* Add metadata for the account itself */
- JsonObject *j_account = sapphire_serialize_account(account);
- json_array_add_object_element(jaccounts, j_account);
- g_slist_free(blist);
- }
- /* Send chats */
- for (GSList *it = chats; it != NULL; it = it->next) {
- SapphireChat *schat = (SapphireChat *) it->data;
- json_array_add_object_element(jchats, sapphire_serialize_chat(schat));
- }
- /* TODO: What if one is.. both? */
- json_object_set_array_member(data, "buddies", jbuddies);
- json_object_set_array_member(data, "chats", jchats);
- json_object_set_array_member(data, "accounts", jaccounts);
- sapphire_send(conn, data);
- json_object_unref(data);
- }
- static void
- sapphire_buddy_status_changed(PurpleBuddy *buddy, gpointer null)
- {
- JsonObject *data = json_object_new();
- json_object_set_string_member(data, "op", "buddyStatus");
- sapphire_serialize_status(data, sapphire_status_for_buddy(buddy));
- sapphire_serialize_buddy(data, buddy);
- sapphire_broadcast(data);
- json_object_unref(data);
- }
- static void
- sapphire_serialize_typing_state(JsonObject *data, PurpleTypingState state)
- {
- /* While we could pass is, that risks future libpurple updates causing
- * breakage */
- int s_state =
- (state == PURPLE_NOT_TYPING) ? 0 :
- (state == PURPLE_TYPING) ? 1 :
- (state == PURPLE_TYPED) ? 2 :
- -1;
- json_object_set_int_member(data, "state", s_state);
- }
- static PurpleTypingState
- sapphire_decode_typing_state(int s_state)
- {
- return (s_state == 0) ? PURPLE_NOT_TYPING :
- (s_state == 1) ? PURPLE_TYPING :
- (s_state == 2) ? PURPLE_TYPED :
- PURPLE_NOT_TYPING;
- }
- static void
- sapphire_buddy_typing_changed(PurpleAccount *account, const char *name, gpointer null)
- {
- JsonObject *data = json_object_new();
- json_object_set_string_member(data, "op", "typing");
- PurpleBuddy *buddy = purple_find_buddy(account, name);
- sapphire_serialize_buddy(data, buddy);
- PurpleConvIm *im = sapphire_im_for_name(account, name);
- PurpleTypingState state = purple_conv_im_get_typing_state(im);
- sapphire_serialize_typing_state(data, state);
- sapphire_broadcast(data);
- json_object_unref(data);
- }
- static void
- sapphire_received_message(PurpleAccount *account, const char *who, const char *message, PurpleConversation *conv,
- PurpleMessageFlags flags, gpointer null)
- {
- /* Find the buddy since the arguments as-is are difficult to work with */
- PurpleBuddy *buddy = NULL;
- /* Whether channel_id needs a g_free */
- gboolean should_free_channel_id = TRUE;
- gchar *channel_id;
- JsonObject *data = json_object_new();
- json_object_set_string_member(data, "op", "message");
- /* Serialization depends on the type of "buffer" in use; we don't
- * smooth out the incongruence between IMs and chats until we're in
- * backend.js on the client */
- PurpleConversationType type = purple_conversation_get_type(conv);
- if (type == PURPLE_CONV_TYPE_IM) {
- /* Serialize the buddy we're talking to */
- buddy = purple_find_buddy(account, who);
- sapphire_serialize_buddy(data, buddy);
- channel_id = sapphire_serialize_buddy_id(buddy);
- } else if (type == PURPLE_CONV_TYPE_CHAT) {
- /* Serialize the chat itself */
- channel_id = sapphire_id_from_conv(conv);
- json_object_set_string_member(data, "chat", channel_id);
- if (flags & PURPLE_MESSAGE_SYSTEM) {
- json_object_set_string_member(data, "who", "system");
- } else {
- /* And just the ID of who sent it. */
- gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
- json_object_set_string_member(data, "who", user_id);
- g_free(user_id);
- /* ...in case the user is offline and not a buddy, also supply an alias */
- json_object_set_string_member(data, "alias", who);
- }
- } else {
- printf("Wat? nonbuddy, non chat?\n");
- }
- //json_object_set_int_member(data, "time", mtime);
- json_object_set_int_member(data, "flags", flags);
- /* Since we might be OTR-protected, the backend can't do anything with the plaintext */
- json_object_set_string_member(data, "content", message);
- /* So, if there are connected clients, we broadcast to them. Otherwise, we need to
- * store the message, so we can replay messages later for when we
- * connect. It's okay if the lookup fails and we null, g_list functions
- * don't mind. Additionally, if this is the first message like this,
- * we'll need to send a push notification. */
- if (sapphire_any_connected_clients()) {
- /* Broadcast */
- gchar *str = json_object_to_string(data);
- sapphire_broadcast_raw_packet(str);
- } else {
- /* Save the message */
- if (type == PURPLE_CONV_TYPE_IM) {
- GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, channel_id);
- unacked_list = g_list_prepend(unacked_list, json_object_ref(data));
- g_hash_table_replace(id_to_unacked_list, channel_id, unacked_list);
- should_free_channel_id = FALSE;
- } else {
- /* TODO: Chats. At the moment, these can accumulate
- * huge amounts of memory, so disabling for now, mk? */
- }
- }
- /* Send a notification for IMs. The push notification module will
- * determine if it's necessary */
- if (type == PURPLE_CONV_TYPE_IM) {
- const char *alias = purple_buddy_get_alias(buddy);
- gchar *notification = g_strdup_printf("Psst, %s messaged you via Sapphire\n", alias);
- sapphire_push_notification(notification);
- g_free(notification);
- }
- if (should_free_channel_id)
- g_free(channel_id);
- json_object_unref(data);
- }
- static void
- sapphire_topic_changed(PurpleConversation *conv, const char *who, const char *topic, gpointer null)
- {
- JsonObject *data = json_object_new();
- gchar *chat_id = sapphire_id_from_conv(conv);
- json_object_set_string_member(data, "op", "topic");
- json_object_set_string_member(data, "who", who);
- json_object_set_string_member(data, "topic", topic);
- json_object_set_string_member(data, "chat", chat_id);
- sapphire_broadcast(data);
- json_object_unref(data);
- g_free(chat_id);
- }
- /* A buddy joined in a room we're subscribed to -- but that doesn't mean the
- * client needs to know. Only send the joined event to clients that have opened
- * the corresponding conversation */
- extern GList *authenticated_connections;
- static void
- sapphire_buddy_joined(PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags, gboolean new_arrival, gpointer null)
- {
- JsonObject *data = json_object_new();
- gchar *chat_id = sapphire_id_from_conv(conv);
- json_object_set_string_member(data, "op", "joined");
- json_object_set_string_member(data, "chat", chat_id);
- /* Send a single element worth of users :( */
- JsonArray *lst = json_array_new();
- JsonObject *buddy = json_object_new();
- printf("Got %s\n", who);
- sapphire_serialize_chat_buddy(buddy, conv, who, flags);
- json_array_add_object_element(lst, buddy);
- json_object_set_array_member(data, "members", lst);
- /* TODO: Maybe don't serialize so many times */
- /* TODO: Don't serialize at all if nobody's subscribed */
- for (GList *it = authenticated_connections; it != NULL; it = it->next) {
- Connection *conn = (Connection *) it->data;
- if (g_hash_table_contains(conn->subscribed_ids, chat_id))
- sapphire_send(conn, data);
- }
- g_free(chat_id);
- json_object_unref(data);
- }
- static void
- sapphire_joined_chat(PurpleConversation *conv, gpointer null)
- {
- /* Try to use the existing chat */
- gchar *id = sapphire_id_from_conv(conv);
- SapphireChat *schat = sapphire_find_chat(id, TRUE);
- if (!schat) {
- /* Surprise! Create a new chat */
- schat = sapphire_chat_from_conv(conv);
- schat->conv = conv;
- printf("Joining chat %s\n", schat->id);
- chats = g_slist_append(chats, schat);
- } else {
- /* Associate with the conv */
- schat->conv = conv;
- }
- g_hash_table_insert(blist_id_to_conversation, id, conv);
- /* It's joined! */
- if (!g_hash_table_contains(id_to_joined, id)) {
- g_hash_table_add(id_to_joined, g_strdup(id));
- g_hash_table_insert(id_to_chat, g_strdup(id), schat);
- }
- }
- /* Certain prpls, particularly those for third-party protocols, should be
- * disabled when not in active use. This function, called from the socket
- * handling when a client connects or disconnects, checks if there are active
- * connections. If there are, relevant prpls are enabled; if not, they are
- * disabled. */
- static gboolean
- sapphire_prpl_defer_connects(const gchar *protocol_id)
- {
- return purple_strequal(protocol_id, "prpl-eionrobb-discord");
- }
- void
- sapphire_enable_accounts_by_connections(void)
- {
- gboolean should_enable = sapphire_any_connected_clients();
- for (GSList *it = purple_accounts; it != NULL; it = it->next) {
- PurpleAccount *account = (PurpleAccount *) it->data;
- const gchar *protocol_id = purple_account_get_protocol_id(account);
- /* Check if the protocol has this quirk */
- if (!sapphire_prpl_defer_connects(protocol_id))
- continue;
- /* It does -- so check which direction we need to go */
- gboolean enab = purple_account_get_enabled(account, UI_ID);
- if (should_enable != enab) {
- purple_account_set_enabled(account, UI_ID, should_enable);
- }
- }
- }
- #ifdef _WIN32
- #include <windows.h>
- extern BOOL SetDllDirectoryA(LPCSTR lpPathName);
- typedef void (WINAPI* LPFNSETDLLDIRECTORY)(LPCSTR);
- static LPFNSETDLLDIRECTORY MySetDllDirectory = NULL;
- #endif
- static void
- init_libpurple(void)
- {
- #ifdef _WIN32
- purple_util_set_user_dir("./.purple");
- HMODULE hmod;
- if ((hmod = GetModuleHandleW(L"kernel32.dll"))) {
- MySetDllDirectory = (LPFNSETDLLDIRECTORY) GetProcAddress(
- hmod, "SetDllDirectoryA");
- if (!MySetDllDirectory)
- printf("SetDllDirectory not supported\n");
- } else
- printf("Error getting kernel32.dll module handle\n");
- /* For Windows XP SP1+ / Server 2003 we use SetDllDirectory to avoid dll hell */
- if (MySetDllDirectory) {
- printf("Using SetDllDirectory\n");
- MySetDllDirectory("C:/Program Files (x86)/Pidgin/");
- }
- #endif
- gchar *search_path = g_build_filename(purple_user_dir(), "plugins", NULL);
- purple_plugins_add_search_path(search_path);
- g_free(search_path);
- #ifdef _WIN32
- purple_plugins_add_search_path("C:/Program Files (x86)/Pidgin/plugins/");
- purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/");
- purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/plugins/");
- #endif
- purple_debug_set_enabled(FALSE);
- sapphire_set_eventloop();
- if (!purple_core_init(UI_ID)) {
- fprintf(stderr,
- "libpurple initialization failed. Dumping core.\n"
- "Please report this!\n");
- abort();
- }
- purple_set_blist(purple_blist_new());
- purple_blist_load();
- purple_prefs_load();
- purple_plugins_load_saved(PLUGIN_SAVE_PREF);
- purple_pounces_load();
- }
- static void
- sapphire_connect_signals(void)
- {
- static int handle;
- purple_signal_connect(purple_accounts_get_handle(), "account-signed-on", &handle,
- PURPLE_CALLBACK(sapphire_signed_on), NULL);
- purple_signal_connect(purple_accounts_get_handle(), "account-enabled", &handle,
- PURPLE_CALLBACK(sapphire_account_enabled), NULL);
- purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", &handle,
- PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
- purple_signal_connect(purple_blist_get_handle(), "buddy-signed-off", &handle,
- PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
- purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", &handle,
- PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", &handle,
- PURPLE_CALLBACK(sapphire_received_message), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", &handle,
- PURPLE_CALLBACK(sapphire_received_message), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "buddy-typing", &handle,
- PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "buddy-typed", &handle,
- PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped", &handle,
- PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "chat-joined", &handle,
- PURPLE_CALLBACK(sapphire_joined_chat), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-joined", &handle,
- PURPLE_CALLBACK(sapphire_buddy_joined), NULL);
- purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", &handle,
- PURPLE_CALLBACK(sapphire_topic_changed), NULL);
- }
- int main(int argc, char *argv[])
- {
- GMainLoop *loop;
- PurpleSavedStatus *status;
- #ifndef _WIN32
- /* libpurple's built-in DNS resolution forks processes to perform
- * blocking lookups without blocking the main process. It does not
- * handle SIGCHLD itself, so if the UI does not you quickly get an army
- * of zombie subprocesses marching around.
- */
- signal(SIGCHLD, SIG_IGN);
- #endif
- #ifdef _WIN32
- g_thread_init(NULL);
- #endif
- g_set_prgname("Sapphire");
- g_set_application_name("Sapphire");
- loop = g_main_loop_new(NULL, FALSE);
- g_main_loop_ref(loop);
- gboolean jailed = (argc >= 2) && (purple_strequal(argv[1], "--jailed"));
- if (jailed) {
- /* If we're running in firejail, we can't use a .purple, since
- * the hidden nature will cause permission errors. Instead, use
- * an opaque name */
- purple_util_set_user_dir("./purple");
- }
- init_libpurple();
- purple_prefs_add_none("/purple/sapphire");
- if (!purple_prefs_get_string(SAPPHIRE_PUSH_EMAIL_PREF)) {
- printf("Push notification email (blank to disable): ");
- char email[128];
- fgets(email, sizeof(email), stdin);
- purple_prefs_add_string(SAPPHIRE_PUSH_EMAIL_PREF, email);
- }
- /* Initialize global hash tables */
- id_to_buddy = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- id_to_unacked_list = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- id_to_chat = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- id_to_account = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- id_to_joined = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- blist_id_to_conversation = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
- sent_icons = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
- sapphire_connect_signals();
- sapphire_init_websocket();
- g_main_context_iteration(g_main_loop_get_context(loop), FALSE);
- GList *l;
- /* Fetch account and enable it */
- for (l = purple_accounts_get_all(); l != NULL; l = l->next) {
- PurpleAccount *candidate = (PurpleAccount *)l->data;
- const gchar *protocol_id = purple_account_get_protocol_id(candidate);
- if (purple_strequal(protocol_id, "prpl-jabber") || purple_strequal(protocol_id, "prpl-eionrobb-discord")) {
- purple_accounts = g_slist_append(purple_accounts, candidate);
- purple_account_set_enabled(candidate, UI_ID, !sapphire_prpl_defer_connects(protocol_id));
- } else {
- purple_account_set_enabled(candidate, UI_ID, FALSE);
- }
- }
- if (!purple_accounts) {
- fprintf(stderr, "No accounts found\n");
- return 1;
- }
- /* Now, to connect the account(s), create a status and activate it. */
- status = purple_savedstatus_new(NULL, PURPLE_STATUS_AVAILABLE);
- purple_savedstatus_activate(status);
- g_main_loop_run(loop);
- return 0;
- }
|