core.c 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  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 -> bool isJoined*/
  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_joined = (uintptr_t) g_hash_table_lookup(id_to_joined, id);
  265. gboolean is_subscribed = g_hash_table_contains(conn->subscribed_ids, id);
  266. if (!is_joined) {
  267. purple_roomlist_room_join(chat->roomlist, chat->room);
  268. g_hash_table_insert(id_to_joined, g_strdup(id), (void *) TRUE);
  269. } else if (!is_subscribed) {
  270. /* If we already joined but not in this connection, just send back details */
  271. const gchar *topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(chat->conv));
  272. JsonArray *users = sapphire_serialize_chat_users(chat);
  273. JsonObject *data = json_object_new();
  274. json_object_set_string_member(data, "op", "joined");
  275. json_object_set_string_member(data, "chat", id);
  276. json_object_set_string_member(data, "topic", topic);
  277. json_object_set_array_member(data, "members", users);
  278. sapphire_send(conn, data);
  279. json_object_unref(data);
  280. }
  281. if (!is_subscribed) {
  282. /* We want to know about this room */
  283. g_hash_table_add(conn->subscribed_ids, g_strdup(id));
  284. }
  285. } else if (purple_strequal(op, "topic")) {
  286. const gchar *chat = json_object_get_string_member(data, "chat");
  287. const gchar *topic = json_object_get_string_member(data, "topic");
  288. PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
  289. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
  290. int id = purple_conv_chat_get_id(conv_chat);
  291. PurpleAccount *account = purple_conversation_get_account(conv);
  292. PurpleConnection *connection = purple_account_get_connection(account);
  293. PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
  294. if (prpl_info && prpl_info->set_chat_topic)
  295. prpl_info->set_chat_topic(connection, id, topic);
  296. else
  297. printf("Set chat topic unimplemented\n");
  298. } else if (purple_strequal(op, "markAsRead")) {
  299. const gchar *id = json_object_get_string_member(data, "id");
  300. /* Free the unacked list entries */
  301. GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, id);
  302. for (GList *it = unacked_list; it != NULL; it = it->next) {
  303. JsonObject *msg = (JsonObject *) it->data;
  304. json_object_unref(msg);
  305. }
  306. /* Free the list itself */
  307. g_list_free(unacked_list);
  308. /* And remove it from the hash table */
  309. g_hash_table_remove(id_to_unacked_list, id);
  310. /* Fake a PURPLE_CONV_UPDATE_UNSEEN signal, so that the room gets
  311. * marked as read.
  312. */
  313. PurpleConversation *conv = sapphire_conversation_for_id(id);
  314. if (!conv) {
  315. fprintf(stderr, "Conversation not found in markAsRead %s\n", id);
  316. return;
  317. }
  318. purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
  319. } else if (purple_strequal(op, "requestBuddy")) {
  320. /* Request to add a buddy */
  321. const gchar *id = json_object_get_string_member(data, "id");
  322. const gchar *alias = json_object_get_string_member(data, "alias");
  323. const gchar *invite = json_object_get_string_member(data, "invite");
  324. PurpleAccount *account = sapphire_decode_account(data);
  325. PurpleBuddy *buddy = purple_buddy_new(account, id, alias);
  326. purple_blist_add_buddy(buddy, NULL, NULL, NULL);
  327. purple_account_add_buddy_with_invite(account, buddy, invite);
  328. } else if (purple_strequal(op, "changeAvatar")) {
  329. /* Request to change our avatar */
  330. PurpleAccount *account = sapphire_decode_account(data);
  331. const gchar *base64 = json_object_get_string_member(data, "base64");
  332. size_t len;
  333. guchar *l_data = g_base64_decode(base64, &len);
  334. PurpleStoredImage *icon = purple_buddy_icons_set_account_icon(account, l_data, len);
  335. /* Update the cache */
  336. const gchar *raw_acct = json_object_get_string_member(data, "account");
  337. sapphire_add_stored_image(raw_acct, icon);
  338. /* Respond that we did it! */
  339. JsonObject *resp = json_object_new();
  340. json_object_set_string_member(resp, "op", "changeAvatar");
  341. json_object_set_string_member(resp, "id", raw_acct);
  342. sapphire_broadcast(resp);
  343. json_object_unref(resp);
  344. } else {
  345. fprintf(stderr, "Unknown op %s\n", op);
  346. }
  347. }
  348. /*** Conversation uiops ***/
  349. static gchar *
  350. sapphire_id_from_parts(PurpleAccount *account, const gchar *id);
  351. static void
  352. sapphire_signed_on(PurpleAccount *account, gpointer null)
  353. {
  354. PurpleConnection *connection = purple_account_get_connection(account);
  355. /* Upsert the account ID */
  356. gchar *acct_id = sapphire_serialize_account_id(account);
  357. if (g_hash_table_contains(id_to_account, acct_id)) {
  358. /* Wait. We already did this account. Bail! TODO: Sync */
  359. g_free(acct_id);
  360. return;
  361. }
  362. g_hash_table_insert(id_to_account, acct_id, account);
  363. /* For type-1 prpls where the openness of a chat determines whether we
  364. * receive events (e.g. IRC), open up all chats as early as possible */
  365. PurpleBlistNode *node;
  366. for ( node = purple_blist_get_root();
  367. node != NULL;
  368. node = purple_blist_node_next(node, TRUE)) {
  369. if (PURPLE_BLIST_NODE_IS_CHAT(node)) {
  370. PurpleChat *chat = PURPLE_CHAT(node);
  371. if (purple_chat_get_account(chat) != account) continue;
  372. GHashTable *components = purple_chat_get_components(chat);
  373. purple_serv_join_chat(connection, components);
  374. }
  375. }
  376. /* For type-2 prpls where we fetch from the room list (e.g. Discord),
  377. * fetch now but do not open yet, since we don't want to spam the
  378. * servers */
  379. if (purple_strequal(account->protocol_id, "prpl-eionrobb-discord")) {
  380. PurpleRoomlist *roomlist = purple_roomlist_get_list(connection);
  381. /* We're persisting the roomlist until later */
  382. purple_roomlist_ref(roomlist);
  383. gboolean in_progress = purple_roomlist_get_in_progress(roomlist);
  384. if (in_progress) {
  385. printf("In progress room list, aborting\n");
  386. return;
  387. }
  388. /* Check the field headings to figure out what to display (name) and index by (ID) */
  389. GList *field_headings = purple_roomlist_get_fields(roomlist);
  390. int index_id = -1, index_name = -1;
  391. int field_idx = 0;
  392. for (; field_headings != NULL; field_headings = field_headings->next, ++field_idx) {
  393. PurpleRoomlistField *field = (PurpleRoomlistField *) field_headings->data;
  394. const char *label = purple_roomlist_field_get_label(field);
  395. gboolean hidden = purple_roomlist_field_get_hidden(field);
  396. if (index_id == -1 && hidden) {
  397. index_id = field_idx;
  398. } else if (index_name == -1 && purple_strequal(label, "Name")) {
  399. index_name = field_idx;
  400. } else {
  401. /* Useless field */
  402. }
  403. }
  404. /* Now, scan the rooms */
  405. GList *rooms = roomlist->rooms; /* XXX: purple3 */
  406. for (; rooms != NULL; rooms = rooms->next) {
  407. PurpleRoomlistRoom *room = (PurpleRoomlistRoom *) rooms->data;
  408. PurpleRoomlistRoomType type = purple_roomlist_room_get_type(room);
  409. /* Skip over categories */
  410. if (type != PURPLE_ROOMLIST_ROOMTYPE_ROOM)
  411. continue;
  412. /* ...but do fetch our category name! */
  413. PurpleRoomlistRoom *parent = purple_roomlist_room_get_parent(room);
  414. const char *group_name = parent ? purple_roomlist_room_get_name(parent) : "Rooms";
  415. GList *fields = purple_roomlist_room_get_fields(room);
  416. const char *id = NULL;
  417. const char *display_name = NULL;
  418. for (int idx = 0; fields != NULL; fields = fields->next, ++idx) {
  419. gchar *value = (gchar *) fields->data;
  420. if (idx == index_id)
  421. id = value;
  422. if (idx == index_name)
  423. display_name = value;
  424. }
  425. /* XXX: Do magic from purple-discord to format ID */
  426. guint64 gid = g_ascii_strtoull(id, NULL, 10);
  427. int nid = ABS((gint) gid);
  428. gchar *snid = g_strdup_printf("%d", nid);
  429. gchar *sapphic_id = sapphire_id_from_parts(account, snid);
  430. g_free(snid);
  431. /* Save the chat */
  432. SapphireChat *schat = sapphire_new_chat(account, g_strdup(sapphic_id), display_name, group_name);
  433. schat->roomlist = roomlist;
  434. schat->room = room;
  435. printf("Saving Dithcord with %s\n", sapphic_id);
  436. chats = g_slist_prepend(chats, schat);
  437. /* No need to g_strdup(sapphic_id) since we already have the exclusive reference */
  438. g_hash_table_insert(id_to_chat, sapphic_id, schat);
  439. }
  440. }
  441. }
  442. static void
  443. sapphire_account_enabled(PurpleAccount *account, gpointer null)
  444. {
  445. printf("Account enabled: %s %s\n", account->username, account->protocol_id);
  446. }
  447. /* Serializes the actual content of a status */
  448. static void
  449. sapphire_serialize_status(JsonObject *data, PurpleStatus *status)
  450. {
  451. JsonObject *obj = json_object_new();
  452. const gchar *id = purple_status_get_id(status);
  453. const gchar *name = purple_status_get_name(status);
  454. const gchar *message = purple_status_get_attr_string(status, "message");
  455. json_object_set_string_member(obj, "id", id);
  456. json_object_set_string_member(obj, "name", name);
  457. if (message != NULL)
  458. json_object_set_string_member(obj, "message", message);
  459. json_object_set_object_member(data, "status", obj);
  460. }
  461. /* Serializes a buddy "by reference", by hashing the buddy. Requires a
  462. * corresponding `buddy` op to be meaningful for the client. Requires
  463. * disambiguating by account, prpl, etc as well as just the name.
  464. * Simultaneously "upserts" the buddy into the global hash table for later
  465. * access.
  466. *
  467. * Result: serialized string, heap allocated. Must be g_free'd later.
  468. */
  469. static gchar *
  470. sapphire_serialize_user_id(PurpleAccount *account, const gchar *name)
  471. {
  472. const gchar *prpl = purple_account_get_protocol_id(account);
  473. const gchar *account_id = purple_account_get_username(account);
  474. /* Smush together the features into a unique ID. TODO: Hash */
  475. gchar *smushed = g_strdup_printf("%s|%s|%s", prpl, account_id, name);
  476. return smushed;
  477. }
  478. static gchar *
  479. sapphire_serialize_buddy_id(PurpleBuddy *buddy)
  480. {
  481. /* Get distinguishing features */
  482. PurpleAccount *p_account = purple_buddy_get_account(buddy);
  483. const gchar *name = purple_normalize(p_account, purple_buddy_get_name(buddy));
  484. gchar *smushed = sapphire_serialize_user_id(p_account, name);
  485. /* Upsert. TODO: Will PurpleBuddy get garbage collected on us? */
  486. if (!g_hash_table_lookup(id_to_buddy, smushed)) {
  487. g_hash_table_replace(id_to_buddy, g_strdup(smushed), buddy);
  488. }
  489. return smushed;
  490. }
  491. /* Resolve from bare nickname who to actual ID */
  492. static gchar *
  493. sapphire_serialize_chat_user_id(PurpleConversation *conv, const gchar *who)
  494. {
  495. PurpleAccount *account = purple_conversation_get_account(conv);
  496. PurpleConnection *connection = purple_account_get_connection(account);
  497. PurplePluginProtocolInfo *prpl_info = sapphire_info_for_connection(connection);
  498. if (prpl_info && prpl_info->get_cb_real_name) {
  499. /* Get the user's intra-protocol canonical name */
  500. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(conv);
  501. int id = purple_conv_chat_get_id(conv_chat);
  502. gchar *real_name = prpl_info->get_cb_real_name(connection, id, who);
  503. gchar *normalized = g_strdup(purple_normalize(account, real_name));
  504. /* Check if it's, uh, us */
  505. const char *username = purple_normalize(account, purple_account_get_username(account));
  506. const char *display_name = purple_connection_get_display_name(connection);
  507. if (purple_strequal(username, normalized) || purple_strequal(display_name, normalized)) {
  508. g_free(normalized);
  509. return sapphire_serialize_account_id(account);
  510. }
  511. printf("From %s to %s to %s\n", who, real_name, normalized);
  512. /* Serialize it formally for protocol independence */
  513. gchar *out = sapphire_serialize_user_id(account, normalized);
  514. g_free(normalized);
  515. g_free(real_name);
  516. return out;
  517. } else {
  518. printf("Bailing on %s\n", who);
  519. return g_strdup(who);
  520. }
  521. }
  522. static void
  523. sapphire_serialize_buddy(JsonObject *data, PurpleBuddy *buddy)
  524. {
  525. gchar *id = sapphire_serialize_buddy_id(buddy);
  526. json_object_set_string_member(data, "buddy", id);
  527. g_free(id);
  528. }
  529. static void
  530. sapphire_serialize_chat_buddy(JsonObject *data, PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags)
  531. {
  532. gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
  533. json_object_set_string_member(data, "id", user_id);
  534. json_object_set_string_member(data, "alias", who);
  535. json_object_set_int_member(data, "flags", flags);
  536. g_free(user_id);
  537. }
  538. /* Add missed messages to buddy/chat object if applicable */
  539. static void
  540. sapphire_serialize_unacked_messages(JsonObject *obj, const gchar *id)
  541. {
  542. GList *lst = g_hash_table_lookup(id_to_unacked_list, id);
  543. if (!lst)
  544. return;
  545. /* Pop missed messages in reverse order */
  546. GList *it;
  547. JsonArray *unacked = json_array_new();
  548. for (it = lst; it != NULL; it = it->next) {
  549. JsonObject *msg = (JsonObject *) it->data;
  550. json_array_add_object_element(unacked, msg);
  551. }
  552. json_object_set_array_member(obj, "unacked", unacked);
  553. }
  554. static JsonArray *
  555. sapphire_serialize_chat_users(SapphireChat *chat)
  556. {
  557. JsonArray *jusers = json_array_new();
  558. if (chat->conv) {
  559. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat->conv);
  560. for (GList *l = purple_conv_chat_get_users(conv_chat); l != NULL; l = l->next) {
  561. JsonObject *juser = json_object_new();
  562. PurpleConvChatBuddy *cb = (PurpleConvChatBuddy *) l->data;
  563. const gchar *who = purple_conv_chat_cb_get_name(cb);
  564. PurpleConvChatBuddyFlags flags = purple_conv_chat_user_get_flags(conv_chat, who);
  565. printf("For %s %s\n", cb->name, cb->alias);
  566. sapphire_serialize_chat_buddy(juser, chat->conv, who, flags);
  567. json_array_add_object_element(jusers, juser);
  568. }
  569. }
  570. return jusers;
  571. }
  572. /* Serializes the unopened chat pieces, not the conversation bits which have a
  573. * rather more complex path */
  574. static JsonObject *
  575. sapphire_serialize_chat(SapphireChat *chat)
  576. {
  577. JsonObject *obj = json_object_new();
  578. json_object_set_string_member(obj, "id", chat->id);
  579. json_object_set_string_member(obj, "name", chat->name);
  580. json_object_set_string_member(obj, "group", chat->group);
  581. json_object_set_string_member(obj, "account", chat->account_id);
  582. sapphire_serialize_unacked_messages(obj, chat->id);
  583. return obj;
  584. }
  585. /* Creates pass-by-reference ID for account.
  586. *
  587. * Return: ID as a string (must be freed by caller)
  588. */
  589. static gchar *
  590. sapphire_serialize_account_id(PurpleAccount *account)
  591. {
  592. /* Get features */
  593. const gchar *prpl = purple_account_get_protocol_id(account);
  594. const gchar *username = purple_account_get_username(account);
  595. /* Smush prpl with username to form an ID */
  596. return g_strdup_printf("%s|%s", prpl, username);
  597. }
  598. /* Serialize actual chat ID */
  599. static gchar *
  600. sapphire_id_from_conv(PurpleConversation *chat)
  601. {
  602. PurpleAccount *account = purple_conversation_get_account(chat);
  603. gchar *acct = sapphire_serialize_account_id(account);
  604. PurpleConvChat *conv_chat = purple_conversation_get_chat_data(chat);
  605. int id = purple_conv_chat_get_id(conv_chat);
  606. gchar *full_id = g_strdup_printf("%s|%d", acct, id);
  607. g_free(acct);
  608. return full_id;
  609. }
  610. /* Find a chat by ID */
  611. static SapphireChat *
  612. sapphire_find_chat(const gchar *id, gboolean use_id)
  613. {
  614. for (GSList *it = chats; it != NULL; it = it->next) {
  615. SapphireChat *candidate = (SapphireChat *) it->data;
  616. gboolean match = FALSE;
  617. if (use_id) {
  618. match = purple_strequal(candidate->id, id);
  619. } else if (candidate->conv) {
  620. /* Ignore the provided ID and compute it ourselves */
  621. gchar *chat = sapphire_id_from_conv(candidate->conv);
  622. match = purple_strequal(id, chat);
  623. g_free(chat);
  624. } else {
  625. printf("ERROR: ID ignored but NULL conv\n");
  626. return NULL;
  627. }
  628. if (match)
  629. return candidate;
  630. }
  631. return NULL;
  632. }
  633. /* Find conversation by ID, the fast way or the slow way.. */
  634. static PurpleConversation *
  635. sapphire_find_conversation(const gchar *chat)
  636. {
  637. PurpleConversation *conv = g_hash_table_lookup(blist_id_to_conversation, chat);
  638. if (conv)
  639. return conv;
  640. /* Not in the hash table -- so iterate */
  641. SapphireChat *schat = sapphire_find_chat(chat, FALSE);
  642. return schat ? schat->conv : NULL;
  643. }
  644. static gchar *
  645. sapphire_id_from_parts(PurpleAccount *account, const gchar *id)
  646. {
  647. gchar *acct = sapphire_serialize_account_id(account);
  648. return g_strdup_printf("%s|%s", acct, id);
  649. }
  650. /* By contrast, this routine serializes a buddy by value, including the ID
  651. * generated by the previous function as well as the actual metadata */
  652. static JsonObject *
  653. sapphire_serialize_buddy_object(PurpleBuddy *buddy)
  654. {
  655. JsonObject *json = json_object_new();
  656. PurpleGroup *group = purple_buddy_get_group(buddy);
  657. const gchar *name = purple_buddy_get_name(buddy);
  658. const gchar *alias = purple_buddy_get_contact_alias(buddy);
  659. const gchar *group_name = purple_group_get_name(group);
  660. gchar *id = sapphire_serialize_buddy_id(buddy);
  661. /* We might have an icon. If so, get it ready for later access, but do
  662. * not send it here. Merely record if there is an icon or not */
  663. PurpleAccount *account = purple_buddy_get_account(buddy);
  664. PurpleBuddyIcon *icon = purple_buddy_icons_find(account, name);
  665. if (icon != NULL) {
  666. purple_buddy_icon_ref(icon);
  667. sapphire_add_buddy_icon(id, icon);
  668. }
  669. json_object_set_boolean_member(json, "hasIcon", icon != NULL);
  670. json_object_set_string_member(json, "id", id);
  671. json_object_set_string_member(json, "name", name);
  672. json_object_set_string_member(json, "alias", alias);
  673. json_object_set_string_member(json, "group", group_name);
  674. gchar *accountID = sapphire_serialize_account_id(account);
  675. if (accountID) {
  676. json_object_set_string_member(json, "account", accountID);
  677. g_free(accountID);
  678. }
  679. sapphire_serialize_status(json, sapphire_status_for_buddy(buddy));
  680. /* Include the ID of the buddy itself */
  681. sapphire_serialize_buddy(json, buddy);
  682. g_free(id);
  683. return json;
  684. }
  685. /* Serialize the account itself for personal information */
  686. static JsonObject *
  687. sapphire_serialize_account(PurpleAccount *account)
  688. {
  689. JsonObject *json = json_object_new();
  690. const gchar *prpl = purple_account_get_protocol_id(account);
  691. const gchar *prpl_name = purple_account_get_protocol_name(account);
  692. const gchar *username = purple_account_get_username(account);
  693. const gchar *alias = purple_account_get_alias(account);
  694. json_object_set_string_member(json, "prpl", prpl);
  695. json_object_set_string_member(json, "prplName", prpl_name);
  696. json_object_set_string_member(json, "name", username);
  697. json_object_set_string_member(json, "alias", alias);
  698. gchar *id = sapphire_serialize_account_id(account);
  699. json_object_set_string_member(json, "id", id);
  700. /* Add our own icon, if applicable, to the store */
  701. PurpleStoredImage *icon =
  702. purple_buddy_icons_find_account_icon(account);
  703. if (icon)
  704. sapphire_add_stored_image(id, icon);
  705. json_object_set_boolean_member(json, "hasIcon", icon != NULL);
  706. g_free(id);
  707. return json;
  708. }
  709. /* Sends the entire world to a new connection. For this, we need to send:
  710. *
  711. * - information about our accounts
  712. * - the buddy list
  713. * - rooms we're in
  714. * - missed messages
  715. *
  716. * Essentially, everything needed for the initial client render.
  717. *
  718. * We do _not_ need to send anything that's not immediately accessible; for
  719. * instance, we can avoid sending the users in present rooms that are not on
  720. * our buddy list, deferring to when we explicitly open the room
  721. *
  722. */
  723. void
  724. sapphire_send_world(Connection *conn)
  725. {
  726. /* Initialize connected state */
  727. conn->subscribed_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  728. JsonObject *data = json_object_new();
  729. json_object_set_string_member(data, "op", "world");
  730. /* Iterate the buddy list of connected accounts to include buddies */
  731. JsonArray *jbuddies = json_array_new();
  732. JsonArray *jaccounts = json_array_new();
  733. JsonArray *jchats = json_array_new();
  734. GSList *acct;
  735. for (acct = purple_accounts; acct != NULL; acct = acct->next) {
  736. PurpleAccount *account = (PurpleAccount *) acct->data;
  737. /* Add buddies from account */
  738. GSList *blist = purple_find_buddies(account, NULL);
  739. for (GSList *it = blist; it != NULL; it = it->next) {
  740. PurpleBuddy *buddy = (PurpleBuddy *) it->data;
  741. JsonObject *bud = sapphire_serialize_buddy_object(buddy);
  742. const gchar *bid = json_object_get_string_member(bud, "buddy");
  743. sapphire_serialize_unacked_messages(bud, bid);
  744. json_array_add_object_element(jbuddies, bud);
  745. }
  746. /* Add metadata for the account itself */
  747. JsonObject *j_account = sapphire_serialize_account(account);
  748. json_array_add_object_element(jaccounts, j_account);
  749. g_slist_free(blist);
  750. }
  751. /* Send chats */
  752. for (GSList *it = chats; it != NULL; it = it->next) {
  753. SapphireChat *schat = (SapphireChat *) it->data;
  754. json_array_add_object_element(jchats, sapphire_serialize_chat(schat));
  755. }
  756. /* TODO: What if one is.. both? */
  757. json_object_set_array_member(data, "buddies", jbuddies);
  758. json_object_set_array_member(data, "chats", jchats);
  759. json_object_set_array_member(data, "accounts", jaccounts);
  760. sapphire_send(conn, data);
  761. json_object_unref(data);
  762. }
  763. static void
  764. sapphire_buddy_status_changed(PurpleBuddy *buddy, gpointer null)
  765. {
  766. JsonObject *data = json_object_new();
  767. json_object_set_string_member(data, "op", "buddyStatus");
  768. sapphire_serialize_status(data, sapphire_status_for_buddy(buddy));
  769. sapphire_serialize_buddy(data, buddy);
  770. sapphire_broadcast(data);
  771. json_object_unref(data);
  772. }
  773. static void
  774. sapphire_serialize_typing_state(JsonObject *data, PurpleTypingState state)
  775. {
  776. /* While we could pass is, that risks future libpurple updates causing
  777. * breakage */
  778. int s_state =
  779. (state == PURPLE_NOT_TYPING) ? 0 :
  780. (state == PURPLE_TYPING) ? 1 :
  781. (state == PURPLE_TYPED) ? 2 :
  782. -1;
  783. json_object_set_int_member(data, "state", s_state);
  784. }
  785. static PurpleTypingState
  786. sapphire_decode_typing_state(int s_state)
  787. {
  788. return (s_state == 0) ? PURPLE_NOT_TYPING :
  789. (s_state == 1) ? PURPLE_TYPING :
  790. (s_state == 2) ? PURPLE_TYPED :
  791. PURPLE_NOT_TYPING;
  792. }
  793. static void
  794. sapphire_buddy_typing_changed(PurpleAccount *account, const char *name, gpointer null)
  795. {
  796. JsonObject *data = json_object_new();
  797. json_object_set_string_member(data, "op", "typing");
  798. PurpleBuddy *buddy = purple_find_buddy(account, name);
  799. sapphire_serialize_buddy(data, buddy);
  800. PurpleConvIm *im = sapphire_im_for_name(account, name);
  801. PurpleTypingState state = purple_conv_im_get_typing_state(im);
  802. sapphire_serialize_typing_state(data, state);
  803. sapphire_broadcast(data);
  804. json_object_unref(data);
  805. }
  806. static void
  807. sapphire_received_message(PurpleAccount *account, const char *who, const char *message, PurpleConversation *conv,
  808. PurpleMessageFlags flags, gpointer null)
  809. {
  810. /* Find the buddy since the arguments as-is are difficult to work with */
  811. PurpleBuddy *buddy = NULL;
  812. /* Whether channel_id needs a g_free */
  813. gboolean should_free_channel_id = TRUE;
  814. gchar *channel_id;
  815. JsonObject *data = json_object_new();
  816. json_object_set_string_member(data, "op", "message");
  817. /* Serialization depends on the type of "buffer" in use; we don't
  818. * smooth out the incongruence between IMs and chats until we're in
  819. * backend.js on the client */
  820. PurpleConversationType type = purple_conversation_get_type(conv);
  821. if (type == PURPLE_CONV_TYPE_IM) {
  822. /* Serialize the buddy we're talking to */
  823. buddy = purple_find_buddy(account, who);
  824. sapphire_serialize_buddy(data, buddy);
  825. channel_id = sapphire_serialize_buddy_id(buddy);
  826. } else if (type == PURPLE_CONV_TYPE_CHAT) {
  827. /* Serialize the chat itself */
  828. channel_id = sapphire_id_from_conv(conv);
  829. json_object_set_string_member(data, "chat", channel_id);
  830. if (flags & PURPLE_MESSAGE_SYSTEM) {
  831. json_object_set_string_member(data, "who", "system");
  832. } else {
  833. /* And just the ID of who sent it. */
  834. gchar *user_id = sapphire_serialize_chat_user_id(conv, who);
  835. json_object_set_string_member(data, "who", user_id);
  836. g_free(user_id);
  837. /* ...in case the user is offline and not a buddy, also supply an alias */
  838. json_object_set_string_member(data, "alias", who);
  839. }
  840. } else {
  841. printf("Wat? nonbuddy, non chat?\n");
  842. }
  843. //json_object_set_int_member(data, "time", mtime);
  844. json_object_set_int_member(data, "flags", flags);
  845. /* Since we might be OTR-protected, the backend can't do anything with the plaintext */
  846. json_object_set_string_member(data, "content", message);
  847. /* So, if there are connected clients, we broadcast to them. Otherwise, we need to
  848. * store the message, so we can replay messages later for when we
  849. * connect. It's okay if the lookup fails and we null, g_list functions
  850. * don't mind. Additionally, if this is the first message like this,
  851. * we'll need to send a push notification. */
  852. if (sapphire_any_connected_clients()) {
  853. /* Broadcast */
  854. gchar *str = json_object_to_string(data);
  855. sapphire_broadcast_raw_packet(str);
  856. } else {
  857. /* Save the message */
  858. if (type == PURPLE_CONV_TYPE_IM) {
  859. GList *unacked_list = g_hash_table_lookup(id_to_unacked_list, channel_id);
  860. unacked_list = g_list_prepend(unacked_list, json_object_ref(data));
  861. g_hash_table_replace(id_to_unacked_list, channel_id, unacked_list);
  862. should_free_channel_id = FALSE;
  863. } else {
  864. /* TODO: Chats. At the moment, these can accumulate
  865. * huge amounts of memory, so disabling for now, mk? */
  866. }
  867. }
  868. /* Send a notification for IMs. The push notification module will
  869. * determine if it's necessary */
  870. if (type == PURPLE_CONV_TYPE_IM) {
  871. const char *alias = purple_buddy_get_alias(buddy);
  872. gchar *notification = g_strdup_printf("Psst, %s messaged you via Sapphire\n", alias);
  873. sapphire_push_notification(notification);
  874. g_free(notification);
  875. }
  876. if (should_free_channel_id)
  877. g_free(channel_id);
  878. json_object_unref(data);
  879. }
  880. static void
  881. sapphire_topic_changed(PurpleConversation *conv, const char *who, const char *topic, gpointer null)
  882. {
  883. JsonObject *data = json_object_new();
  884. gchar *chat_id = sapphire_id_from_conv(conv);
  885. json_object_set_string_member(data, "op", "topic");
  886. json_object_set_string_member(data, "who", who);
  887. json_object_set_string_member(data, "topic", topic);
  888. json_object_set_string_member(data, "chat", chat_id);
  889. sapphire_broadcast(data);
  890. json_object_unref(data);
  891. g_free(chat_id);
  892. }
  893. /* A buddy joined in a room we're subscribed to -- but that doesn't mean the
  894. * client needs to know. Only send the joined event to clients that have opened
  895. * the corresponding conversation */
  896. extern GList *authenticated_connections;
  897. static void
  898. sapphire_buddy_joined(PurpleConversation *conv, const char *who, PurpleConvChatBuddyFlags flags, gboolean new_arrival, gpointer null)
  899. {
  900. JsonObject *data = json_object_new();
  901. gchar *chat_id = sapphire_id_from_conv(conv);
  902. json_object_set_string_member(data, "op", "joined");
  903. json_object_set_string_member(data, "chat", chat_id);
  904. /* Send a single element worth of users :( */
  905. JsonArray *lst = json_array_new();
  906. JsonObject *buddy = json_object_new();
  907. printf("Got %s\n", who);
  908. sapphire_serialize_chat_buddy(buddy, conv, who, flags);
  909. json_array_add_object_element(lst, buddy);
  910. json_object_set_array_member(data, "members", lst);
  911. /* TODO: Maybe don't serialize so many times */
  912. /* TODO: Don't serialize at all if nobody's subscribed */
  913. for (GList *it = authenticated_connections; it != NULL; it = it->next) {
  914. Connection *conn = (Connection *) it->data;
  915. if (g_hash_table_contains(conn->subscribed_ids, chat_id))
  916. sapphire_send(conn, data);
  917. }
  918. g_free(chat_id);
  919. json_object_unref(data);
  920. }
  921. static void
  922. sapphire_joined_chat(PurpleConversation *conv, gpointer null)
  923. {
  924. /* Try to use the existing chat */
  925. gchar *id = sapphire_id_from_conv(conv);
  926. SapphireChat *schat = sapphire_find_chat(id, TRUE);
  927. if (!schat) {
  928. /* Surprise! Create a new chat */
  929. schat = sapphire_chat_from_conv(conv);
  930. schat->conv = conv;
  931. printf("Joining chat %s\n", schat->id);
  932. chats = g_slist_append(chats, schat);
  933. } else {
  934. /* Associate with the conv */
  935. schat->conv = conv;
  936. }
  937. g_hash_table_insert(blist_id_to_conversation, id, conv);
  938. }
  939. /* Certain prpls, particularly those for third-party protocols, should be
  940. * disabled when not in active use. This function, called from the socket
  941. * handling when a client connects or disconnects, checks if there are active
  942. * connections. If there are, relevant prpls are enabled; if not, they are
  943. * disabled. */
  944. static gboolean
  945. sapphire_prpl_defer_connects(const gchar *protocol_id)
  946. {
  947. return purple_strequal(protocol_id, "prpl-eionrobb-discord");
  948. }
  949. void
  950. sapphire_enable_accounts_by_connections(void)
  951. {
  952. gboolean should_enable = sapphire_any_connected_clients();
  953. for (GSList *it = purple_accounts; it != NULL; it = it->next) {
  954. PurpleAccount *account = (PurpleAccount *) it->data;
  955. const gchar *protocol_id = purple_account_get_protocol_id(account);
  956. /* Check if the protocol has this quirk */
  957. if (!sapphire_prpl_defer_connects(protocol_id))
  958. continue;
  959. /* It does -- so check which direction we need to go */
  960. gboolean enab = purple_account_get_enabled(account, UI_ID);
  961. if (should_enable != enab) {
  962. purple_account_set_enabled(account, UI_ID, should_enable);
  963. }
  964. }
  965. }
  966. #ifdef _WIN32
  967. #include <windows.h>
  968. extern BOOL SetDllDirectoryA(LPCSTR lpPathName);
  969. typedef void (WINAPI* LPFNSETDLLDIRECTORY)(LPCSTR);
  970. static LPFNSETDLLDIRECTORY MySetDllDirectory = NULL;
  971. #endif
  972. static void
  973. init_libpurple(void)
  974. {
  975. #ifdef _WIN32
  976. purple_util_set_user_dir("./.purple");
  977. HMODULE hmod;
  978. if ((hmod = GetModuleHandleW(L"kernel32.dll"))) {
  979. MySetDllDirectory = (LPFNSETDLLDIRECTORY) GetProcAddress(
  980. hmod, "SetDllDirectoryA");
  981. if (!MySetDllDirectory)
  982. printf("SetDllDirectory not supported\n");
  983. } else
  984. printf("Error getting kernel32.dll module handle\n");
  985. /* For Windows XP SP1+ / Server 2003 we use SetDllDirectory to avoid dll hell */
  986. if (MySetDllDirectory) {
  987. printf("Using SetDllDirectory\n");
  988. MySetDllDirectory("C:/Program Files (x86)/Pidgin/");
  989. }
  990. #endif
  991. gchar *search_path = g_build_filename(purple_user_dir(), "plugins", NULL);
  992. purple_plugins_add_search_path(search_path);
  993. g_free(search_path);
  994. #ifdef _WIN32
  995. purple_plugins_add_search_path("C:/Program Files (x86)/Pidgin/plugins/");
  996. purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/");
  997. purple_plugins_add_search_path("C:/Program\\ Files\\ \\(x86\\)/Pidgin/plugins/");
  998. #endif
  999. purple_debug_set_enabled(FALSE);
  1000. sapphire_set_eventloop();
  1001. if (!purple_core_init(UI_ID)) {
  1002. fprintf(stderr,
  1003. "libpurple initialization failed. Dumping core.\n"
  1004. "Please report this!\n");
  1005. abort();
  1006. }
  1007. purple_set_blist(purple_blist_new());
  1008. purple_blist_load();
  1009. purple_prefs_load();
  1010. purple_plugins_load_saved(PLUGIN_SAVE_PREF);
  1011. purple_pounces_load();
  1012. }
  1013. static void
  1014. sapphire_connect_signals(void)
  1015. {
  1016. static int handle;
  1017. purple_signal_connect(purple_accounts_get_handle(), "account-signed-on", &handle,
  1018. PURPLE_CALLBACK(sapphire_signed_on), NULL);
  1019. purple_signal_connect(purple_accounts_get_handle(), "account-enabled", &handle,
  1020. PURPLE_CALLBACK(sapphire_account_enabled), NULL);
  1021. purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", &handle,
  1022. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1023. purple_signal_connect(purple_blist_get_handle(), "buddy-signed-off", &handle,
  1024. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1025. purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", &handle,
  1026. PURPLE_CALLBACK(sapphire_buddy_status_changed), NULL);
  1027. purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", &handle,
  1028. PURPLE_CALLBACK(sapphire_received_message), NULL);
  1029. purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", &handle,
  1030. PURPLE_CALLBACK(sapphire_received_message), NULL);
  1031. purple_signal_connect(purple_conversations_get_handle(), "buddy-typing", &handle,
  1032. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1033. purple_signal_connect(purple_conversations_get_handle(), "buddy-typed", &handle,
  1034. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1035. purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped", &handle,
  1036. PURPLE_CALLBACK(sapphire_buddy_typing_changed), NULL);
  1037. purple_signal_connect(purple_conversations_get_handle(), "chat-joined", &handle,
  1038. PURPLE_CALLBACK(sapphire_joined_chat), NULL);
  1039. purple_signal_connect(purple_conversations_get_handle(), "chat-buddy-joined", &handle,
  1040. PURPLE_CALLBACK(sapphire_buddy_joined), NULL);
  1041. purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", &handle,
  1042. PURPLE_CALLBACK(sapphire_topic_changed), NULL);
  1043. }
  1044. int main(int argc, char *argv[])
  1045. {
  1046. GMainLoop *loop;
  1047. PurpleSavedStatus *status;
  1048. #ifndef _WIN32
  1049. /* libpurple's built-in DNS resolution forks processes to perform
  1050. * blocking lookups without blocking the main process. It does not
  1051. * handle SIGCHLD itself, so if the UI does not you quickly get an army
  1052. * of zombie subprocesses marching around.
  1053. */
  1054. signal(SIGCHLD, SIG_IGN);
  1055. #endif
  1056. #ifdef _WIN32
  1057. g_thread_init(NULL);
  1058. #endif
  1059. g_set_prgname("Sapphire");
  1060. g_set_application_name("Sapphire");
  1061. loop = g_main_loop_new(NULL, FALSE);
  1062. g_main_loop_ref(loop);
  1063. gboolean jailed = (argc >= 2) && (purple_strequal(argv[1], "--jailed"));
  1064. if (jailed) {
  1065. /* If we're running in firejail, we can't use a .purple, since
  1066. * the hidden nature will cause permission errors. Instead, use
  1067. * an opaque name */
  1068. purple_util_set_user_dir("./purple");
  1069. }
  1070. init_libpurple();
  1071. purple_prefs_add_none("/purple/sapphire");
  1072. if (!purple_prefs_get_string(SAPPHIRE_PUSH_EMAIL_PREF)) {
  1073. printf("Push notification email (blank to disable): ");
  1074. char email[128];
  1075. fgets(email, sizeof(email), stdin);
  1076. purple_prefs_add_string(SAPPHIRE_PUSH_EMAIL_PREF, email);
  1077. }
  1078. /* Initialize global hash tables */
  1079. id_to_buddy = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1080. id_to_unacked_list = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1081. id_to_chat = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1082. id_to_account = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1083. id_to_joined = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1084. blist_id_to_conversation = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
  1085. sent_icons = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1086. sapphire_connect_signals();
  1087. sapphire_init_websocket();
  1088. g_main_context_iteration(g_main_loop_get_context(loop), FALSE);
  1089. GList *l;
  1090. /* Fetch account and enable it */
  1091. for (l = purple_accounts_get_all(); l != NULL; l = l->next) {
  1092. PurpleAccount *candidate = (PurpleAccount *)l->data;
  1093. const gchar *protocol_id = purple_account_get_protocol_id(candidate);
  1094. if (purple_strequal(protocol_id, "prpl-jabber") || purple_strequal(protocol_id, "prpl-eionrobb-discord")) {
  1095. purple_accounts = g_slist_append(purple_accounts, candidate);
  1096. purple_account_set_enabled(candidate, UI_ID, !sapphire_prpl_defer_connects(protocol_id));
  1097. } else {
  1098. purple_account_set_enabled(candidate, UI_ID, FALSE);
  1099. }
  1100. }
  1101. if (!purple_accounts) {
  1102. fprintf(stderr, "No accounts found\n");
  1103. return 1;
  1104. }
  1105. /* Now, to connect the account(s), create a status and activate it. */
  1106. status = purple_savedstatus_new(NULL, PURPLE_STATUS_AVAILABLE);
  1107. purple_savedstatus_activate(status);
  1108. g_main_loop_run(loop);
  1109. return 0;
  1110. }