core.c 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483
  1. /*
  2. * sapphire-backend
  3. *
  4. * Copyright (C) 2018 Alyssa Rosenzweig
  5. * Copyright (C) 2018 libpurple authors
  6. *
  7. * This program is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 2 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, write to the Free Software
  19. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
  20. *
  21. */
  22. #include <stdint.h>
  23. #include "purple.h"
  24. #include <assert.h>
  25. #include <glib.h>
  26. #include <signal.h>
  27. #include <string.h>
  28. #ifndef _WIN32
  29. #include <unistd.h>
  30. #else
  31. #include "win32/win32dep.h"
  32. #endif
  33. #include "core.h"
  34. #include "websocket.h"
  35. #include "push.h"
  36. #include "event-loop.h"
  37. #include "json_compat.h"
  38. #define PLUGIN_SAVE_PREF "/purple/sapphire/plugins/saved"
  39. #define SAPPHIRE_PASSWORD_PREF "/purple/sapphire/password"
  40. #define UI_ID "sapphire"
  41. #define purple_serv_send_typing serv_send_typing
  42. #define purple_serv_join_chat serv_join_chat
  43. /* List of connected accounts */
  44. GSList *purple_accounts;
  45. /* List of SapphireChats, whether they are conversations yet or not */
  46. GSList *chats;
  47. /* Hash table of channel IDs to lists of unacked messages ready for replay */
  48. GHashTable *id_to_unacked_list;
  49. /* Hash table of chat IDs to PurpleChats */
  50. GHashTable *id_to_chat;
  51. /* Account ID to PurpleAccount */
  52. GHashTable *id_to_account;
  53. /* Blist Chat ID set */
  54. GHashTable *id_to_joined;
  55. /* All known buddies/other users are maintained in a hash table from network
  56. * serializable identifier to PurpleBuddy, since we can't transmit the buddy
  57. * object itself each time, this enables pass-by-reference */
  58. GHashTable *id_to_buddy;
  59. GHashTable *blist_id_to_conversation;
  60. /* Our internal tracking for chats, whether they are joined as
  61. * PurpleConversations or not. Smoothes over PurpleChat, PurpleConversation,
  62. * and room lists */
  63. typedef struct {
  64. const char *id; /* Unique, prpl-agnostic ID */
  65. const char *account_id; /* Account ID corresponding to account */
  66. const char *name; /* User visible name */
  67. const char *group; /* Group name, like from the blist */
  68. PurpleAccount *account;
  69. /* Bits needed to join, if roomlist */
  70. PurpleRoomlist *roomlist;
  71. PurpleRoomlistRoom *room;
  72. /* Corresponding conversation, if we have joined */
  73. PurpleConversation *conv;
  74. } SapphireChat;
  75. static gchar *
  76. sapphire_serialize_account_id(PurpleAccount *account);
  77. /* Creates a new heap-allocated SapphireChat. Must be freed later. */
  78. static SapphireChat *
  79. sapphire_new_chat(PurpleAccount *account, const char *id, const char *name, const char *group)
  80. {
  81. SapphireChat *schat = g_new0(SapphireChat, 1);
  82. schat->id = id;
  83. schat->account = account;
  84. schat->account_id = sapphire_serialize_account_id(account);
  85. schat->name = g_strdup(name);
  86. schat->group = g_strdup(group);
  87. return schat;
  88. }
  89. static gchar *
  90. sapphire_id_from_conv(PurpleConversation *chat);
  91. static SapphireChat *
  92. sapphire_chat_from_conv(PurpleConversation *conv)
  93. {
  94. return sapphire_new_chat(
  95. purple_conversation_get_account(conv),
  96. sapphire_id_from_conv(conv),
  97. purple_conversation_get_name(conv),
  98. "Chats");
  99. }
  100. /* Functions to upload icons to the proxy */
  101. GHashTable *sent_icons;
  102. static void
  103. sapphire_send_icon(const gchar *name, const gchar *ext, gconstpointer data, size_t size, const gchar *hash)
  104. {
  105. if (purple_strequal(g_hash_table_lookup(sent_icons, name), hash)) {
  106. /* Don't duplicate. */
  107. return;
  108. }
  109. /* If there is an active connection, send this icon. Otherwise, save
  110. * it to be sent later */
  111. gchar *base64 = g_base64_encode(data, size);
  112. JsonObject *obj = json_object_new();
  113. json_object_set_string_member(obj, "op", "icon");
  114. json_object_set_string_member(obj, "name", name);
  115. json_object_set_string_member(obj, "ext", ext);
  116. json_object_set_string_member(obj, "base64", base64);
  117. gchar *str = json_object_to_string(obj);
  118. gchar *str_prefixed = g_strdup_printf(">%s", str);
  119. /* Assume that it needs to save the string. Callee will g_free it itself in the off-chance it doesn't need it anymore */
  120. sapphire_send_any_or_save(str_prefixed);
  121. /* Mark that the icon is sent so we don't try later */
  122. g_hash_table_insert(sent_icons, g_strdup(name), g_strdup(hash));
  123. g_free(str);
  124. g_free(base64);
  125. json_object_unref(obj);
  126. }
  127. void
  128. sapphire_add_buddy_icon(const gchar *name, PurpleBuddyIcon *icon)
  129. {
  130. size_t size;
  131. gconstpointer data = purple_buddy_icon_get_data(icon, &size);
  132. sapphire_send_icon(name, purple_buddy_icon_get_extension(icon), data, size, purple_buddy_icon_get_checksum(icon));
  133. }
  134. void
  135. sapphire_add_stored_image(const gchar *name, PurpleStoredImage *icon)
  136. {
  137. sapphire_send_icon(name, purple_imgstore_get_extension(icon), purple_imgstore_get_data(icon), purple_imgstore_get_size(icon), purple_imgstore_get_filename(icon));
  138. }
  139. /* Generic purple related helpers */
  140. static PurpleStatus *
  141. sapphire_status_for_buddy(PurpleBuddy *buddy)
  142. {
  143. PurplePresence *presence = purple_buddy_get_presence(buddy);
  144. return purple_presence_get_active_status(presence);
  145. }
  146. static PurplePluginProtocolInfo *
  147. sapphire_info_for_connection(PurpleConnection *connection)
  148. {
  149. PurplePlugin *prpl = purple_connection_get_prpl(connection);
  150. return PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
  151. }
  152. static PurpleConvIm *
  153. sapphire_im_for_name(PurpleAccount *account, const char *name)
  154. {
  155. PurpleConversation *conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account);
  156. if (conv == NULL) {
  157. /* If not found, create it */
  158. conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name);
  159. }
  160. return purple_conversation_get_im_data(conv);
  161. }
  162. /* Search for a PurpleConversation, either as a chat or an IM. Returns NULL if
  163. * not found */
  164. static PurpleConversation *
  165. sapphire_conversation_for_id(const gchar *id)
  166. {
  167. PurpleConversation *as_chat = g_hash_table_lookup(blist_id_to_conversation, id);
  168. if (as_chat)
  169. return as_chat;
  170. PurpleBuddy *buddy = g_hash_table_lookup(id_to_buddy, id);
  171. if (buddy) {
  172. PurpleAccount *account = purple_buddy_get_account(buddy);
  173. const gchar *name = purple_buddy_get_name(buddy);
  174. return purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account);
  175. }
  176. return NULL;
  177. }
  178. /* Helper to serialize and broadcast */
  179. static void
  180. sapphire_broadcast(JsonObject *msg)
  181. {
  182. gchar *str = json_object_to_string(msg);
  183. sapphire_broadcast_raw_packet(str);
  184. g_free(str);
  185. }
  186. static void
  187. sapphire_send(Connection *conn, JsonObject *msg)
  188. {
  189. gchar *str = json_object_to_string(msg);
  190. sapphire_send_raw_packet(conn, str);
  191. g_free(str);
  192. }
  193. static PurpleTypingState
  194. sapphire_decode_typing_state(int s_state);
  195. static PurpleConversation *
  196. sapphire_find_conversation(const gchar *chat);
  197. static JsonArray *
  198. sapphire_serialize_chat_users(SapphireChat *chat);
  199. static PurpleBuddy *
  200. sapphire_decode_buddy(JsonObject *data)
  201. {
  202. const gchar *buddy_id = json_object_get_string_member(data, "buddy");
  203. if (!buddy_id)
  204. return NULL;
  205. /* Find the associated buddy */
  206. PurpleBuddy *buddy = g_hash_table_lookup(id_to_buddy, buddy_id);
  207. if (!buddy) {
  208. fprintf(stderr, "Bad buddy id %s\n", buddy_id);
  209. return NULL;
  210. }
  211. return buddy;
  212. }
  213. static PurpleAccount *
  214. sapphire_decode_account(JsonObject *data)
  215. {
  216. const gchar *account_id = json_object_get_string_member(data, "account");
  217. return g_hash_table_lookup(id_to_account, account_id);
  218. }
  219. void
  220. sapphire_process_message(Connection *conn, JsonObject *data)
  221. {
  222. const gchar *op = json_object_get_string_member(data, "op");
  223. if (purple_strequal(op, "message")) {
  224. /* Send an outgoing IM */
  225. const gchar *content = json_object_get_string_member(data, "content");
  226. /* Content is HTML, possibly OTR-encrypted so we can't do processing */
  227. gchar *marked = g_strdup(content);
  228. if (json_object_has_member(data, "buddy")) {
  229. PurpleBuddy *buddy = sapphire_decode_buddy(data);
  230. PurpleAccount *buddy_account = purple_buddy_get_account(buddy);
  231. const gchar *buddy_name = purple_buddy_get_name(buddy);
  232. purple_conv_im_send(sapphire_im_for_name(buddy_account, buddy_name), marked);
  233. } else if (json_object_has_member(data, "chat")) {
  234. const gchar *chat = json_object_get_string_member(data, "chat");
  235. PurpleConversation *conv = sapphire_find_conversation(chat);
  236. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
  237. purple_conv_chat_send(conv_chat, marked);
  238. } else {
  239. fprintf(stderr, "No recipient specified in message\n");
  240. return;
  241. }
  242. g_free(marked);
  243. } else if (purple_strequal(op, "typing")) {
  244. /* Our buddy typing status changed */
  245. int s_state = json_object_get_int_member(data, "state");
  246. PurpleTypingState state = sapphire_decode_typing_state(s_state);
  247. PurpleBuddy *buddy = sapphire_decode_buddy(data);
  248. if (!buddy) {
  249. fprintf(stderr, "No buddy\n");
  250. return;
  251. }
  252. PurpleAccount *buddy_account = purple_buddy_get_account(buddy);
  253. const gchar *buddy_name = purple_buddy_get_name(buddy);
  254. PurpleConnection *connection = purple_account_get_connection(buddy_account);
  255. purple_serv_send_typing(connection, buddy_name, state);
  256. } else if (purple_strequal(op, "joinChat")) {
  257. /* Join a MUC */
  258. const gchar *id = json_object_get_string_member(data, "id");
  259. SapphireChat *chat = g_hash_table_lookup(id_to_chat, id);
  260. if (!chat) {
  261. printf("Chat not found %s\n", id);
  262. return;
  263. }
  264. gboolean is_subscribed = g_hash_table_contains(conn->subscribed_ids, id);
  265. if (g_hash_table_contains(id_to_joined, id)) {
  266. purple_roomlist_room_join(chat->roomlist, chat->room);
  267. g_hash_table_add(id_to_joined, g_strdup(id));
  268. } else if (!is_subscribed) {
  269. /* If we already joined but not in this connection, just send back details */
  270. const gchar *topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(chat->conv));
  271. JsonArray *users = sapphire_serialize_chat_users(chat);
  272. JsonObject *data = json_object_new();
  273. json_object_set_string_member(data, "op", "joined");
  274. json_object_set_string_member(data, "chat", id);
  275. json_object_set_string_member(data, "topic", topic);
  276. json_object_set_array_member(data, "members", users);
  277. sapphire_send(conn, data);
  278. json_object_unref(data);
  279. }
  280. if (!is_subscribed) {
  281. /* We want to know about this room */
  282. g_hash_table_add(conn->subscribed_ids, g_strdup(id));
  283. }
  284. } else if (purple_strequal(op, "topic")) {
  285. const gchar *chat = json_object_get_string_member(data, "chat");
  286. const gchar *topic = json_object_get_string_member(data, "topic");
  287. PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
  288. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
  289. int id = purple_conv_chat_get_id(conv_chat);
  290. PurpleAccount *account = purple_conversation_get_account(conv);
  291. PurpleConnection *connection = purple_account_get_connection(account);
  292. PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
  293. if (prpl_info && prpl_info->set_chat_topic)
  294. prpl_info->set_chat_topic(connection, id, topic);
  295. else
  296. printf("Set chat topic unimplemented\n");
  297. } else if (purple_strequal(op, "markAsRead")) {
  298. const gchar *id = json_object_get_string_member(data, "id");
  299. /* Free the unacked list entries */
  300. GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, id);
  301. for (GList *it = unacked_list; it != NULL; it = it->next) {
  302. JsonObject *msg = (JsonObject *) it->data;
  303. json_object_unref(msg);
  304. }
  305. /* Free the list itself */
  306. g_list_free(unacked_list);
  307. /* And remove it from the hash table */
  308. g_hash_table_remove(id_to_unacked_list, id);
  309. /* Fake a PURPLE_CONV_UPDATE_UNSEEN signal, so that the room gets
  310. * marked as read.
  311. */
  312. PurpleConversation *conv = sapphire_conversation_for_id(id);
  313. if (!conv) {
  314. fprintf(stderr, "Conversation not found in markAsRead %s\n", id);
  315. return;
  316. }
  317. purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
  318. } else if (purple_strequal(op, "requestBuddy")) {
  319. /* Request to add a buddy */
  320. const gchar *id = json_object_get_string_member(data, "id");
  321. const gchar *alias = json_object_get_string_member(data, "alias");
  322. const gchar *invite = json_object_get_string_member(data, "invite");
  323. PurpleAccount *account = sapphire_decode_account(data);
  324. PurpleBuddy *buddy = purple_buddy_new(account, id, alias);
  325. purple_blist_add_buddy(buddy, NULL, NULL, NULL);
  326. purple_account_add_buddy_with_invite(account, buddy, invite);
  327. } else if (purple_strequal(op, "changeAvatar")) {
  328. /* Request to change our avatar */
  329. PurpleAccount *account = sapphire_decode_account(data);
  330. const gchar *base64 = json_object_get_string_member(data, "base64");
  331. size_t len;
  332. guchar *l_data = g_base64_decode(base64, &len);
  333. PurpleStoredImage *icon = purple_buddy_icons_set_account_icon(account, l_data, len);
  334. /* Update the cache */
  335. const gchar *raw_acct = json_object_get_string_member(data, "account");
  336. sapphire_add_stored_image(raw_acct, icon);
  337. /* Respond that we did it! */
  338. JsonObject *resp = json_object_new();
  339. json_object_set_string_member(resp, "op", "changeAvatar");
  340. json_object_set_string_member(resp, "id", raw_acct);
  341. sapphire_broadcast(resp);
  342. json_object_unref(resp);
  343. } else {
  344. fprintf(stderr, "Unknown op %s\n", op);
  345. }
  346. }
  347. /*** Conversation uiops ***/
  348. static gchar *
  349. sapphire_id_from_parts(PurpleAccount *account, const gchar *id);
  350. static void
  351. sapphire_signed_on(PurpleAccount *account, gpointer null)
  352. {
  353. PurpleConnection *connection = purple_account_get_connection(account);
  354. /* Upsert the account ID */
  355. gchar *acct_id = sapphire_serialize_account_id(account);
  356. if (g_hash_table_contains(id_to_account, acct_id)) {
  357. /* Wait. We already did this account. Bail! TODO: Sync */
  358. g_free(acct_id);
  359. return;
  360. }
  361. g_hash_table_insert(id_to_account, acct_id, account);
  362. /* For type-1 prpls where the openness of a chat determines whether we
  363. * receive events (e.g. IRC), open up all chats as early as possible */
  364. PurpleBlistNode *node;
  365. for ( node = purple_blist_get_root();
  366. node != NULL;
  367. node = purple_blist_node_next(node, TRUE)) {
  368. if (PURPLE_BLIST_NODE_IS_CHAT(node)) {
  369. PurpleChat *chat = PURPLE_CHAT(node);
  370. if (purple_chat_get_account(chat) != account) continue;
  371. GHashTable *components = purple_chat_get_components(chat);
  372. purple_serv_join_chat(connection, components);
  373. }
  374. }
  375. /* For type-2 prpls where we fetch from the room list (e.g. Discord),
  376. * fetch now but do not open yet, since we don't want to spam the
  377. * servers */
  378. if (purple_strequal(account->protocol_id, "prpl-eionrobb-discord")) {
  379. PurpleRoomlist *roomlist = purple_roomlist_get_list(connection);
  380. /* We're persisting the roomlist until later */
  381. purple_roomlist_ref(roomlist);
  382. gboolean in_progress = purple_roomlist_get_in_progress(roomlist);
  383. if (in_progress) {
  384. printf("In progress room list, aborting\n");
  385. return;
  386. }
  387. /* Check the field headings to figure out what to display (name) and index by (ID) */
  388. GList *field_headings = purple_roomlist_get_fields(roomlist);
  389. int index_id = -1, index_name = -1;
  390. int field_idx = 0;
  391. for (; field_headings != NULL; field_headings = field_headings->next, ++field_idx) {
  392. PurpleRoomlistField *field = (PurpleRoomlistField *) field_headings->data;
  393. const char *label = purple_roomlist_field_get_label(field);
  394. gboolean hidden = purple_roomlist_field_get_hidden(field);
  395. if (index_id == -1 && hidden) {
  396. index_id = field_idx;
  397. } else if (index_name == -1 && purple_strequal(label, "Name")) {
  398. index_name = field_idx;
  399. } else {
  400. /* Useless field */
  401. }
  402. }
  403. /* Now, scan the rooms */
  404. GList *rooms = roomlist->rooms; /* XXX: purple3 */
  405. for (; rooms != NULL; rooms = rooms->next) {
  406. PurpleRoomlistRoom *room = (PurpleRoomlistRoom *) rooms->data;
  407. PurpleRoomlistRoomType type = purple_roomlist_room_get_type(room);
  408. /* Skip over categories */
  409. if (type != PURPLE_ROOMLIST_ROOMTYPE_ROOM)
  410. continue;
  411. /* ...but do fetch our category name! */
  412. PurpleRoomlistRoom *parent = purple_roomlist_room_get_parent(room);
  413. const char *group_name = parent ? purple_roomlist_room_get_name(parent) : "Rooms";
  414. GList *fields = purple_roomlist_room_get_fields(room);
  415. const char *id = NULL;
  416. const char *display_name = NULL;
  417. for (int idx = 0; fields != NULL; fields = fields->next, ++idx) {
  418. gchar *value = (gchar *) fields->data;
  419. if (idx == index_id)
  420. id = value;
  421. if (idx == index_name)
  422. display_name = value;
  423. }
  424. /* XXX: Do magic from purple-discord to format ID */
  425. guint64 gid = g_ascii_strtoull(id, NULL, 10);
  426. int nid = ABS((gint) gid);
  427. gchar *snid = g_strdup_printf("%d", nid);
  428. gchar *sapphic_id = sapphire_id_from_parts(account, snid);
  429. g_free(snid);
  430. /* Save the chat */
  431. SapphireChat *schat = sapphire_new_chat(account, g_strdup(sapphic_id), display_name, group_name);
  432. schat->roomlist = roomlist;
  433. schat->room = room;
  434. printf("Saving Dithcord with %s\n", sapphic_id);
  435. chats = g_slist_prepend(chats, schat);
  436. /* No need to g_strdup(sapphic_id) since we already have the exclusive reference */
  437. g_hash_table_insert(id_to_chat, sapphic_id, schat);
  438. }
  439. }
  440. }
  441. static void
  442. sapphire_account_enabled(PurpleAccount *account, gpointer null)
  443. {
  444. printf("Account enabled: %s %s\n", account->username, account->protocol_id);
  445. }
  446. /* Serializes the actual content of a status */
  447. static void
  448. sapphire_serialize_status(JsonObject *data, PurpleStatus *status)
  449. {
  450. JsonObject *obj = json_object_new();
  451. const gchar *id = purple_status_get_id(status);
  452. const gchar *name = purple_status_get_name(status);
  453. const gchar *message = purple_status_get_attr_string(status, "message");
  454. json_object_set_string_member(obj, "id", id);
  455. json_object_set_string_member(obj, "name", name);
  456. if (message != NULL)
  457. json_object_set_string_member(obj, "message", message);
  458. json_object_set_object_member(data, "status", obj);
  459. }
  460. /* Serializes a buddy "by reference", by hashing the buddy. Requires a
  461. * corresponding `buddy` op to be meaningful for the client. Requires
  462. * disambiguating by account, prpl, etc as well as just the name.
  463. * Simultaneously "upserts" the buddy into the global hash table for later
  464. * access.
  465. *
  466. * Result: serialized string, heap allocated. Must be g_free'd later.
  467. */
  468. static gchar *
  469. sapphire_serialize_user_id(PurpleAccount *account, const gchar *name)
  470. {
  471. const gchar *prpl = purple_account_get_protocol_id(account);
  472. const gchar *account_id = purple_account_get_username(account);
  473. /* Smush together the features into a unique ID. TODO: Hash */
  474. gchar *smushed = g_strdup_printf("%s|%s|%s", prpl, account_id, name);
  475. return smushed;
  476. }
  477. static gchar *
  478. sapphire_serialize_buddy_id(PurpleBuddy *buddy)
  479. {
  480. /* Get distinguishing features */
  481. PurpleAccount *p_account = purple_buddy_get_account(buddy);
  482. const gchar *name = purple_normalize(p_account, purple_buddy_get_name(buddy));
  483. gchar *smushed = sapphire_serialize_user_id(p_account, name);
  484. /* Upsert. TODO: Will PurpleBuddy get garbage collected on us? */
  485. if (!g_hash_table_lookup(id_to_buddy, smushed)) {
  486. g_hash_table_replace(id_to_buddy, g_strdup(smushed), buddy);
  487. }
  488. return smushed;
  489. }
  490. /* Resolve from bare nickname who to actual ID */
  491. static gchar *
  492. sapphire_serialize_chat_user_id(PurpleConversation *conv, const gchar *who)
  493. {
  494. PurpleAccount *account = purple_conversation_get_account(conv);
  495. PurpleConnection *connection = purple_account_get_connection(account);
  496. PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
  497. if (prpl_info && prpl_info->get_cb_real_name) {
  498. /* Get the user's intra-protocol canonical name */
  499. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
  500. int id = purple_conv_chat_get_id(conv_chat);
  501. gchar *real_name = prpl_info->get_cb_real_name(connection, id, who);
  502. gchar *normalized = g_strdup(purple_normalize(account, real_name));
  503. /* Check if it's, uh, us */
  504. const char *username = purple_normalize(account, purple_account_get_username(account));
  505. const char *display_name = purple_connection_get_display_name(connection);
  506. if (purple_strequal(username, normalized) || purple_strequal(display_name, normalized)) {
  507. g_free(normalized);
  508. return sapphire_serialize_account_id(account);
  509. }
  510. printf("From %s to %s to %s\n", who, real_name, normalized);
  511. /* Serialize it formally for protocol independence */
  512. gchar *out = sapphire_serialize_user_id(account, normalized);
  513. g_free(normalized);
  514. g_free(real_name);
  515. return out;
  516. } else {
  517. printf("Bailing on %s\n", who);
  518. return g_strdup(who);
  519. }
  520. }
  521. static void
  522. sapphire_serialize_buddy(JsonObject *data, PurpleBuddy *buddy)
  523. {
  524. gchar *id = sapphire_serialize_buddy_id(buddy);
  525. json_object_set_string_member(data, "buddy", id);
  526. g_free(id);
  527. }
  528. static void
  529. sapphire_serialize_chat_buddy(JsonObject *data, PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags)
  530. {
  531. gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
  532. json_object_set_string_member(data, "id", user_id);
  533. json_object_set_string_member(data, "alias", who);
  534. json_object_set_int_member(data, "flags", flags);
  535. g_free(user_id);
  536. }
  537. /* Add missed messages to buddy/chat object if applicable */
  538. static void
  539. sapphire_serialize_unacked_messages(JsonObject *obj, const gchar *id)
  540. {
  541. GList *lst = g_hash_table_lookup(id_to_unacked_list, id);
  542. if (!lst)
  543. return;
  544. /* Pop missed messages in reverse order */
  545. GList *it;
  546. JsonArray *unacked = json_array_new();
  547. for (it = lst; it != NULL; it = it->next) {
  548. JsonObject *msg = (JsonObject *) it->data;
  549. json_array_add_object_element(unacked, msg);
  550. }
  551. json_object_set_array_member(obj, "unacked", unacked);
  552. }
  553. static JsonArray *
  554. sapphire_serialize_chat_users(SapphireChat *chat)
  555. {
  556. JsonArray *jusers = json_array_new();
  557. if (chat->conv) {
  558. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat->conv);
  559. for (GList *l = purple_conv_chat_get_users(conv_chat); l != NULL; l = l->next) {
  560. JsonObject *juser = json_object_new();
  561. PurpleConvChatBuddy *cb = (PurpleConvChatBuddy *) l->data;
  562. const gchar *who = purple_conv_chat_cb_get_name(cb);
  563. PurpleConvChatBuddyFlags flags = purple_conv_chat_user_get_flags(conv_chat, who);
  564. printf("For %s %s\n", cb->name, cb->alias);
  565. sapphire_serialize_chat_buddy(juser, chat->conv, who, flags);
  566. json_array_add_object_element(jusers, juser);
  567. }
  568. }
  569. return jusers;
  570. }
  571. /* Serializes the unopened chat pieces, not the conversation bits which have a
  572. * rather more complex path */
  573. static JsonObject *
  574. sapphire_serialize_chat(SapphireChat *chat)
  575. {
  576. JsonObject *obj = json_object_new();
  577. json_object_set_string_member(obj, "id", chat->id);
  578. json_object_set_string_member(obj, "name", chat->name);
  579. json_object_set_string_member(obj, "group", chat->group);
  580. json_object_set_string_member(obj, "account", chat->account_id);
  581. sapphire_serialize_unacked_messages(obj, chat->id);
  582. return obj;
  583. }
  584. /* Creates pass-by-reference ID for account.
  585. *
  586. * Return: ID as a string (must be freed by caller)
  587. */
  588. static gchar *
  589. sapphire_serialize_account_id(PurpleAccount *account)
  590. {
  591. /* Get features */
  592. const gchar *prpl = purple_account_get_protocol_id(account);
  593. const gchar *username = purple_account_get_username(account);
  594. /* Smush prpl with username to form an ID */
  595. return g_strdup_printf("%s|%s", prpl, username);
  596. }
  597. /* Serialize actual chat ID */
  598. static gchar *
  599. sapphire_id_from_conv(PurpleConversation *chat)
  600. {
  601. PurpleAccount *account = purple_conversation_get_account(chat);
  602. gchar *acct = sapphire_serialize_account_id(account);
  603. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat);
  604. int id = purple_conv_chat_get_id(conv_chat);
  605. gchar *full_id = g_strdup_printf("%s|%d", acct, id);
  606. g_free(acct);
  607. return full_id;
  608. }
  609. /* Find a chat by ID */
  610. static SapphireChat *
  611. sapphire_find_chat(const gchar *id, gboolean use_id)
  612. {
  613. for (GSList *it = chats; it != NULL; it = it->next) {
  614. SapphireChat *candidate = (SapphireChat *) it->data;
  615. gboolean match = FALSE;
  616. if (use_id) {
  617. match = purple_strequal(candidate->id, id);
  618. } else if (candidate->conv) {
  619. /* Ignore the provided ID and compute it ourselves */
  620. gchar *chat = sapphire_id_from_conv(candidate->conv);
  621. match = purple_strequal(id, chat);
  622. g_free(chat);
  623. } else {
  624. printf("ERROR: ID ignored but NULL conv\n");
  625. return NULL;
  626. }
  627. if (match)
  628. return candidate;
  629. }
  630. return NULL;
  631. }
  632. /* Find conversation by ID, the fast way or the slow way.. */
  633. static PurpleConversation *
  634. sapphire_find_conversation(const gchar *chat)
  635. {
  636. PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
  637. if (conv)
  638. return conv;
  639. /* Not in the hash table -- so iterate */
  640. SapphireChat *schat = sapphire_find_chat(chat, FALSE);
  641. return schat ? schat->conv : NULL;
  642. }
  643. static gchar *
  644. sapphire_id_from_parts(PurpleAccount *account, const gchar *id)
  645. {
  646. gchar *acct = sapphire_serialize_account_id(account);
  647. return g_strdup_printf("%s|%s", acct, id);
  648. }
  649. /* By contrast, this routine serializes a buddy by value, including the ID
  650. * generated by the previous function as well as the actual metadata */
  651. static JsonObject *
  652. sapphire_serialize_buddy_object(PurpleBuddy *buddy)
  653. {
  654. JsonObject *json = json_object_new();
  655. PurpleGroup *group = purple_buddy_get_group(buddy);
  656. const gchar *name = purple_buddy_get_name(buddy);
  657. const gchar *alias = purple_buddy_get_contact_alias(buddy);
  658. const gchar *group_name = purple_group_get_name(group);
  659. gchar *id = sapphire_serialize_buddy_id(buddy);
  660. /* We might have an icon. If so, get it ready for later access, but do
  661. * not send it here. Merely record if there is an icon or not */
  662. PurpleAccount *account = purple_buddy_get_account(buddy);
  663. PurpleBuddyIcon *icon = purple_buddy_icons_find(account, name);
  664. if (icon != NULL) {
  665. purple_buddy_icon_ref(icon);
  666. sapphire_add_buddy_icon(id, icon);
  667. }
  668. json_object_set_boolean_member(json, "hasIcon", icon != NULL);
  669. json_object_set_string_member(json, "id", id);
  670. json_object_set_string_member(json, "name", name);
  671. json_object_set_string_member(json, "alias", alias);
  672. json_object_set_string_member(json, "group", group_name);
  673. gchar *accountID = sapphire_serialize_account_id(account);
  674. if (accountID) {
  675. json_object_set_string_member(json, "account", accountID);
  676. g_free(accountID);
  677. }
  678. sapphire_serialize_status(json, sapphire_status_for_buddy(buddy));
  679. /* Include the ID of the buddy itself */
  680. sapphire_serialize_buddy(json, buddy);
  681. g_free(id);
  682. return json;
  683. }
  684. /* Serialize the account itself for personal information */
  685. static JsonObject *
  686. sapphire_serialize_account(PurpleAccount *account)
  687. {
  688. JsonObject *json = json_object_new();
  689. const gchar *prpl = purple_account_get_protocol_id(account);
  690. const gchar *prpl_name = purple_account_get_protocol_name(account);
  691. const gchar *username = purple_account_get_username(account);
  692. const gchar *alias = purple_account_get_alias(account);
  693. json_object_set_string_member(json, "prpl", prpl);
  694. json_object_set_string_member(json, "prplName", prpl_name);
  695. json_object_set_string_member(json, "name", username);
  696. json_object_set_string_member(json, "alias", alias);
  697. gchar *id = sapphire_serialize_account_id(account);
  698. json_object_set_string_member(json, "id", id);
  699. /* Add our own icon, if applicable, to the store */
  700. PurpleStoredImage *icon =
  701. purple_buddy_icons_find_account_icon(account);
  702. if (icon)
  703. sapphire_add_stored_image(id, icon);
  704. json_object_set_boolean_member(json, "hasIcon", icon != NULL);
  705. g_free(id);
  706. return json;
  707. }
  708. /* Sends the entire world to a new connection. For this, we need to send:
  709. *
  710. * - information about our accounts
  711. * - the buddy list
  712. * - rooms we're in
  713. * - missed messages
  714. *
  715. * Essentially, everything needed for the initial client render.
  716. *
  717. * We do _not_ need to send anything that's not immediately accessible; for
  718. * instance, we can avoid sending the users in present rooms that are not on
  719. * our buddy list, deferring to when we explicitly open the room
  720. *
  721. */
  722. void
  723. sapphire_send_world(Connection *conn)
  724. {
  725. /* Initialize connected state */
  726. conn->subscribed_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  727. JsonObject *data = json_object_new();
  728. json_object_set_string_member(data, "op", "world");
  729. /* Iterate the buddy list of connected accounts to include buddies */
  730. JsonArray *jbuddies = json_array_new();
  731. JsonArray *jaccounts = json_array_new();
  732. JsonArray *jchats = json_array_new();
  733. GSList *acct;
  734. for (acct = purple_accounts; acct != NULL; acct = acct->next) {
  735. PurpleAccount *account = (PurpleAccount *) acct->data;
  736. /* Add buddies from account */
  737. GSList *blist = purple_find_buddies(account, NULL);
  738. for (GSList *it = blist; it != NULL; it = it->next) {
  739. PurpleBuddy *buddy = (PurpleBuddy *) it->data;
  740. JsonObject *bud = sapphire_serialize_buddy_object(buddy);
  741. const gchar *bid = json_object_get_string_member(bud, "buddy");
  742. sapphire_serialize_unacked_messages(bud, bid);
  743. json_array_add_object_element(jbuddies, bud);
  744. }
  745. /* Add metadata for the account itself */
  746. JsonObject *j_account = sapphire_serialize_account(account);
  747. json_array_add_object_element(jaccounts, j_account);
  748. g_slist_free(blist);
  749. }
  750. /* Send chats */
  751. for (GSList *it = chats; it != NULL; it = it->next) {
  752. SapphireChat *schat = (SapphireChat *) it->data;
  753. json_array_add_object_element(jchats, sapphire_serialize_chat(schat));
  754. }
  755. /* TODO: What if one is.. both? */
  756. json_object_set_array_member(data, "buddies", jbuddies);
  757. json_object_set_array_member(data, "chats", jchats);
  758. json_object_set_array_member(data, "accounts", jaccounts);
  759. sapphire_send(conn, data);
  760. json_object_unref(data);
  761. }
  762. static void
  763. sapphire_buddy_status_changed(PurpleBuddy *buddy, gpointer null)
  764. {
  765. JsonObject *data = json_object_new();
  766. json_object_set_string_member(data, "op", "buddyStatus");
  767. sapphire_serialize_status(data, sapphire_status_for_buddy(buddy));
  768. sapphire_serialize_buddy(data, buddy);
  769. sapphire_broadcast(data);
  770. json_object_unref(data);
  771. }
  772. static void
  773. sapphire_serialize_typing_state(JsonObject *data, PurpleTypingState state)
  774. {
  775. /* While we could pass is, that risks future libpurple updates causing
  776. * breakage */
  777. int s_state =
  778. (state == PURPLE_NOT_TYPING) ? 0 :
  779. (state == PURPLE_TYPING) ? 1 :
  780. (state == PURPLE_TYPED) ? 2 :
  781. -1;
  782. json_object_set_int_member(data, "state", s_state);
  783. }
  784. static PurpleTypingState
  785. sapphire_decode_typing_state(int s_state)
  786. {
  787. return (s_state == 0) ? PURPLE_NOT_TYPING :
  788. (s_state == 1) ? PURPLE_TYPING :
  789. (s_state == 2) ? PURPLE_TYPED :
  790. PURPLE_NOT_TYPING;
  791. }
  792. static void
  793. sapphire_buddy_typing_changed(PurpleAccount *account, const char *name, gpointer null)
  794. {
  795. JsonObject *data = json_object_new();
  796. json_object_set_string_member(data, "op", "typing");
  797. PurpleBuddy *buddy = purple_find_buddy(account, name);
  798. sapphire_serialize_buddy(data, buddy);
  799. PurpleConvIm *im = sapphire_im_for_name(account, name);
  800. PurpleTypingState state = purple_conv_im_get_typing_state(im);
  801. sapphire_serialize_typing_state(data, state);
  802. sapphire_broadcast(data);
  803. json_object_unref(data);
  804. }
  805. static void
  806. sapphire_received_message(PurpleAccount *account, const char *who, const char *message, PurpleConversation *conv,
  807. PurpleMessageFlags flags, gpointer null)
  808. {
  809. /* Find the buddy since the arguments as-is are difficult to work with */
  810. PurpleBuddy *buddy = NULL;
  811. /* Whether channel_id needs a g_free */
  812. gboolean should_free_channel_id = TRUE;
  813. gchar *channel_id;
  814. JsonObject *data = json_object_new();
  815. json_object_set_string_member(data, "op", "message");
  816. /* Serialization depends on the type of "buffer" in use; we don't
  817. * smooth out the incongruence between IMs and chats until we're in
  818. * backend.js on the client */
  819. PurpleConversationType type = purple_conversation_get_type(conv);
  820. if (type == PURPLE_CONV_TYPE_IM) {
  821. /* Serialize the buddy we're talking to */
  822. buddy = purple_find_buddy(account, who);
  823. sapphire_serialize_buddy(data, buddy);
  824. channel_id = sapphire_serialize_buddy_id(buddy);
  825. } else if (type == PURPLE_CONV_TYPE_CHAT) {
  826. /* Serialize the chat itself */
  827. channel_id = sapphire_id_from_conv(conv);
  828. json_object_set_string_member(data, "chat", channel_id);
  829. if (flags & PURPLE_MESSAGE_SYSTEM) {
  830. json_object_set_string_member(data, "who", "system");
  831. } else {
  832. /* And just the ID of who sent it. */
  833. gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
  834. json_object_set_string_member(data, "who", user_id);
  835. g_free(user_id);
  836. /* ...in case the user is offline and not a buddy, also supply an alias */
  837. json_object_set_string_member(data, "alias", who);
  838. }
  839. } else {
  840. printf("Wat? nonbuddy, non chat?\n");
  841. }
  842. //json_object_set_int_member(data, "time", mtime);
  843. json_object_set_int_member(data, "flags", flags);
  844. /* Since we might be OTR-protected, the backend can't do anything with the plaintext */
  845. json_object_set_string_member(data, "content", message);
  846. /* So, if there are connected clients, we broadcast to them. Otherwise, we need to
  847. * store the message, so we can replay messages later for when we
  848. * connect. It's okay if the lookup fails and we null, g_list functions
  849. * don't mind. Additionally, if this is the first message like this,
  850. * we'll need to send a push notification. */
  851. if (sapphire_any_connected_clients()) {
  852. /* Broadcast */
  853. gchar *str = json_object_to_string(data);
  854. sapphire_broadcast_raw_packet(str);
  855. } else {
  856. /* Save the message */
  857. if (type == PURPLE_CONV_TYPE_IM) {
  858. GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, channel_id);
  859. unacked_list = g_list_prepend(unacked_list, json_object_ref(data));
  860. g_hash_table_replace(id_to_unacked_list, channel_id, unacked_list);
  861. should_free_channel_id = FALSE;
  862. } else {
  863. /* TODO: Chats. At the moment, these can accumulate
  864. * huge amounts of memory, so disabling for now, mk? */
  865. }
  866. }
  867. /* Send a notification for IMs. The push notification module will
  868. * determine if it's necessary */
  869. if (type == PURPLE_CONV_TYPE_IM) {
  870. const char *alias = purple_buddy_get_alias(buddy);
  871. gchar *notification = g_strdup_printf("Psst, %s messaged you via Sapphire\n", alias);
  872. sapphire_push_notification(notification);
  873. g_free(notification);
  874. }
  875. if (should_free_channel_id)
  876. g_free(channel_id);
  877. json_object_unref(data);
  878. }
  879. static void
  880. sapphire_topic_changed(PurpleConversation *conv, const char *who, const char *topic, gpointer null)
  881. {
  882. JsonObject *data = json_object_new();
  883. gchar *chat_id = sapphire_id_from_conv(conv);
  884. json_object_set_string_member(data, "op", "topic");
  885. json_object_set_string_member(data, "who", who);
  886. json_object_set_string_member(data, "topic", topic);
  887. json_object_set_string_member(data, "chat", chat_id);
  888. sapphire_broadcast(data);
  889. json_object_unref(data);
  890. g_free(chat_id);
  891. }
  892. /* A buddy joined in a room we're subscribed to -- but that doesn't mean the
  893. * client needs to know. Only send the joined event to clients that have opened
  894. * the corresponding conversation */
  895. extern GList *authenticated_connections;
  896. static void
  897. sapphire_buddy_joined(PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags, gboolean new_arrival, gpointer null)
  898. {
  899. JsonObject *data = json_object_new();
  900. gchar *chat_id = sapphire_id_from_conv(conv);
  901. json_object_set_string_member(data, "op", "joined");
  902. json_object_set_string_member(data, "chat", chat_id);
  903. /* Send a single element worth of users :( */
  904. JsonArray *lst = json_array_new();
  905. JsonObject *buddy = json_object_new();
  906. printf("Got %s\n", who);
  907. sapphire_serialize_chat_buddy(buddy, conv, who, flags);
  908. json_array_add_object_element(lst, buddy);
  909. json_object_set_array_member(data, "members", lst);
  910. /* TODO: Maybe don't serialize so many times */
  911. /* TODO: Don't serialize at all if nobody's subscribed */
  912. for (GList *it = authenticated_connections; it != NULL; it = it->next) {
  913. Connection *conn = (Connection *) it->data;
  914. if (g_hash_table_contains(conn->subscribed_ids, chat_id))
  915. sapphire_send(conn, data);
  916. }
  917. g_free(chat_id);
  918. json_object_unref(data);
  919. }
  920. static void
  921. sapphire_joined_chat(PurpleConversation *conv, gpointer null)
  922. {
  923. /* Try to use the existing chat */
  924. gchar *id = sapphire_id_from_conv(conv);
  925. SapphireChat *schat = sapphire_find_chat(id, TRUE);
  926. if (!schat) {
  927. /* Surprise! Create a new chat */
  928. schat = sapphire_chat_from_conv(conv);
  929. schat->conv = conv;
  930. printf("Joining chat %s\n", schat->id);
  931. chats = g_slist_append(chats, schat);
  932. } else {
  933. /* Associate with the conv */
  934. schat->conv = conv;
  935. }
  936. g_hash_table_insert(blist_id_to_conversation, id, conv);
  937. /* It's joined! */
  938. if (!g_hash_table_contains(id_to_joined, id)) {
  939. g_hash_table_add(id_to_joined, g_strdup(id));
  940. g_hash_table_insert(id_to_chat, g_strdup(id), schat);
  941. }
  942. }
  943. /* Certain prpls, particularly those for third-party protocols, should be
  944. * disabled when not in active use. This function, called from the socket
  945. * handling when a client connects or disconnects, checks if there are active
  946. * connections. If there are, relevant prpls are enabled; if not, they are
  947. * disabled. */
  948. static gboolean
  949. sapphire_prpl_defer_connects(const gchar *protocol_id)
  950. {
  951. return purple_strequal(protocol_id, "prpl-eionrobb-discord");
  952. }
  953. void
  954. sapphire_enable_accounts_by_connections(void)
  955. {
  956. gboolean should_enable = sapphire_any_connected_clients();
  957. for (GSList *it = purple_accounts; it != NULL; it = it->next) {
  958. PurpleAccount *account = (PurpleAccount *) it->data;
  959. const gchar *protocol_id = purple_account_get_protocol_id(account);
  960. /* Check if the protocol has this quirk */
  961. if (!sapphire_prpl_defer_connects(protocol_id))
  962. continue;
  963. /* It does -- so check which direction we need to go */
  964. gboolean enab = purple_account_get_enabled(account, UI_ID);
  965. if (should_enable != enab) {
  966. purple_account_set_enabled(account, UI_ID, should_enable);
  967. }
  968. }
  969. }
  970. #ifdef _WIN32
  971. #include <windows.h>
  972. extern BOOL SetDllDirectoryA(LPCSTR lpPathName);
  973. typedef void (WINAPI* LPFNSETDLLDIRECTORY)(LPCSTR);
  974. static LPFNSETDLLDIRECTORY MySetDllDirectory = NULL;
  975. #endif
  976. static void
  977. init_libpurple(void)
  978. {
  979. #ifdef _WIN32
  980. purple_util_set_user_dir("./.purple");
  981. HMODULE hmod;
  982. if ((hmod = GetModuleHandleW(L"kernel32.dll"))) {
  983. MySetDllDirectory = (LPFNSETDLLDIRECTORY) GetProcAddress(
  984. hmod, "SetDllDirectoryA");
  985. if (!MySetDllDirectory)
  986. printf("SetDllDirectory not supported\n");
  987. } else
  988. printf("Error getting kernel32.dll module handle\n");
  989. /* For Windows XP SP1+ / Server 2003 we use SetDllDirectory to avoid dll hell */
  990. if (MySetDllDirectory) {
  991. printf("Using SetDllDirectory\n");
  992. MySetDllDirectory("C:/Program Files (x86)/Pidgin/");
  993. }
  994. #endif
  995. gchar *search_path = g_build_filename(purple_user_dir(), "plugins", NULL);
  996. purple_plugins_add_search_path(search_path);
  997. g_free(search_path);
  998. #ifdef _WIN32
  999. purple_plugins_add_search_path("C:/Program Files (x86)/Pidgin/plugins/");
  1000. purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/");
  1001. purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/plugins/");
  1002. #endif
  1003. purple_debug_set_enabled(FALSE);
  1004. sapphire_set_eventloop();
  1005. if (!purple_core_init(UI_ID)) {
  1006. fprintf(stderr,
  1007. "libpurple initialization failed. Dumping core.\n"
  1008. "Please report this!\n");
  1009. abort();
  1010. }
  1011. purple_set_blist(purple_blist_new());
  1012. purple_blist_load();
  1013. purple_prefs_load();
  1014. purple_plugins_load_saved(PLUGIN_SAVE_PREF);
  1015. purple_pounces_load();
  1016. }
  1017. static void
  1018. sapphire_connect_signals(void)
  1019. {
  1020. static int handle;
  1021. purple_signal_connect(purple_accounts_get_handle(), "account-signed-on", &handle,
  1022. PURPLE_CALLBACK(sapphire_signed_on), NULL);
  1023. purple_signal_connect(purple_accounts_get_handle(), "account-enabled", &handle,
  1024. PURPLE_CALLBACK(sapphire_account_enabled), NULL);
  1025. purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", &handle,
  1026. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1027. purple_signal_connect(purple_blist_get_handle(), "buddy-signed-off", &handle,
  1028. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1029. purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", &handle,
  1030. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1031. purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", &handle,
  1032. PURPLE_CALLBACK(sapphire_received_message), NULL);
  1033. purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", &handle,
  1034. PURPLE_CALLBACK(sapphire_received_message), NULL);
  1035. purple_signal_connect(purple_conversations_get_handle(), "buddy-typing", &handle,
  1036. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1037. purple_signal_connect(purple_conversations_get_handle(), "buddy-typed", &handle,
  1038. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1039. purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped", &handle,
  1040. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1041. purple_signal_connect(purple_conversations_get_handle(), "chat-joined", &handle,
  1042. PURPLE_CALLBACK(sapphire_joined_chat), NULL);
  1043. purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-joined", &handle,
  1044. PURPLE_CALLBACK(sapphire_buddy_joined), NULL);
  1045. purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", &handle,
  1046. PURPLE_CALLBACK(sapphire_topic_changed), NULL);
  1047. }
  1048. int main(int argc, char *argv[])
  1049. {
  1050. GMainLoop *loop;
  1051. PurpleSavedStatus *status;
  1052. #ifndef _WIN32
  1053. /* libpurple's built-in DNS resolution forks processes to perform
  1054. * blocking lookups without blocking the main process. It does not
  1055. * handle SIGCHLD itself, so if the UI does not you quickly get an army
  1056. * of zombie subprocesses marching around.
  1057. */
  1058. signal(SIGCHLD, SIG_IGN);
  1059. #endif
  1060. #ifdef _WIN32
  1061. g_thread_init(NULL);
  1062. #endif
  1063. g_set_prgname("Sapphire");
  1064. g_set_application_name("Sapphire");
  1065. loop = g_main_loop_new(NULL, FALSE);
  1066. g_main_loop_ref(loop);
  1067. gboolean jailed = (argc >= 2) && (purple_strequal(argv[1], "--jailed"));
  1068. if (jailed) {
  1069. /* If we're running in firejail, we can't use a .purple, since
  1070. * the hidden nature will cause permission errors. Instead, use
  1071. * an opaque name */
  1072. purple_util_set_user_dir("./purple");
  1073. }
  1074. init_libpurple();
  1075. purple_prefs_add_none("/purple/sapphire");
  1076. if (!purple_prefs_get_string(SAPPHIRE_PUSH_EMAIL_PREF)) {
  1077. printf("Push notification email (blank to disable): ");
  1078. char email[128];
  1079. fgets(email, sizeof(email), stdin);
  1080. purple_prefs_add_string(SAPPHIRE_PUSH_EMAIL_PREF, email);
  1081. }
  1082. /* Initialize global hash tables */
  1083. id_to_buddy = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1084. id_to_unacked_list = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1085. id_to_chat = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1086. id_to_account = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1087. id_to_joined = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1088. blist_id_to_conversation = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1089. sent_icons = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1090. sapphire_connect_signals();
  1091. sapphire_init_websocket();
  1092. g_main_context_iteration(g_main_loop_get_context(loop), FALSE);
  1093. GList *l;
  1094. /* Fetch account and enable it */
  1095. for (l = purple_accounts_get_all(); l != NULL; l = l->next) {
  1096. PurpleAccount *candidate = (PurpleAccount *)l->data;
  1097. const gchar *protocol_id = purple_account_get_protocol_id(candidate);
  1098. if (purple_strequal(protocol_id, "prpl-jabber") || purple_strequal(protocol_id, "prpl-eionrobb-discord")) {
  1099. purple_accounts = g_slist_append(purple_accounts, candidate);
  1100. purple_account_set_enabled(candidate, UI_ID, !sapphire_prpl_defer_connects(protocol_id));
  1101. } else {
  1102. purple_account_set_enabled(candidate, UI_ID, FALSE);
  1103. }
  1104. }
  1105. if (!purple_accounts) {
  1106. fprintf(stderr, "No accounts found\n");
  1107. return 1;
  1108. }
  1109. /* Now, to connect the account(s), create a status and activate it. */
  1110. status = purple_savedstatus_new(NULL, PURPLE_STATUS_AVAILABLE);
  1111. purple_savedstatus_activate(status);
  1112. g_main_loop_run(loop);
  1113. return 0;
  1114. }