libgroupme.c 91 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177
  1. /*
  2. * GroupMe plugin for libpurple
  3. * Copyright (C) 2017-2018 Alyssa Rosenzweig
  4. * Copyright (C) 2016 Eion Robb
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. #include <stdio.h>
  20. #include <stdlib.h>
  21. #include <string.h>
  22. #ifdef __GNUC__
  23. #include <unistd.h>
  24. #endif
  25. #include <errno.h>
  26. #ifdef ENABLE_NLS
  27. # define GETTEXT_PACKAGE "purple-groupme"
  28. # include <glib/gi18n-lib.h>
  29. # ifdef _WIN32
  30. # ifdef LOCALEDIR
  31. # unset LOCALEDIR
  32. # endif
  33. # define LOCALEDIR wpurple_locale_dir()
  34. # endif
  35. #else
  36. # define _(a) (a)
  37. # define N_(a) (a)
  38. #endif
  39. #include "glib_compat.h"
  40. #include "json_compat.h"
  41. #include "purple_compat.h"
  42. #define GROUPME_PLUGIN_ID "prpl-alyssarosenzweig-groupme"
  43. #ifndef GROUPME_PLUGIN_VERSION
  44. #define GROUPME_PLUGIN_VERSION "0.1"
  45. #endif
  46. #define GROUPME_PLUGIN_WEBSITE "https://notabug.com/alyssa/groupme-purple"
  47. #define GROUPME_USERAGENT "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
  48. #define GROUPME_API_SERVER "api.groupme.com/v3"
  49. #define GROUPME_PUSH_SERVER "push.groupme.com/faye?"
  50. #define GROUPME_GATEWAY_SERVER "push.groupme.com"
  51. #define GROUPME_GATEWAY_PORT 443
  52. #define GROUPME_GATEWAY_SERVER_PATH "/faye"
  53. /* TODO: Websockets */
  54. #define USE_LONG_POLL
  55. #ifdef USE_LONG_POLL
  56. #define GROUPME_PUSH_TYPE "long-polling"
  57. #else
  58. #define GROUPME_PUSH_TYPE "websocket"
  59. #endif
  60. #define IGNORE_PRINTS
  61. typedef struct {
  62. guint64 id;
  63. gchar *name;
  64. gchar *icon;
  65. guint64 owner;
  66. GArray *members; /* list of member ids */
  67. GHashTable *nicknames; /* id->nick? */
  68. GHashTable *nicknames_rev; /* reverse */
  69. } GroupMeGuild;
  70. typedef struct {
  71. guint64 id;
  72. gchar *nick;
  73. gboolean is_op;
  74. } GroupMeGuildMembership;
  75. typedef struct {
  76. guint64 id;
  77. gchar *id_s;
  78. gchar *name;
  79. gchar *avatar;
  80. GHashTable *guild_memberships;
  81. gboolean bot;
  82. } GroupMeUser;
  83. typedef struct {
  84. PurpleAccount *account;
  85. PurpleConnection *pc;
  86. GHashTable *cookie_table;
  87. gchar *session_token;
  88. gchar *channel;
  89. guint64 self_user_id;
  90. gchar *self_username;
  91. guint64 last_message_id;
  92. gint64 last_load_last_message_id;
  93. gchar *token;
  94. gchar *session_id;
  95. gchar *mfa_ticket;
  96. PurpleSslConnection *websocket;
  97. gboolean websocket_header_received;
  98. gboolean sync_complete;
  99. guchar packet_code;
  100. gchar *frame;
  101. guint64 frame_len;
  102. guint64 frame_len_progress;
  103. gint64 seq; /* incrementing counter */
  104. guint heartbeat_timeout;
  105. GHashTable *one_to_ones; /* A store of known room_id's -> username's */
  106. GHashTable *one_to_ones_rev; /* A store of known usernames's -> room_id's */
  107. GHashTable *last_message_id_dm; /* A store of known room_id's -> last_message_id's */
  108. GHashTable *sent_message_ids; /* A store of message id's that we generated from this instance */
  109. GHashTable *result_callbacks; /* Result ID -> Callback function */
  110. GQueue *received_message_queue; /* A store of the last 10 received message id's for de-dup */
  111. GHashTable *new_users;
  112. GHashTable *new_guilds;
  113. GSList *http_conns; /**< PurpleHttpConnection to be cancelled on logout */
  114. gint frames_since_reconnect;
  115. GSList *pending_writes;
  116. gint roomlist_guild_count;
  117. gchar *client_id;
  118. int push_id;
  119. } GroupMeAccount;
  120. typedef struct {
  121. GroupMeAccount *account;
  122. GroupMeGuild *guild;
  123. } GroupMeAccountGuild;
  124. static guint64
  125. to_int(const gchar *id)
  126. {
  127. return id ? g_ascii_strtoull(id, NULL, 10) : 0;
  128. }
  129. static gchar *
  130. from_int(guint64 id)
  131. {
  132. return g_strdup_printf("%" G_GUINT64_FORMAT, id);
  133. }
  134. /** libpurple requires unique chat id's per conversation.
  135. we use a hash function to convert the 64bit conversation id
  136. into a platform-dependent chat id (worst case 32bit).
  137. previously we used g_int64_hash() from glib,
  138. however libpurple requires positive integers */
  139. static gint
  140. groupme_chat_hash(guint64 chat_id)
  141. {
  142. return ABS((gint) chat_id);
  143. }
  144. static void groupme_free_guild_membership(gpointer data);
  145. /* creating */
  146. static GroupMeUser *
  147. groupme_new_user(JsonObject *json)
  148. {
  149. GroupMeUser *user = g_new0(GroupMeUser, 1);
  150. user->id_s = json_object_get_string_member(json, "user_id");
  151. if (!user->id_s)
  152. user->id_s = json_object_get_string_member(json, "id");
  153. user->id = to_int(user->id_s);
  154. user->name = json_object_get_string_member(json, "nickname");
  155. if (!user->name)
  156. user->name = json_object_get_string_member(json, "name");
  157. user->avatar = g_strdup(json_object_get_string_member(json, "image_url"));
  158. user->name = g_strdup(user->name);
  159. user->id_s = g_strdup(user->id_s);
  160. user->guild_memberships = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, groupme_free_guild_membership);
  161. return user;
  162. }
  163. static GroupMeGuild *
  164. groupme_new_guild(JsonObject *json)
  165. {
  166. GroupMeGuild *guild = g_new0(GroupMeGuild, 1);
  167. guild->id = to_int(json_object_get_string_member(json, "id"));
  168. guild->name = g_strdup(json_object_get_string_member(json, "name"));
  169. guild->icon = g_strdup(json_object_get_string_member(json, "image_url"));
  170. guild->members = g_array_new(TRUE, TRUE, sizeof(guint64));
  171. guild->nicknames = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, g_free);
  172. guild->nicknames_rev = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  173. return guild;
  174. }
  175. static GroupMeGuildMembership *
  176. groupme_new_guild_membership(guint64 id, JsonObject *json)
  177. {
  178. GroupMeGuildMembership *guild_membership = g_new0(GroupMeGuildMembership, 1);
  179. guild_membership->id = id;
  180. /* Search for op roles */
  181. JsonArray *roles = json_object_get_array_member(json, "roles");
  182. gint i, len = json_array_get_length(roles);
  183. guild_membership->is_op = FALSE;
  184. for (i = len - 1; i >= 0; i--) {
  185. const gchar *role = json_array_get_string_element(roles, i);
  186. if ((g_strcmp0(role, "admin") == 0) || (g_strcmp0(role, "op") == 0)) {
  187. guild_membership->is_op = TRUE;
  188. break;
  189. }
  190. }
  191. return guild_membership;
  192. }
  193. /* freeing */
  194. static void
  195. groupme_free_guild_membership(gpointer data)
  196. {
  197. GroupMeGuildMembership *guild_membership = data;
  198. g_free(guild_membership->nick);
  199. g_free(guild_membership);
  200. }
  201. static void
  202. groupme_free_user(gpointer data)
  203. {
  204. GroupMeUser *user = data;
  205. g_free(user->name);
  206. g_free(user->avatar);
  207. g_hash_table_unref(user->guild_memberships);
  208. g_free(user);
  209. }
  210. static void
  211. groupme_free_guild(gpointer data)
  212. {
  213. GroupMeGuild *guild = data;
  214. g_free(guild->name);
  215. g_free(guild->icon);
  216. g_array_unref(guild->members);
  217. g_hash_table_unref(guild->nicknames);
  218. g_hash_table_unref(guild->nicknames_rev);
  219. g_free(guild);
  220. }
  221. static void groupme_start_socket(GroupMeAccount *ya);
  222. static void
  223. groupme_got_subscription(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  224. {
  225. /* We're good to go */
  226. groupme_start_socket(da);
  227. }
  228. typedef void (*GroupMeProxyCallbackFunc)(GroupMeAccount *ya, JsonNode *node, gpointer user_data);
  229. typedef struct {
  230. GroupMeAccount *ya;
  231. GroupMeProxyCallbackFunc callback;
  232. gpointer user_data;
  233. } GroupMeProxyConnection;
  234. static void groupme_fetch_url(GroupMeAccount *da, const gchar *url, const gchar *postdata, GroupMeProxyCallbackFunc callback, gpointer user_data);
  235. static void
  236. groupme_got_handshake(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  237. {
  238. if (node != NULL) {
  239. JsonArray *responseA = json_node_get_array(node);
  240. JsonObject *response = json_array_get_object_element(responseA, 0);
  241. if (json_object_has_member(response, "successful")) {
  242. const gchar *clientId = json_object_get_string_member(response, "clientId");
  243. da->client_id = g_strdup(clientId);
  244. /* Subscribe now */
  245. const gchar *str = g_strdup_printf(
  246. "{\"channel\": \"/meta/subscribe\", \"clientId\": \"%s\", \"subscription\": \"/user/%" G_GUINT64_FORMAT "\", \"ext\": {\"timestamp\": %" G_GUINT64_FORMAT ", \"access_token\": \"%s\"}, \"id\": 2}",
  247. clientId,
  248. da->self_user_id,
  249. time(NULL),
  250. da->token);
  251. da->push_id = 3;
  252. groupme_fetch_url(da, "https://" GROUPME_PUSH_SERVER, str, groupme_got_subscription, NULL);
  253. }
  254. }
  255. }
  256. static GroupMeUser *
  257. groupme_get_user_name(GroupMeAccount *da, int discriminator, gchar *name)
  258. {
  259. GHashTableIter iter;
  260. gpointer key, value;
  261. g_hash_table_iter_init(&iter, da->new_users);
  262. while (g_hash_table_iter_next(&iter, &key, &value)) {
  263. GroupMeUser *user = value;
  264. if (/*user->discriminator == discriminator && */purple_strequal(user->name, name)) {
  265. return value;
  266. }
  267. }
  268. return NULL;
  269. }
  270. static GroupMeUser *
  271. groupme_get_user_fullname(GroupMeAccount *da, const gchar *name)
  272. {
  273. g_return_val_if_fail(name && *name, NULL);
  274. gchar **split_name = g_strsplit(name, "#", 2);
  275. GroupMeUser *user = NULL;
  276. if (split_name != NULL) {
  277. if (split_name[0] && split_name[1]) {
  278. user = groupme_get_user_name(da, to_int(split_name[1]), split_name[0]);
  279. }
  280. g_strfreev(split_name);
  281. }
  282. return user;
  283. }
  284. static GroupMeUser *
  285. groupme_get_user(GroupMeAccount *da, guint64 id)
  286. {
  287. return g_hash_table_lookup_int64(da->new_users, id);
  288. }
  289. static GroupMeUser *
  290. groupme_upsert_user(GHashTable *user_table, JsonObject *json)
  291. {
  292. const gchar *suid = json_object_get_string_member(json, "user_id");
  293. if (!suid)
  294. suid = json_object_get_string_member(json, "id");
  295. guint64 *key = NULL, user_id = to_int(suid);
  296. GroupMeUser *user = NULL;
  297. if (g_hash_table_lookup_extended_int64(user_table, user_id, (gpointer) &key, (gpointer) &user)) {
  298. return user;
  299. } else {
  300. user = groupme_new_user(json);
  301. g_hash_table_replace_int64(user_table, user->id, user);
  302. return user;
  303. }
  304. }
  305. static gchar *
  306. groupme_alloc_nickname(GroupMeUser *user, GroupMeGuild *guild, const gchar *suggested_nick)
  307. {
  308. const gchar *base_nick = suggested_nick ? suggested_nick : user->name;
  309. gchar *nick = NULL;
  310. if (base_nick == NULL) {
  311. return NULL;
  312. }
  313. guint64 *existing = g_hash_table_lookup(guild->nicknames_rev, base_nick);
  314. if (existing && *existing != user->id) {
  315. /* Ambiguous; try with the real name */
  316. nick = g_strdup_printf("%s (%s)", base_nick, user->name);
  317. existing = g_hash_table_lookup(guild->nicknames_rev, nick);
  318. if (existing && *existing != user->id) {
  319. /* Ambiguous; use the UUID */
  320. g_free(nick);
  321. nick = g_strdup_printf("%s (%" G_GUINT64_FORMAT ")", base_nick, user->id);
  322. }
  323. }
  324. if (!nick) {
  325. nick = g_strdup(base_nick);
  326. }
  327. g_hash_table_replace_int64(guild->nicknames, user->id, g_strdup(nick));
  328. g_hash_table_replace(guild->nicknames_rev, g_strdup(nick), g_memdup(&user->id, sizeof(user->id)));
  329. return nick;
  330. }
  331. static GroupMeGuild *
  332. groupme_get_guild(GroupMeAccount *da, guint64 id)
  333. {
  334. return g_hash_table_lookup_int64(da->new_guilds, id);
  335. }
  336. static GroupMeGuild *
  337. groupme_upsert_guild(GHashTable *guild_table, JsonObject *json)
  338. {
  339. guint64 *key = NULL, guild_id = to_int(json_object_get_string_member(json, "id"));
  340. GroupMeGuild *guild = NULL;
  341. if (g_hash_table_lookup_extended_int64(guild_table, guild_id, (gpointer) &key, (gpointer) &guild)) {
  342. return guild;
  343. } else {
  344. guild = groupme_new_guild(json);
  345. g_hash_table_replace_int64(guild_table, guild->id, guild);
  346. return guild;
  347. }
  348. }
  349. PurpleChatUserFlags
  350. groupme_get_user_flags(GroupMeAccount *da, GroupMeGuild *guild, GroupMeUser *user)
  351. {
  352. if (user == NULL) {
  353. return PURPLE_CHAT_USER_NONE;
  354. }
  355. guint64 gid = guild->id;
  356. GroupMeGuildMembership *guild_membership = g_hash_table_lookup_int64(user->guild_memberships, gid);
  357. PurpleChatUserFlags best_flag = user->bot ? PURPLE_CHAT_USER_VOICE : PURPLE_CHAT_USER_NONE;
  358. if (guild_membership == NULL)
  359. return best_flag;
  360. if (guild_membership->is_op)
  361. return PURPLE_CHAT_USER_OP;
  362. return best_flag;
  363. }
  364. #if PURPLE_VERSION_CHECK(3, 0, 0)
  365. static void
  366. groupme_update_cookies(GroupMeAccount *ya, const GList *cookie_headers)
  367. {
  368. const gchar *cookie_start;
  369. const gchar *cookie_end;
  370. gchar *cookie_name;
  371. gchar *cookie_value;
  372. const GList *cur;
  373. for (cur = cookie_headers; cur != NULL; cur = g_list_next(cur)) {
  374. cookie_start = cur->data;
  375. cookie_end = strchr(cookie_start, '=');
  376. if (cookie_end != NULL) {
  377. cookie_name = g_strndup(cookie_start, cookie_end - cookie_start);
  378. cookie_start = cookie_end + 1;
  379. cookie_end = strchr(cookie_start, ';');
  380. if (cookie_end != NULL) {
  381. cookie_value = g_strndup(cookie_start, cookie_end - cookie_start);
  382. cookie_start = cookie_end;
  383. g_hash_table_replace(ya->cookie_table, cookie_name, cookie_value);
  384. }
  385. }
  386. }
  387. }
  388. #else
  389. static void
  390. groupme_update_cookies(GroupMeAccount *ya, const gchar *headers)
  391. {
  392. const gchar *cookie_start;
  393. const gchar *cookie_end;
  394. gchar *cookie_name;
  395. gchar *cookie_value;
  396. int header_len;
  397. g_return_if_fail(headers != NULL);
  398. header_len = strlen(headers);
  399. /* look for the next "Set-Cookie: " */
  400. /* grab the data up until ';' */
  401. cookie_start = headers;
  402. while ((cookie_start = strstr(cookie_start, "\r\nSet-Cookie: ")) && (cookie_start - headers) < header_len) {
  403. cookie_start += 14;
  404. cookie_end = strchr(cookie_start, '=');
  405. if (cookie_end != NULL) {
  406. cookie_name = g_strndup(cookie_start, cookie_end - cookie_start);
  407. cookie_start = cookie_end + 1;
  408. cookie_end = strchr(cookie_start, ';');
  409. if (cookie_end != NULL) {
  410. cookie_value = g_strndup(cookie_start, cookie_end - cookie_start);
  411. cookie_start = cookie_end;
  412. g_hash_table_replace(ya->cookie_table, cookie_name, cookie_value);
  413. }
  414. }
  415. }
  416. }
  417. #endif
  418. static void
  419. groupme_cookie_foreach_cb(gchar *cookie_name, gchar *cookie_value, GString *str)
  420. {
  421. g_string_append_printf(str, "%s=%s;", cookie_name, cookie_value);
  422. }
  423. static gchar *
  424. groupme_cookies_to_string(GroupMeAccount *ya)
  425. {
  426. GString *str;
  427. str = g_string_new(NULL);
  428. g_hash_table_foreach(ya->cookie_table, (GHFunc) groupme_cookie_foreach_cb, str);
  429. return g_string_free(str, FALSE);
  430. }
  431. static void
  432. groupme_response_callback(PurpleHttpConnection *http_conn,
  433. #if PURPLE_VERSION_CHECK(3, 0, 0)
  434. PurpleHttpResponse *response, gpointer user_data)
  435. {
  436. gsize len;
  437. const gchar *url_text = purple_http_response_get_data(response, &len);
  438. const gchar *error_message = purple_http_response_get_error(response);
  439. #else
  440. gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message)
  441. {
  442. #endif
  443. const gchar *body;
  444. gsize body_len;
  445. GroupMeProxyConnection *conn = user_data;
  446. JsonParser *parser = json_parser_new();
  447. conn->ya->http_conns = g_slist_remove(conn->ya->http_conns, http_conn);
  448. #if !PURPLE_VERSION_CHECK(3, 0, 0)
  449. groupme_update_cookies(conn->ya, url_text);
  450. body = g_strstr_len(url_text, len, "\r\n\r\n");
  451. body = body ? body + 4 : body;
  452. body_len = len - (body - url_text);
  453. #else
  454. groupme_update_cookies(conn->ya, purple_http_response_get_headers_by_name(response, "Set-Cookie"));
  455. body = url_text;
  456. body_len = len;
  457. #endif
  458. if (body == NULL && error_message != NULL) {
  459. /* connection error - unersolvable dns name, non existing server */
  460. gchar *error_msg_formatted = g_strdup_printf(_("Connection error: %s."), error_message);
  461. purple_connection_error(conn->ya->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, error_msg_formatted);
  462. g_free(error_msg_formatted);
  463. g_free(conn);
  464. return;
  465. }
  466. if (body != NULL && !json_parser_load_from_data(parser, body, body_len, NULL)) {
  467. if (conn->callback) {
  468. JsonNode *dummy_node = json_node_new(JSON_NODE_OBJECT);
  469. JsonObject *dummy_object = json_object_new();
  470. json_node_set_object(dummy_node, dummy_object);
  471. json_object_set_string_member(dummy_object, "body", body);
  472. json_object_set_int_member(dummy_object, "len", body_len);
  473. g_dataset_set_data(dummy_node, "raw_body", (gpointer) body);
  474. conn->callback(conn->ya, dummy_node, conn->user_data);
  475. g_dataset_destroy(dummy_node);
  476. json_node_free(dummy_node);
  477. json_object_unref(dummy_object);
  478. }
  479. } else {
  480. JsonNode *root = json_parser_get_root(parser);
  481. purple_debug_misc("groupme", "Got response: %s\n", body);
  482. if (conn->callback) {
  483. conn->callback(conn->ya, root, conn->user_data);
  484. }
  485. }
  486. g_object_unref(parser);
  487. g_free(conn);
  488. }
  489. static void
  490. groupme_fetch_url_with_method(GroupMeAccount *ya, const gchar *method, const gchar *_url, const gchar *postdata, GroupMeProxyCallbackFunc callback, gpointer user_data)
  491. {
  492. PurpleAccount *account;
  493. GroupMeProxyConnection *conn;
  494. gchar *cookies;
  495. PurpleHttpConnection *http_conn;
  496. account = ya->account;
  497. if (purple_account_is_disconnected(account)) {
  498. return;
  499. }
  500. conn = g_new0(GroupMeProxyConnection, 1);
  501. conn->ya = ya;
  502. conn->callback = callback;
  503. conn->user_data = user_data;
  504. cookies = groupme_cookies_to_string(ya);
  505. if (method == NULL) {
  506. method = "GET";
  507. }
  508. /* Attach token to requests */
  509. gchar *url = g_strdup(_url);
  510. if (ya->token) {
  511. g_free(url);
  512. url = g_strdup_printf("%s&token=%s", _url, ya->token);
  513. }
  514. purple_debug_info("groupme", "Fetching url %s\n", url);
  515. #if PURPLE_VERSION_CHECK(3, 0, 0)
  516. PurpleHttpRequest *request = purple_http_request_new(url);
  517. purple_http_request_set_method(request, method);
  518. purple_http_request_header_set(request, "Accept", "*/*");
  519. purple_http_request_header_set(request, "User-Agent", GROUPME_USERAGENT);
  520. purple_http_request_header_set(request, "Cookie", cookies);
  521. if (postdata) {
  522. if (strstr(url, "/login") && strstr(postdata, "password")) {
  523. purple_debug_info("groupme", "With postdata ###PASSWORD REMOVED###\n");
  524. } else {
  525. purple_debug_info("groupme", "With postdata %s\n", postdata);
  526. }
  527. if (postdata[0] == '{') {
  528. purple_http_request_header_set(request, "Content-Type", "application/json");
  529. } else {
  530. purple_http_request_header_set(request, "Content-Type", "application/x-www-form-urlencoded");
  531. }
  532. purple_http_request_set_contents(request, postdata, -1);
  533. }
  534. http_conn = purple_http_request(ya->pc, request, groupme_response_callback, conn);
  535. purple_http_request_unref(request);
  536. if (http_conn != NULL) {
  537. ya->http_conns = g_slist_prepend(ya->http_conns, http_conn);
  538. }
  539. #else
  540. GString *headers;
  541. gchar *host = NULL, *path = NULL, *user = NULL, *password = NULL;
  542. int port;
  543. purple_url_parse(url, &host, &port, &path, &user, &password);
  544. headers = g_string_new(NULL);
  545. /* Use the full 'url' until libpurple can handle path's longer than 256 chars */
  546. g_string_append_printf(headers, "%s /%s HTTP/1.0\r\n", method, path);
  547. g_string_append_printf(headers, "Connection: close\r\n");
  548. g_string_append_printf(headers, "Host: %s\r\n", host);
  549. g_string_append_printf(headers, "Accept: */*\r\n");
  550. g_string_append_printf(headers, "User-Agent: " GROUPME_USERAGENT "\r\n");
  551. g_string_append_printf(headers, "Cookie: %s\r\n", cookies);
  552. if (postdata) {
  553. if (strstr(url, "/login") && strstr(postdata, "password")) {
  554. purple_debug_info("groupme", "With postdata ###PASSWORD REMOVED###\n");
  555. } else {
  556. purple_debug_info("groupme", "With postdata %s\n", postdata);
  557. }
  558. if (postdata[0] == '{') {
  559. g_string_append(headers, "Content-Type: application/json\r\n");
  560. } else {
  561. g_string_append(headers, "Content-Type: application/x-www-form-urlencoded\r\n");
  562. }
  563. g_string_append_printf(headers, "Content-Length: %" G_GSIZE_FORMAT "\r\n", strlen(postdata));
  564. g_string_append(headers, "\r\n");
  565. g_string_append(headers, postdata);
  566. } else {
  567. g_string_append(headers, "\r\n");
  568. }
  569. g_free(host);
  570. g_free(path);
  571. g_free(user);
  572. g_free(password);
  573. http_conn = purple_util_fetch_url_request_len_with_account(ya->account, url, FALSE, GROUPME_USERAGENT, TRUE, headers->str, TRUE, 6553500, groupme_response_callback, conn);
  574. if (http_conn != NULL) {
  575. ya->http_conns = g_slist_prepend(ya->http_conns, http_conn);
  576. }
  577. g_string_free(headers, TRUE);
  578. #endif
  579. g_free(cookies);
  580. g_free(url);
  581. }
  582. static void
  583. groupme_fetch_url(GroupMeAccount *da, const gchar *url, const gchar *postdata, GroupMeProxyCallbackFunc callback, gpointer user_data)
  584. {
  585. groupme_fetch_url_with_method(da, (postdata ? "POST" : "GET"), url, postdata, callback, user_data);
  586. }
  587. static void groupme_socket_write_json(GroupMeAccount *ya, JsonObject *data);
  588. static GHashTable *groupme_chat_info_defaults(PurpleConnection *pc, const char *chatname);
  589. static void groupme_mark_room_messages_read(GroupMeAccount *ya, guint64 room_id);
  590. static void groupme_init_push(GroupMeAccount *da);
  591. static guint64
  592. groupme_process_message(GroupMeAccount *da, int channel, JsonObject *data, gboolean is_dm);
  593. static void
  594. groupme_got_push(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  595. {
  596. JsonArray *subscriptions = json_node_get_array(node);
  597. guint len = json_array_get_length(subscriptions);
  598. for (int i = len - 1; i >= 0; i--) {
  599. JsonObject *sub = json_array_get_object_element(subscriptions, i);
  600. /* Actuate the new response */
  601. if (!json_object_has_member(sub, "data"))
  602. continue;
  603. JsonObject *data = json_object_get_object_member(sub, "data");
  604. if (!json_object_has_member(data, "subject"))
  605. continue;
  606. JsonObject *subj = json_object_get_object_member(data, "subject");
  607. const gchar *type = json_object_get_string_member(data, "type");
  608. if (g_strcmp0(type, "line.create") == 0) {
  609. /* Incoming message */
  610. int gid = to_int(json_object_get_string_member(subj, "group_id"));
  611. groupme_process_message(da, gid, subj, FALSE);
  612. } else if (g_strcmp0(type, "direct_message.create") == 0) {
  613. /* Direct message: either our sent message or theirs */
  614. int sid = to_int(json_object_get_string_member(subj, "sender_id"));
  615. int rid = to_int(json_object_get_string_member(subj, "recipient_id"));
  616. /* Sometimes we receive our own messages, account for that */
  617. int channel = sid == da->self_user_id ? rid : sid;
  618. groupme_process_message(da, channel, subj, TRUE);
  619. } else {
  620. printf("Unknown type %s, check debug logs\n", type);
  621. }
  622. }
  623. #ifdef USE_LONG_POLL
  624. /* Long polling consists of repeated reqeusts to the push server */
  625. groupme_init_push(da);
  626. #endif
  627. }
  628. static void
  629. groupme_init_push(GroupMeAccount *da)
  630. {
  631. JsonObject *data = json_object_new();
  632. json_object_set_string_member(data, "channel", "/meta/connect");
  633. json_object_set_string_member(data, "clientId", da->client_id);
  634. json_object_set_string_member(data, "connectionType", GROUPME_PUSH_TYPE);
  635. gchar *id = from_int(da->push_id++);
  636. json_object_set_string_member(data, "id", id);
  637. groupme_fetch_url(da, "https://" GROUPME_PUSH_SERVER, json_object_to_string(data), groupme_got_push, NULL);
  638. //groupme_socket_write_json(da, data);
  639. json_object_unref(data);
  640. g_free(id);
  641. }
  642. void groupme_handle_add_new_user(GroupMeAccount *ya, JsonObject *obj);
  643. PurpleGroup *groupme_get_or_create_group(const gchar *name);
  644. static void groupme_got_history_static(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  645. static void groupme_got_history_of_room(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  646. static void groupme_populate_guild(GroupMeAccount *da, JsonObject *guild);
  647. static void groupme_got_guilds(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  648. static void groupme_got_chats(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  649. static void groupme_got_avatar(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  650. static void groupme_get_avatar(GroupMeAccount *da, GroupMeUser *user);
  651. static const gchar *groupme_normalise_room_name(const gchar *guild_name, const gchar *name);
  652. static GroupMeGuild *groupme_open_chat(GroupMeAccount *da, guint64 id, gchar *name, gboolean present);
  653. static void
  654. groupme_create_associate(GroupMeAccount *da, guint64 id)
  655. {
  656. gchar *id_s = from_int(id);
  657. /* First, check to see if we already are associated */
  658. PurpleBuddy *buddy = purple_find_buddy(da->account, id_s);
  659. /* If not, associate */
  660. if (!buddy) {
  661. GroupMeUser *user = groupme_get_user(da, id);
  662. if (!user) {
  663. printf("Not associating with unknown user %d\n", id);
  664. return;
  665. }
  666. buddy = purple_buddy_new(da->account, id_s, user->name);
  667. purple_blist_add_buddy(buddy, NULL, groupme_get_or_create_group("GroupMe"), NULL);
  668. /* Bring it up */
  669. purple_protocol_got_user_status(da->account, id_s, "online", NULL);
  670. groupme_get_avatar(da, user);
  671. }
  672. }
  673. static gchar *
  674. groupme_get_real_name(PurpleConnection *pc, gint id, const char *who)
  675. {
  676. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  677. PurpleChatConversation *chatconv;
  678. chatconv = purple_conversations_find_chat(pc, id);
  679. guint64 *room_id_ptr = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
  680. if (!room_id_ptr) {
  681. goto bail;
  682. }
  683. guint64 room_id = *room_id_ptr;
  684. GroupMeGuild *channel = groupme_get_guild(da, room_id);
  685. if (!channel)
  686. goto bail;
  687. guint64 *uid = g_hash_table_lookup(channel->nicknames_rev, who);
  688. if (uid) {
  689. groupme_create_associate(da, *uid);
  690. return from_int(*uid);
  691. }
  692. /* Probably a fullname already, bail out */
  693. bail:
  694. return g_strdup(who);
  695. }
  696. static guint64
  697. groupme_process_message(GroupMeAccount *da, int channel, JsonObject *data, gboolean is_dm)
  698. {
  699. const gchar *guid = json_object_get_string_member(data, "source_guid");
  700. guint64 author_id = to_int(json_object_get_string_member(data, "sender_id"));
  701. const gchar *content = json_object_get_string_member(data, "text");
  702. const gchar *timestamp_str = json_object_get_string_member(data, "created_at");
  703. time_t timestamp = purple_str_to_time(timestamp_str, FALSE, NULL, NULL, NULL);
  704. JsonArray *attachments = json_object_get_array_member(data, "attachments");
  705. PurpleMessageFlags flags;
  706. gchar *tmp;
  707. gint i;
  708. /* Drop our own messages that were pinged back to us */
  709. if ((author_id == da->self_user_id) && g_hash_table_remove(da->sent_message_ids, guid))
  710. return;
  711. if (author_id == da->self_user_id && is_dm) {
  712. flags = PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED;
  713. } else {
  714. flags = PURPLE_MESSAGE_RECV;
  715. }
  716. if (is_dm) {
  717. /* private message */
  718. if (author_id == da->self_user_id) {
  719. PurpleConversation *conv;
  720. PurpleIMConversation *imconv;
  721. PurpleMessage *msg;
  722. gchar *username = groupme_get_user(da, channel)->id_s;
  723. imconv = purple_conversations_find_im_with_account(username, da->account);
  724. if (imconv == NULL) {
  725. imconv = purple_im_conversation_new(da->account, username);
  726. }
  727. conv = PURPLE_CONVERSATION(imconv);
  728. if (content && *content) {
  729. msg = purple_message_new_outgoing(username, content, flags);
  730. purple_message_set_time(msg, timestamp);
  731. purple_conversation_write_message(conv, msg);
  732. purple_message_destroy(msg);
  733. }
  734. if (attachments) {
  735. for (i = json_array_get_length(attachments) - 1; i >= 0; i--) {
  736. JsonObject *attachment = json_array_get_object_element(attachments, i);
  737. const gchar *url = json_object_get_string_member(attachment, "url");
  738. msg = purple_message_new_outgoing(username, url, flags);
  739. purple_message_set_time(msg, timestamp);
  740. purple_conversation_write_message(conv, msg);
  741. purple_message_destroy(msg);
  742. }
  743. }
  744. } else {
  745. GroupMeUser *author = groupme_upsert_user(da->new_users, data);
  746. gchar *merged_username = author->id_s;
  747. if (content && *content) {
  748. purple_serv_got_im(da->pc, merged_username, content, flags, timestamp);
  749. }
  750. if (attachments) {
  751. for (i = json_array_get_length(attachments) - 1; i >= 0; i--) {
  752. JsonObject *attachment = json_array_get_object_element(attachments, i);
  753. const gchar *url = json_object_get_string_member(attachment, "url");
  754. purple_serv_got_im(da->pc, merged_username, url, flags, timestamp);
  755. }
  756. }
  757. }
  758. } else {
  759. /* Open the buffer if it's not already */
  760. groupme_open_chat(da, channel, NULL, FALSE);
  761. gchar *name = json_object_get_string_member(data, "name");
  762. if (content && *content) {
  763. purple_serv_got_chat_in(da->pc, groupme_chat_hash(channel), name, flags, content, timestamp);
  764. }
  765. if (attachments) {
  766. for (i = json_array_get_length(attachments) - 1; i >= 0; i--) {
  767. JsonObject *attachment = json_array_get_object_element(attachments, i);
  768. if (json_object_has_member(attachment, "url")) {
  769. const gchar *url = json_object_get_string_member(attachment, "url");
  770. purple_serv_got_chat_in(da->pc, groupme_chat_hash(channel), name, flags, url, timestamp);
  771. }
  772. }
  773. }
  774. }
  775. return 1;
  776. }
  777. struct groupme_group_typing_data {
  778. GroupMeAccount *da;
  779. guint64 channel_id;
  780. gchar *username;
  781. gboolean set;
  782. gboolean free_me;
  783. };
  784. static gboolean
  785. groupme_set_group_typing(void *_u)
  786. {
  787. if (_u == NULL) {
  788. return FALSE;
  789. }
  790. struct groupme_group_typing_data *ctx = _u;
  791. PurpleChatConversation *chatconv = purple_conversations_find_chat(ctx->da->pc, groupme_chat_hash(ctx->channel_id));
  792. if (chatconv == NULL) {
  793. goto release_ctx;
  794. }
  795. PurpleChatUser *cb = purple_chat_conversation_find_user(chatconv, ctx->username);
  796. if (!cb) {
  797. goto release_ctx;
  798. }
  799. PurpleChatUserFlags cbflags;
  800. cbflags = purple_chat_user_get_flags(cb);
  801. if (ctx->set) {
  802. cbflags |= PURPLE_CHAT_USER_TYPING;
  803. } else {
  804. cbflags &= ~PURPLE_CHAT_USER_TYPING;
  805. }
  806. purple_chat_user_set_flags(cb, cbflags);
  807. release_ctx:
  808. if (ctx->free_me) {
  809. g_free(ctx->username);
  810. g_free(ctx);
  811. }
  812. return FALSE;
  813. }
  814. static void
  815. groupme_got_nick_change(GroupMeAccount *da, GroupMeUser *user, GroupMeGuild *guild, const gchar *new, const gchar *old, gboolean self)
  816. {
  817. gchar *old_safe = g_strdup(old);
  818. if (old) {
  819. g_hash_table_remove(guild->nicknames_rev, old);
  820. }
  821. /* Nick change */
  822. gchar *nick = groupme_alloc_nickname(user, guild, new);
  823. /* Propagate through the guild, see e.g. irc_msg_nick */
  824. GHashTableIter channel_iter;
  825. gpointer key, value;
  826. /* TODO: Nick */
  827. PurpleChatConversation *chat = purple_conversations_find_chat(da->pc, groupme_chat_hash(guild->id));
  828. if (chat && purple_chat_conversation_has_user(chat, old_safe)) {
  829. purple_chat_conversation_rename_user(chat, old_safe, nick);
  830. }
  831. g_free(nick);
  832. }
  833. PurpleChat *
  834. groupme_bring_up_buddies(PurpleAccount *account)
  835. {
  836. PurpleBlistNode *node;
  837. GSList *lst = purple_find_buddies(account, NULL);
  838. while (lst) {
  839. PurpleBuddy *buddy = (PurpleBuddy *) lst->data;
  840. purple_protocol_got_user_status(account, buddy->name, "online", NULL);
  841. purple_protocol_got_user_idle(account, buddy->name, 0, 0);
  842. lst = g_slist_delete_link(lst, lst);
  843. }
  844. return NULL;
  845. }
  846. PurpleChat *
  847. groupme_find_chat_from_node(PurpleAccount *account, const char *id, PurpleBlistNode *root)
  848. {
  849. PurpleBlistNode *node;
  850. for (node = root;
  851. node != NULL;
  852. node = purple_blist_node_next(node, TRUE)) {
  853. if (PURPLE_IS_CHAT(node)) {
  854. PurpleChat *chat = PURPLE_CHAT(node);
  855. if (purple_chat_get_account(chat) != account) {
  856. continue;
  857. }
  858. GHashTable *components = purple_chat_get_components(chat);
  859. const gchar *chat_id = g_hash_table_lookup(components, "id");
  860. if (purple_strequal(chat_id, id)) {
  861. return chat;
  862. }
  863. }
  864. }
  865. return NULL;
  866. }
  867. PurpleChat *
  868. groupme_find_chat(PurpleAccount *account, const char *id)
  869. {
  870. return groupme_find_chat_from_node(account, id, purple_blist_get_root());
  871. }
  872. PurpleChat *
  873. groupme_find_chat_in_group(PurpleAccount *account, const char *id, PurpleGroup *group)
  874. {
  875. g_return_val_if_fail(group != NULL, NULL);
  876. return groupme_find_chat_from_node(account, id, PURPLE_BLIST_NODE(group));
  877. }
  878. static void
  879. groupme_add_channel_to_blist(GroupMeAccount *da, GroupMeGuild *channel, PurpleGroup *group)
  880. {
  881. GHashTable *components = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  882. gchar *id = from_int(channel->id);
  883. g_hash_table_replace(components, g_strdup("id"), id);
  884. g_hash_table_replace(components, g_strdup("name"), g_strdup(channel->name));
  885. /* Don't re-add the channel to the same group */
  886. if (groupme_find_chat_in_group(da->account, id, group) == NULL) {
  887. PurpleChat *chat = purple_chat_new(da->account, channel->name, components);
  888. purple_blist_add_chat(chat, group, NULL);
  889. } else {
  890. g_hash_table_unref(components);
  891. }
  892. }
  893. PurpleGroup *
  894. groupme_get_or_create_group(const gchar *name)
  895. {
  896. PurpleGroup *groupme_group = purple_blist_find_group(name);
  897. if (!groupme_group) {
  898. groupme_group = purple_group_new(name);
  899. purple_blist_add_group(groupme_group, NULL);
  900. }
  901. return groupme_group;
  902. }
  903. static const gchar *
  904. groupme_normalise_room_name(const gchar *guild_name, const gchar *name)
  905. {
  906. gchar *channel_name = g_strconcat(guild_name, "#", name, NULL);
  907. static gchar *old_name = NULL;
  908. g_free(old_name);
  909. old_name = g_ascii_strdown(channel_name, -1);
  910. purple_util_chrreplace(old_name, ' ', '_');
  911. g_free(channel_name);
  912. return old_name;
  913. }
  914. static gchar *
  915. groupme_roomlist_serialize(PurpleRoomlistRoom *room)
  916. {
  917. GList *fields = purple_roomlist_room_get_fields(room);
  918. const gchar *id = (const gchar *) fields->data;
  919. return g_strdup(id);
  920. }
  921. PurpleRoomlist *
  922. groupme_roomlist_get_list(PurpleConnection *pc)
  923. {
  924. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  925. PurpleRoomlist *roomlist;
  926. GList *fields = NULL;
  927. PurpleRoomlistField *f;
  928. roomlist = purple_roomlist_new(da->account);
  929. f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("ID"), "id", TRUE);
  930. fields = g_list_append(fields, f);
  931. purple_roomlist_set_fields(roomlist, fields);
  932. purple_roomlist_set_in_progress(roomlist, TRUE);
  933. GHashTableIter iter;
  934. gpointer key, guild;
  935. g_hash_table_iter_init(&iter, da->new_guilds);
  936. while (g_hash_table_iter_next(&iter, &key, &guild)) {
  937. GroupMeGuild *g = (GroupMeGuild *) guild;
  938. PurpleRoomlistRoom *room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, g->name, NULL);
  939. gchar *channel_id = from_int(g->id);
  940. purple_roomlist_room_add_field(roomlist, room, channel_id);
  941. purple_roomlist_room_add(roomlist, room);
  942. g_free(channel_id);
  943. }
  944. purple_roomlist_set_in_progress(roomlist, FALSE);
  945. return roomlist;
  946. }
  947. static void
  948. groupme_restart_channel(GroupMeAccount *da)
  949. {
  950. purple_connection_set_state(da->pc, PURPLE_CONNECTION_CONNECTING);
  951. groupme_start_socket(da);
  952. }
  953. static guint groupme_conv_send_typing(PurpleConversation *conv, PurpleIMTypingState state, GroupMeAccount *ya);
  954. static gulong chat_conversation_typing_signal = 0;
  955. static void groupme_mark_conv_seen(PurpleConversation *conv, PurpleConversationUpdateType type);
  956. static gulong conversation_updated_signal = 0;
  957. typedef struct {
  958. GroupMeAccount *da;
  959. GroupMeUser *user;
  960. } GroupMeUserInviteResponseStore;
  961. static void
  962. groupme_populate_guild(GroupMeAccount *da, JsonObject *guild)
  963. {
  964. GroupMeGuild *g = groupme_upsert_guild(da->new_guilds, guild);
  965. gchar *name = json_object_get_string_member(guild, "name");
  966. /* Add chat to blist */
  967. PurpleGroup *group = groupme_get_or_create_group("GroupMe Chats");
  968. groupme_add_channel_to_blist(da, g, group);
  969. JsonArray *members = json_object_get_array_member(guild, "members");
  970. /* Populate members */
  971. for (int j = json_array_get_length(members) - 1; j >= 0; j--) {
  972. JsonObject *member = json_array_get_object_element(members, j);
  973. GroupMeUser *u = groupme_upsert_user(da->new_users, member);
  974. g_array_append_val(g->members, u->id);
  975. GroupMeGuildMembership *membership = groupme_new_guild_membership(g->id, member);
  976. g_hash_table_replace_int64(u->guild_memberships, g->id, membership);
  977. membership->nick = groupme_alloc_nickname(u, g, json_object_get_string_member(member, "nickname"));
  978. }
  979. }
  980. static void
  981. groupme_got_guilds(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  982. {
  983. JsonObject *container = json_node_get_object(node);
  984. JsonArray *guilds = json_object_get_array_member(container, "response");
  985. guint len = json_array_get_length(guilds);
  986. for (int i = len - 1; i >= 0; i--) {
  987. JsonObject *guild = json_array_get_object_element(guilds, i);
  988. groupme_populate_guild(da, guild);
  989. }
  990. }
  991. static void
  992. groupme_got_chats(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  993. {
  994. JsonObject *container = json_node_get_object(node);
  995. JsonArray *chats = json_object_get_array_member(container, "response");
  996. guint len = json_array_get_length(chats);
  997. for (int i = len - 1; i >= 0; i--) {
  998. JsonObject *chat = json_array_get_object_element(chats, i);
  999. JsonObject *msg = json_object_get_object_member(chat, "last_message");
  1000. JsonObject *other = json_object_get_object_member(chat, "other_user");
  1001. const gchar *chan = json_object_get_string_member(other, "id");
  1002. /* TODO: Actually fetch history, rolling, save counts, etc */
  1003. groupme_upsert_user(da->new_users, other);
  1004. groupme_process_message(da, to_int(chan), msg, TRUE);
  1005. }
  1006. }
  1007. static void
  1008. groupme_got_self(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  1009. {
  1010. JsonObject *container = json_node_get_object(node);
  1011. JsonObject *resp = json_object_get_object_member(container, "response");
  1012. da->self_user_id = to_int(json_object_get_string_member(resp, "id"));
  1013. da->self_username = g_strdup(json_object_get_string_member(resp, "name"));
  1014. /* Now that we have the user ID, we can start the websocket handshake */
  1015. {
  1016. const gchar *str = "{\"channel\": \"/meta/handshake\", \"version\": \"1.0\", \"supportedConnectionTypes\": [\"" GROUPME_PUSH_TYPE "\"], \"id\": 1}";
  1017. groupme_fetch_url(da, "https://" GROUPME_PUSH_SERVER, str, groupme_got_handshake, NULL);
  1018. }
  1019. }
  1020. static void groupme_login_response(GroupMeAccount *da, JsonNode *node, gpointer user_data);
  1021. static void
  1022. groupme_mfa_text_entry(gpointer user_data, const gchar *code)
  1023. {
  1024. GroupMeAccount *da = user_data;
  1025. JsonObject *data = json_object_new();
  1026. gchar *str;
  1027. json_object_set_string_member(data, "code", code);
  1028. json_object_set_string_member(data, "ticket", da->mfa_ticket);
  1029. str = json_object_to_string(data);
  1030. groupme_fetch_url(da, "https://" GROUPME_API_SERVER "/api/v6/auth/mfa/totp", str, groupme_login_response, NULL);
  1031. g_free(str);
  1032. json_object_unref(data);
  1033. g_free(da->mfa_ticket);
  1034. da->mfa_ticket = NULL;
  1035. }
  1036. static void
  1037. groupme_mfa_cancel(gpointer user_data)
  1038. {
  1039. GroupMeAccount *da = user_data;
  1040. purple_connection_error(da->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("Cancelled 2FA auth"));
  1041. }
  1042. static void
  1043. groupme_login_response(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  1044. {
  1045. if (node != NULL) {
  1046. JsonObject *response = json_node_get_object(node);
  1047. da->token = g_strdup(json_object_get_string_member(response, "token"));
  1048. purple_account_set_string(da->account, "token", da->token);
  1049. if (da->token) {
  1050. groupme_start_socket(da);
  1051. return;
  1052. }
  1053. if (json_object_get_boolean_member(response, "mfa")) {
  1054. g_free(da->mfa_ticket);
  1055. da->mfa_ticket = g_strdup(json_object_get_string_member(response, "ticket"));
  1056. purple_request_input(da->pc, _("Two-factor authentication"),
  1057. _("Enter GroupMe auth code"),
  1058. _("You can get this token from your two-factor authentication mobile app."),
  1059. NULL, FALSE, FALSE, "",
  1060. _("_Login"), G_CALLBACK(groupme_mfa_text_entry),
  1061. _("_Cancel"), G_CALLBACK(groupme_mfa_cancel),
  1062. purple_request_cpar_from_connection(da->pc),
  1063. da);
  1064. return;
  1065. }
  1066. if (json_object_has_member(response, "email")) {
  1067. /* Probably an error about new location */
  1068. purple_connection_error(da->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, json_object_get_string_member(response, "email"));
  1069. return;
  1070. }
  1071. if (json_object_has_member(response, "password")) {
  1072. /* Probably an error about bad password */
  1073. purple_connection_error(da->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, json_object_get_string_member(response, "password"));
  1074. return;
  1075. }
  1076. }
  1077. purple_connection_error(da->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("Bad username/password"));
  1078. }
  1079. void
  1080. groupme_login(PurpleAccount *account)
  1081. {
  1082. GroupMeAccount *da;
  1083. PurpleConnection *pc = purple_account_get_connection(account);
  1084. PurpleConnectionFlags pc_flags;
  1085. pc_flags = purple_connection_get_flags(pc);
  1086. pc_flags |= PURPLE_CONNECTION_FLAG_NO_FONTSIZE;
  1087. pc_flags |= PURPLE_CONNECTION_FLAG_NO_BGCOLOR;
  1088. pc_flags |= PURPLE_CONNECTION_FLAG_NO_IMAGES;
  1089. purple_connection_set_flags(pc, pc_flags);
  1090. da = g_new0(GroupMeAccount, 1);
  1091. purple_connection_set_protocol_data(pc, da);
  1092. da->account = account;
  1093. da->pc = pc;
  1094. da->cookie_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1095. da->last_load_last_message_id = purple_account_get_int(account, "last_message_id_high", 0);
  1096. if (da->last_load_last_message_id != 0) {
  1097. da->last_load_last_message_id = (da->last_load_last_message_id << 32) | ((guint64) purple_account_get_int(account, "last_message_id_low", 0) & 0xFFFFFFFF);
  1098. }
  1099. da->one_to_ones = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1100. da->one_to_ones_rev = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1101. da->last_message_id_dm = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1102. da->sent_message_ids = g_hash_table_new_full(g_str_insensitive_hash, g_str_insensitive_equal, g_free, NULL);
  1103. da->result_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
  1104. da->received_message_queue = g_queue_new();
  1105. /* TODO make these the roots of all groupme data */
  1106. da->new_users = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, groupme_free_user);
  1107. da->new_guilds = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, groupme_free_guild);
  1108. purple_connection_set_state(pc, PURPLE_CONNECTION_CONNECTING);
  1109. const gchar *dev_token = purple_connection_get_password(da->pc);
  1110. da->token = g_strdup(dev_token);
  1111. /* Test the REST API */
  1112. groupme_fetch_url(da, "https://" GROUPME_API_SERVER "/users/me?", NULL, groupme_got_self, NULL);
  1113. groupme_fetch_url(da, "https://" GROUPME_API_SERVER "/groups?", NULL, groupme_got_guilds, NULL);
  1114. groupme_fetch_url(da, "https://" GROUPME_API_SERVER "/chats?", NULL, groupme_got_chats, NULL);
  1115. /* XXX: Authenticate good */
  1116. purple_connection_set_state(da->pc, PURPLE_CONNECTION_CONNECTED);
  1117. groupme_bring_up_buddies(da->account);
  1118. if (!chat_conversation_typing_signal) {
  1119. chat_conversation_typing_signal = purple_signal_connect(purple_conversations_get_handle(), "chat-conversation-typing", purple_connection_get_protocol(pc), PURPLE_CALLBACK(groupme_conv_send_typing), NULL);
  1120. }
  1121. if (!conversation_updated_signal) {
  1122. conversation_updated_signal = purple_signal_connect(purple_conversations_get_handle(), "conversation-updated", purple_connection_get_protocol(pc), PURPLE_CALLBACK(groupme_mark_conv_seen), NULL);
  1123. }
  1124. }
  1125. static void
  1126. groupme_close(PurpleConnection *pc)
  1127. {
  1128. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  1129. g_return_if_fail(da != NULL);
  1130. if (da->heartbeat_timeout) {
  1131. g_source_remove(da->heartbeat_timeout);
  1132. }
  1133. if (da->websocket != NULL) {
  1134. purple_ssl_close(da->websocket);
  1135. da->websocket = NULL;
  1136. }
  1137. g_hash_table_unref(da->one_to_ones);
  1138. da->one_to_ones = NULL;
  1139. g_hash_table_unref(da->one_to_ones_rev);
  1140. da->one_to_ones_rev = NULL;
  1141. g_hash_table_unref(da->last_message_id_dm);
  1142. da->last_message_id_dm = NULL;
  1143. g_hash_table_unref(da->sent_message_ids);
  1144. da->sent_message_ids = NULL;
  1145. g_hash_table_unref(da->result_callbacks);
  1146. da->result_callbacks = NULL;
  1147. g_hash_table_unref(da->new_users);
  1148. da->new_users = NULL;
  1149. g_hash_table_unref(da->new_guilds);
  1150. da->new_guilds = NULL;
  1151. g_queue_free(da->received_message_queue);
  1152. da->received_message_queue = NULL;
  1153. while (da->http_conns) {
  1154. #if !PURPLE_VERSION_CHECK(3, 0, 0)
  1155. purple_util_fetch_url_cancel(da->http_conns->data);
  1156. #else
  1157. purple_http_conn_cancel(da->http_conns->data);
  1158. #endif
  1159. da->http_conns = g_slist_delete_link(da->http_conns, da->http_conns);
  1160. }
  1161. while (da->pending_writes) {
  1162. json_object_unref(da->pending_writes->data);
  1163. da->pending_writes = g_slist_delete_link(da->pending_writes, da->pending_writes);
  1164. }
  1165. g_hash_table_destroy(da->cookie_table);
  1166. da->cookie_table = NULL;
  1167. g_free(da->frame);
  1168. da->frame = NULL;
  1169. g_free(da->token);
  1170. da->token = NULL;
  1171. g_free(da->session_id);
  1172. da->session_id = NULL;
  1173. g_free(da->self_username);
  1174. da->self_username = NULL;
  1175. g_free(da);
  1176. }
  1177. /* static void groupme_start_polling(GroupMeAccount *ya); */
  1178. static gboolean
  1179. groupme_process_frame(GroupMeAccount *da, const gchar *frame)
  1180. {
  1181. JsonParser *parser = json_parser_new();
  1182. JsonNode *root;
  1183. gint64 opcode;
  1184. printf("process frame!\n");
  1185. purple_debug_info("groupme", "got frame data: %s\n", frame);
  1186. if (!json_parser_load_from_data(parser, frame, -1, NULL)) {
  1187. purple_debug_error("groupme", "Error parsing response: %s\n", frame);
  1188. return TRUE;
  1189. }
  1190. root = json_parser_get_root(parser);
  1191. if (root != NULL) {
  1192. JsonObject *obj = json_node_get_object(root);
  1193. printf("TODO: Wire up websocket\n");
  1194. }
  1195. g_object_unref(parser);
  1196. return TRUE;
  1197. }
  1198. static guchar *
  1199. groupme_websocket_mask(guchar key[4], const guchar *pload, guint64 psize)
  1200. {
  1201. guint64 i;
  1202. guchar *ret = g_new0(guchar, psize);
  1203. for (i = 0; i < psize; i++) {
  1204. ret[i] = pload[i] ^ key[i % 4];
  1205. }
  1206. return ret;
  1207. }
  1208. static void
  1209. groupme_socket_write_data(GroupMeAccount *ya, guchar *data, gsize data_len, guchar type)
  1210. {
  1211. guchar *full_data;
  1212. guint len_size = 1;
  1213. guchar mkey[4] = { 0x12, 0x34, 0x56, 0x78 };
  1214. if (data_len) {
  1215. purple_debug_info("groupme", "sending frame: %*s\n", (int) data_len, data);
  1216. }
  1217. data = groupme_websocket_mask(mkey, data, data_len);
  1218. if (data_len > 125) {
  1219. if (data_len <= G_MAXUINT16) {
  1220. len_size += 2;
  1221. } else {
  1222. len_size += 8;
  1223. }
  1224. }
  1225. full_data = g_new0(guchar, 1 + data_len + len_size + 4);
  1226. if (type == 0) {
  1227. type = 129;
  1228. }
  1229. full_data[0] = type;
  1230. if (data_len <= 125) {
  1231. full_data[1] = data_len | 0x80;
  1232. } else if (data_len <= G_MAXUINT16) {
  1233. guint16 be_len = GUINT16_TO_BE(data_len);
  1234. full_data[1] = 126 | 0x80;
  1235. memmove(full_data + 2, &be_len, 2);
  1236. } else {
  1237. guint64 be_len = GUINT64_TO_BE(data_len);
  1238. full_data[1] = 127 | 0x80;
  1239. memmove(full_data + 2, &be_len, 8);
  1240. }
  1241. memmove(full_data + (1 + len_size), &mkey, 4);
  1242. memmove(full_data + (1 + len_size + 4), data, data_len);
  1243. purple_ssl_write(ya->websocket, full_data, 1 + data_len + len_size + 4);
  1244. g_free(full_data);
  1245. g_free(data);
  1246. }
  1247. /* takes ownership of data parameter */
  1248. static void
  1249. groupme_socket_write_json(GroupMeAccount *rca, JsonObject *data)
  1250. {
  1251. JsonNode *node;
  1252. gchar *str;
  1253. gsize len;
  1254. JsonGenerator *generator;
  1255. if (rca->websocket == NULL) {
  1256. if (data != NULL) {
  1257. rca->pending_writes = g_slist_append(rca->pending_writes, data);
  1258. }
  1259. return;
  1260. }
  1261. node = json_node_new(JSON_NODE_OBJECT);
  1262. json_node_set_object(node, data);
  1263. generator = json_generator_new();
  1264. json_generator_set_root(generator, node);
  1265. str = json_generator_to_data(generator, &len);
  1266. g_object_unref(generator);
  1267. json_node_free(node);
  1268. groupme_socket_write_data(rca, (guchar *) str, len, 0);
  1269. g_free(str);
  1270. }
  1271. static void
  1272. groupme_socket_got_data(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond)
  1273. {
  1274. GroupMeAccount *ya = userdata;
  1275. guchar length_code;
  1276. int read_len = 0;
  1277. gboolean done_some_reads = FALSE;
  1278. printf("ws got data\n");
  1279. if (G_UNLIKELY(!ya->websocket_header_received)) {
  1280. gint nlbr_count = 0;
  1281. gchar nextchar;
  1282. while (nlbr_count < 4 && (read_len = purple_ssl_read(conn, &nextchar, 1)) == 1) {
  1283. if (nextchar == '\r' || nextchar == '\n') {
  1284. nlbr_count++;
  1285. } else {
  1286. nlbr_count = 0;
  1287. }
  1288. }
  1289. if (nlbr_count == 4) {
  1290. ya->websocket_header_received = TRUE;
  1291. done_some_reads = TRUE;
  1292. /* flush stuff that we attempted to send before the websocket was ready */
  1293. while (ya->pending_writes) {
  1294. groupme_socket_write_json(ya, ya->pending_writes->data);
  1295. ya->pending_writes = g_slist_delete_link(ya->pending_writes, ya->pending_writes);
  1296. }
  1297. }
  1298. }
  1299. while (ya->frame || (read_len = purple_ssl_read(conn, &ya->packet_code, 1)) == 1) {
  1300. if (!ya->frame) {
  1301. if (ya->packet_code != 129) {
  1302. if (ya->packet_code == 136) {
  1303. purple_debug_error("groupme", "websocket closed\n");
  1304. length_code = 0;
  1305. purple_ssl_read(conn, &length_code, 1);
  1306. if (length_code > 0 && length_code <= 125) {
  1307. guchar error_buf[2];
  1308. if (purple_ssl_read(conn, &error_buf, 2) == 2) {
  1309. gint error_code = (error_buf[0] << 8) + error_buf[1];
  1310. purple_debug_error("groupme", "error code %d\n", error_code);
  1311. if (error_code == 4004) {
  1312. /* bad auth token, clear and reset */
  1313. purple_account_set_string(ya->account, "token", NULL);
  1314. purple_connection_error(ya->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Reauthentication required"));
  1315. return;
  1316. }
  1317. }
  1318. }
  1319. /* Try reconnect */
  1320. groupme_start_socket(ya);
  1321. return;
  1322. } else if (ya->packet_code == 137) {
  1323. /* Ping */
  1324. gint ping_frame_len = 0;
  1325. length_code = 0;
  1326. purple_ssl_read(conn, &length_code, 1);
  1327. if (length_code <= 125) {
  1328. ping_frame_len = length_code;
  1329. } else if (length_code == 126) {
  1330. guchar len_buf[2];
  1331. purple_ssl_read(conn, len_buf, 2);
  1332. ping_frame_len = (len_buf[0] << 8) + len_buf[1];
  1333. } else if (length_code == 127) {
  1334. purple_ssl_read(conn, &ping_frame_len, 8);
  1335. ping_frame_len = GUINT64_FROM_BE(ping_frame_len);
  1336. }
  1337. if (ping_frame_len) {
  1338. guchar *pong_data = g_new0(guchar, ping_frame_len);
  1339. purple_ssl_read(conn, pong_data, ping_frame_len);
  1340. groupme_socket_write_data(ya, pong_data, ping_frame_len, 138);
  1341. g_free(pong_data);
  1342. } else {
  1343. groupme_socket_write_data(ya, (guchar *) "", 0, 138);
  1344. }
  1345. return;
  1346. } else if (ya->packet_code == 138) {
  1347. /* Ignore pong */
  1348. return;
  1349. }
  1350. purple_debug_error("groupme", "unknown websocket error %d\n", ya->packet_code);
  1351. return;
  1352. }
  1353. length_code = 0;
  1354. purple_ssl_read(conn, &length_code, 1);
  1355. if (length_code <= 125) {
  1356. ya->frame_len = length_code;
  1357. } else if (length_code == 126) {
  1358. guchar len_buf[2];
  1359. purple_ssl_read(conn, len_buf, 2);
  1360. ya->frame_len = (len_buf[0] << 8) + len_buf[1];
  1361. } else if (length_code == 127) {
  1362. purple_ssl_read(conn, &ya->frame_len, 8);
  1363. ya->frame_len = GUINT64_FROM_BE(ya->frame_len);
  1364. }
  1365. ya->frame = g_new0(gchar, ya->frame_len + 1);
  1366. ya->frame_len_progress = 0;
  1367. }
  1368. do {
  1369. read_len = purple_ssl_read(conn, ya->frame + ya->frame_len_progress, ya->frame_len - ya->frame_len_progress);
  1370. if (read_len > 0) {
  1371. ya->frame_len_progress += read_len;
  1372. }
  1373. } while (read_len > 0 && ya->frame_len_progress < ya->frame_len);
  1374. done_some_reads = TRUE;
  1375. if (ya->frame_len_progress == ya->frame_len) {
  1376. gboolean success = groupme_process_frame(ya, ya->frame);
  1377. g_free(ya->frame);
  1378. ya->frame = NULL;
  1379. ya->packet_code = 0;
  1380. ya->frame_len = 0;
  1381. ya->frames_since_reconnect++;
  1382. if (G_UNLIKELY(ya->websocket == NULL || success == FALSE)) {
  1383. return;
  1384. }
  1385. } else {
  1386. return;
  1387. }
  1388. }
  1389. if (done_some_reads == FALSE && read_len <= 0) {
  1390. if (read_len < 0 && errno == EAGAIN) {
  1391. return;
  1392. }
  1393. purple_debug_error("groupme", "got errno %d, read_len %d from websocket thread\n", errno, read_len);
  1394. if (ya->frames_since_reconnect < 2) {
  1395. purple_connection_error(ya->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Lost connection to server"));
  1396. } else {
  1397. /* Try reconnect */
  1398. groupme_start_socket(ya);
  1399. }
  1400. }
  1401. }
  1402. static void
  1403. groupme_socket_connected(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond)
  1404. {
  1405. GroupMeAccount *da = userdata;
  1406. gchar *websocket_header;
  1407. const gchar *websocket_key = "15XF+ptKDhYVERXoGcdHTA=="; /* TODO don't be lazy */
  1408. purple_ssl_input_add(da->websocket, groupme_socket_got_data, da);
  1409. websocket_header = g_strdup_printf("GET %s HTTP/1.1\r\n"
  1410. "Host: %s\r\n"
  1411. "Connection: Upgrade\r\n"
  1412. "Pragma: no-cache\r\n"
  1413. "Cache-Control: no-cache\r\n"
  1414. "Upgrade: websocket\r\n"
  1415. "Sec-WebSocket-Version: 13\r\n"
  1416. "Sec-WebSocket-Key: %s\r\n"
  1417. "User-Agent: " GROUPME_USERAGENT "\r\n"
  1418. "\r\n",
  1419. GROUPME_GATEWAY_SERVER_PATH, GROUPME_GATEWAY_SERVER,
  1420. websocket_key);
  1421. purple_ssl_write(da->websocket, websocket_header, strlen(websocket_header));
  1422. g_free(websocket_header);
  1423. groupme_init_push(da);
  1424. }
  1425. static void
  1426. groupme_socket_failed(PurpleSslConnection *conn, PurpleSslErrorType errortype, gpointer userdata)
  1427. {
  1428. GroupMeAccount *da = userdata;
  1429. da->websocket = NULL;
  1430. da->websocket_header_received = FALSE;
  1431. if (da->frames_since_reconnect < 1) {
  1432. purple_connection_error(da->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Couldn't connect to gateway"));
  1433. } else {
  1434. groupme_restart_channel(da);
  1435. }
  1436. }
  1437. static void
  1438. groupme_start_socket(GroupMeAccount *da)
  1439. {
  1440. #ifdef USE_LONG_POLL
  1441. groupme_init_push(da);
  1442. return;
  1443. #endif
  1444. if (da->heartbeat_timeout) {
  1445. g_source_remove(da->heartbeat_timeout);
  1446. }
  1447. /* Reset all the old stuff */
  1448. if (da->websocket != NULL) {
  1449. purple_ssl_close(da->websocket);
  1450. }
  1451. da->websocket = NULL;
  1452. da->websocket_header_received = FALSE;
  1453. g_free(da->frame);
  1454. da->frame = NULL;
  1455. da->packet_code = 0;
  1456. da->frame_len = 0;
  1457. da->frames_since_reconnect = 0;
  1458. da->websocket = purple_ssl_connect(da->account, GROUPME_GATEWAY_SERVER, GROUPME_GATEWAY_PORT, groupme_socket_connected, groupme_socket_failed, da);
  1459. }
  1460. static void
  1461. groupme_chat_leave_by_room_id(PurpleConnection *pc, guint64 room_id)
  1462. {
  1463. /*GroupMeAccount *ya = purple_connection_get_protocol_data(pc);
  1464. JsonObject *data = json_object_new();
  1465. JsonArray *params = json_array_new();
  1466. json_array_add_string_element(params, room_id);
  1467. json_object_set_string_member(data, "msg", "method");
  1468. json_object_set_string_member(data, "method", "leaveRoom");
  1469. json_object_set_array_member(data, "params", params);
  1470. json_object_set_string_member(data, "id", groupme_get_next_id_str(ya));
  1471. groupme_socket_write_json(ya, data);*/
  1472. }
  1473. static void
  1474. groupme_chat_leave(PurpleConnection *pc, int id)
  1475. {
  1476. PurpleChatConversation *chatconv;
  1477. /* TODO check source */
  1478. chatconv = purple_conversations_find_chat(pc, id);
  1479. guint64 room_id = *(guint64 *) purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
  1480. if (!room_id) {
  1481. /* TODO FIXME? */
  1482. room_id = to_int(purple_conversation_get_name(PURPLE_CONVERSATION(chatconv)));
  1483. }
  1484. groupme_chat_leave_by_room_id(pc, room_id);
  1485. }
  1486. /* Invite to a _group DM_
  1487. * The API for inviting to a guild is different, TODO implement that one too */
  1488. static void
  1489. groupme_chat_invite(PurpleConnection *pc, int id, const char *message, const char *who)
  1490. {
  1491. GroupMeAccount *ya;
  1492. guint64 room_id;
  1493. PurpleChatConversation *chatconv;
  1494. GroupMeUser *user;
  1495. JsonObject *data;
  1496. ya = purple_connection_get_protocol_data(pc);
  1497. chatconv = purple_conversations_find_chat(pc, id);
  1498. guint64 *room_id_ptr = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
  1499. if(!room_id_ptr) {
  1500. return;
  1501. }
  1502. room_id = *room_id_ptr;
  1503. user = groupme_get_user_fullname(ya, who);
  1504. if (!user) {
  1505. purple_debug_info("groupme", "Missing user in invitation for %s", who);
  1506. return;
  1507. }
  1508. data = json_object_new();
  1509. json_object_set_string_member(data, "recipient", from_int(user->id));
  1510. gchar *postdata = json_object_to_string(data);
  1511. gchar *url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/channels/%" G_GUINT64_FORMAT "/recipients/%" G_GUINT64_FORMAT, room_id, user->id);
  1512. groupme_fetch_url_with_method(ya, "PUT", url, postdata, NULL, NULL);
  1513. g_free(url);
  1514. g_free(postdata);
  1515. json_object_unref(data);
  1516. }
  1517. static const gchar *
  1518. groupme_resolve_nick(GroupMeAccount *da, guint64 id, guint64 channel)
  1519. {
  1520. GroupMeGuild *g = groupme_get_guild(da, channel);
  1521. const gchar *nick = g_hash_table_lookup_int64(g->nicknames, id);
  1522. if (nick)
  1523. return nick;
  1524. GroupMeUser *u = groupme_get_user(da, id);
  1525. return u->name;
  1526. }
  1527. static void
  1528. groupme_chat_nick(PurpleConnection *pc, int id, gchar *new_nick)
  1529. {
  1530. PurpleChatConversation *chatconv;
  1531. /* TODO check source */
  1532. chatconv = purple_conversations_find_chat(pc, id);
  1533. guint64 room_id = *(guint64 *) purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
  1534. if (!room_id) {
  1535. /* TODO FIXME? */
  1536. room_id = to_int(purple_conversation_get_name(PURPLE_CONVERSATION(chatconv)));
  1537. }
  1538. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  1539. GroupMeGuild *guild = groupme_get_guild(da, room_id);
  1540. JsonObject *container = json_object_new();
  1541. JsonObject *data = json_object_new();
  1542. json_object_set_string_member(data, "nickname", new_nick);
  1543. json_object_set_object_member(container, "membership", data);
  1544. gchar *postdata = json_object_to_string(container);
  1545. gchar *url = g_strdup_printf("https://" GROUPME_API_SERVER "/groups/%" G_GUINT64_FORMAT "/memberships/update?", guild->id);
  1546. groupme_fetch_url(da, url, postdata, NULL, NULL);
  1547. g_free(url);
  1548. g_free(postdata);
  1549. json_object_unref(container);
  1550. /* Propragate locally as well */
  1551. const gchar *old_nick = g_hash_table_lookup_int64(guild->nicknames, da->self_user_id);
  1552. groupme_got_nick_change(da, groupme_get_user(da, da->self_user_id), guild, new_nick, old_nick, TRUE);
  1553. }
  1554. static GList *
  1555. groupme_chat_info(PurpleConnection *pc)
  1556. {
  1557. GList *m = NULL;
  1558. PurpleProtocolChatEntry *pce;
  1559. pce = g_new0(PurpleProtocolChatEntry, 1);
  1560. pce->label = _("ID");
  1561. pce->identifier = "id";
  1562. m = g_list_append(m, pce);
  1563. pce = g_new0(PurpleProtocolChatEntry, 1);
  1564. pce->label = _("Name");
  1565. pce->identifier = "name";
  1566. m = g_list_append(m, pce);
  1567. return m;
  1568. }
  1569. static gboolean
  1570. str_is_number(const gchar *str)
  1571. {
  1572. gint i = strlen(str) - 1;
  1573. for (; i >= 0; i--) {
  1574. if (!g_ascii_isdigit(str[i])) {
  1575. return FALSE;
  1576. }
  1577. }
  1578. return TRUE;
  1579. }
  1580. static __attribute__((optimize("O0"))) GHashTable *
  1581. groupme_chat_info_defaults(PurpleConnection *pc, const char *chatname)
  1582. {
  1583. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  1584. GHashTable *defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
  1585. if (chatname != NULL) {
  1586. if (str_is_number(chatname)) {
  1587. printf("Bad case 1\n");
  1588. //GroupMeChannel *channel = groupme_get_channel_global(da, chatname);
  1589. //if (channel != NULL) {
  1590. //g_hash_table_insert(defaults, "name", g_strdup(channel->name));
  1591. //}
  1592. g_hash_table_insert(defaults, "id", g_strdup(chatname));
  1593. } else {
  1594. printf("XXXX bad\n");
  1595. /*GroupMeChannel *channel = groupme_get_channel_global_name(da, chatname);
  1596. if (channel != NULL) {
  1597. g_hash_table_insert(defaults, "name", g_strdup(channel->name));
  1598. g_hash_table_insert(defaults, "id", from_int(channel->id));
  1599. }*/
  1600. }
  1601. }
  1602. return defaults;
  1603. }
  1604. static gchar *
  1605. groupme_get_chat_name(GHashTable *data)
  1606. {
  1607. gchar *temp;
  1608. if (data == NULL) {
  1609. return NULL;
  1610. }
  1611. temp = g_hash_table_lookup(data, "name");
  1612. if (temp == NULL) {
  1613. temp = g_hash_table_lookup(data, "id");
  1614. }
  1615. if (temp == NULL) {
  1616. return NULL;
  1617. }
  1618. return g_strdup(temp);
  1619. }
  1620. static void groupme_set_room_last_id(GroupMeAccount *da, guint64 channel_id, guint64 last_id);
  1621. static void
  1622. groupme_got_history_of_room(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  1623. {
  1624. JsonObject *container = json_node_get_object(node);
  1625. JsonObject *resp = json_object_get_object_member(container, "response");
  1626. JsonArray *messages = json_object_get_array_member(resp, "messages");
  1627. GroupMeGuild *channel = user_data;
  1628. gint i, len = json_array_get_length(messages);
  1629. guint64 last_message = /* channel->last_message_id */ 0 /* XXX */;
  1630. guint64 rolling_last_message_id = 0;
  1631. /* latest are first */
  1632. for (i = len - 1; i >= 0; i--) {
  1633. JsonObject *message = json_array_get_object_element(messages, i);
  1634. #if 0
  1635. guint64 id = to_int(json_object_get_string_member(message, "id"));
  1636. if (id >= last_message) {
  1637. break;
  1638. }
  1639. #endif
  1640. rolling_last_message_id = groupme_process_message(da, channel->id, message, FALSE);
  1641. }
  1642. if (rolling_last_message_id != 0) {
  1643. /* ACK HISTORY ACK */
  1644. #if 0
  1645. groupme_set_room_last_id(da, channel->id, rolling_last_message_id);
  1646. if (rolling_last_message_id < last_message) {
  1647. /* Request the next 100 messages */
  1648. gchar *url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/channels/%" G_GUINT64_FORMAT "/messages?limit=100&after=%" G_GUINT64_FORMAT, channel->id, rolling_last_message_id);
  1649. groupme_fetch_url(da, url, NULL, groupme_got_history_of_room, channel);
  1650. g_free(url);
  1651. }
  1652. #endif
  1653. }
  1654. }
  1655. /* identical endpoint as above, but not rolling */
  1656. static void
  1657. groupme_got_history_static(GroupMeAccount *da, JsonNode *node, gpointer user_data)
  1658. {
  1659. JsonArray *messages = json_node_get_array(node);
  1660. gint i, len = json_array_get_length(messages);
  1661. for (i = len - 1; i >= 0; i--) {
  1662. JsonObject *message = json_array_get_object_element(messages, i);
  1663. //groupme_process_message(da, message, FALSE);
  1664. }
  1665. }
  1666. /* libpurple can't store a 64bit int on a 32bit machine, so convert to
  1667. * something more usable instead (puke). also needs to work cross platform, in
  1668. * case the accounts.xml is being shared (double puke)
  1669. */
  1670. static guint64
  1671. groupme_get_room_last_id(GroupMeAccount *da, guint64 id)
  1672. {
  1673. guint64 last_message_id = da->last_load_last_message_id;
  1674. PurpleBlistNode *blistnode = NULL;
  1675. gchar *channel_id = from_int(id);
  1676. if (g_hash_table_contains(da->one_to_ones, channel_id)) {
  1677. /* is a direct message */
  1678. blistnode = PURPLE_BLIST_NODE(purple_blist_find_buddy(da->account, g_hash_table_lookup(da->one_to_ones, channel_id)));
  1679. } else {
  1680. /* twas a group chat */
  1681. blistnode = PURPLE_BLIST_NODE(purple_blist_find_chat(da->account, channel_id));
  1682. }
  1683. if (blistnode != NULL) {
  1684. guint64 last_room_id = purple_blist_node_get_int(blistnode, "last_message_id_high");
  1685. if (last_room_id != 0) {
  1686. last_room_id = (last_room_id << 32) | ((guint64) purple_blist_node_get_int(blistnode, "last_message_id_low") & 0xFFFFFFFF);
  1687. last_message_id = MAX(da->last_message_id, last_room_id);
  1688. }
  1689. }
  1690. g_free(channel_id);
  1691. return last_message_id;
  1692. }
  1693. static void
  1694. groupme_set_room_last_id(GroupMeAccount *da, guint64 id, guint64 last_id)
  1695. {
  1696. PurpleBlistNode *blistnode = NULL;
  1697. gchar *channel_id = from_int(id);
  1698. if (g_hash_table_contains(da->one_to_ones, channel_id)) {
  1699. /* is a direct message */
  1700. blistnode = PURPLE_BLIST_NODE(purple_blist_find_buddy(da->account, g_hash_table_lookup(da->one_to_ones, channel_id)));
  1701. } else {
  1702. /* twas a group chat */
  1703. blistnode = PURPLE_BLIST_NODE(purple_blist_find_chat(da->account, channel_id));
  1704. }
  1705. if (blistnode != NULL) {
  1706. purple_blist_node_set_int(blistnode, "last_message_id_high", last_id >> 32);
  1707. purple_blist_node_set_int(blistnode, "last_message_id_low", last_id & 0xFFFFFFFF);
  1708. }
  1709. da->last_message_id = MAX(da->last_message_id, last_id);
  1710. purple_account_set_int(da->account, "last_message_id_high", last_id >> 32);
  1711. purple_account_set_int(da->account, "last_message_id_low", last_id & 0xFFFFFFFF);
  1712. g_free(channel_id);
  1713. }
  1714. static void groupme_join_chat(PurpleConnection *pc, GHashTable *chatdata);
  1715. static GroupMeGuild *
  1716. groupme_open_chat(GroupMeAccount *da, guint64 id, gchar *name, gboolean present)
  1717. {
  1718. PurpleChatConversation *chatconv = NULL;
  1719. GroupMeGuild *channel = groupme_get_guild(da, id);
  1720. if (channel == NULL) {
  1721. return NULL;
  1722. }
  1723. if (name == NULL) {
  1724. name = channel->name;
  1725. }
  1726. gchar *id_str = from_int(id);
  1727. chatconv = purple_conversations_find_chat_with_account(id_str, da->account);
  1728. if (chatconv != NULL && !purple_chat_conversation_has_left(chatconv)) {
  1729. g_free(id_str);
  1730. if (present) {
  1731. purple_conversation_present(PURPLE_CONVERSATION(chatconv));
  1732. }
  1733. return NULL;
  1734. }
  1735. chatconv = purple_serv_got_joined_chat(da->pc, groupme_chat_hash(id), id_str);
  1736. g_free(id_str);
  1737. purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "id", g_memdup(&(id), sizeof(guint64)));
  1738. purple_conversation_present(PURPLE_CONVERSATION(chatconv));
  1739. /* Adds members */
  1740. GList *users = NULL, *flags = NULL;
  1741. for (int j = channel->members->len - 1; j >= 0; j--) {
  1742. int uid = g_array_index(channel->members, guint64, j);
  1743. GroupMeUser *u = groupme_get_user(da, uid);
  1744. PurpleChatUserFlags cbflags = groupme_get_user_flags(da, channel, u);
  1745. users = g_list_prepend(users, g_strdup(groupme_resolve_nick(da, uid, channel->id)));
  1746. flags = g_list_prepend(flags, GINT_TO_POINTER(cbflags));
  1747. }
  1748. purple_chat_conversation_clear_users(chatconv);
  1749. purple_chat_conversation_add_users(chatconv, users, NULL, flags, FALSE);
  1750. while (users != NULL) {
  1751. g_free(users->data);
  1752. users = g_list_delete_link(users, users);
  1753. }
  1754. g_list_free(users);
  1755. g_list_free(flags);
  1756. return channel;
  1757. }
  1758. static void
  1759. groupme_join_chat(PurpleConnection *pc, GHashTable *chatdata)
  1760. {
  1761. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  1762. guint64 id = to_int(g_hash_table_lookup(chatdata, "id"));
  1763. gchar *name = (gchar *) g_hash_table_lookup(chatdata, "name");
  1764. GroupMeGuild *channel = groupme_open_chat(da, id, name, TRUE);
  1765. if (!channel) {
  1766. return;
  1767. }
  1768. /* TODO: HISTORY */
  1769. #if 0
  1770. /* Get any missing messages */
  1771. guint64 last_message_id = groupme_get_room_last_id(da, id);
  1772. if (last_message_id != 0 && channel->last_message_id > last_message_id) {
  1773. gchar *url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/channels/%" G_GUINT64_FORMAT "/messages?limit=100&after=%" G_GUINT64_FORMAT, id, last_message_id);
  1774. groupme_fetch_url(da, url, NULL, groupme_got_history_of_room, channel);
  1775. g_free(url);
  1776. }
  1777. #endif
  1778. gchar *url = g_strdup_printf("https://" GROUPME_API_SERVER "/groups/%" G_GUINT64_FORMAT "/messages?limit=100", id);
  1779. groupme_fetch_url(da, url, NULL, groupme_got_history_of_room, channel);
  1780. }
  1781. static void
  1782. groupme_mark_room_messages_read(GroupMeAccount *da, guint64 channel_id)
  1783. {
  1784. if (!channel_id) {
  1785. return;
  1786. }
  1787. printf("XXX: TODO mark room messages\n");
  1788. #if 0
  1789. GroupMeChannel *channel = groupme_get_channel_global_int(da, channel_id);
  1790. guint64 last_message_id;
  1791. if (channel) {
  1792. last_message_id = channel->last_message_id;
  1793. } else {
  1794. gchar *channel = from_int(channel_id);
  1795. gchar *msg = g_hash_table_lookup(da->last_message_id_dm, channel);
  1796. g_free(channel);
  1797. if (msg) {
  1798. last_message_id = to_int(msg);
  1799. } else {
  1800. purple_debug_info("groupme", "Unknown acked channel %" G_GUINT64_FORMAT, channel_id);
  1801. return;
  1802. }
  1803. }
  1804. if (last_message_id == 0) {
  1805. purple_debug_info("groupme", "Won't ack message ID == 0");
  1806. }
  1807. guint64 known_message_id = groupme_get_room_last_id(da, channel_id);
  1808. if (last_message_id == known_message_id) {
  1809. /* Up to date */
  1810. return;
  1811. }
  1812. groupme_set_room_last_id(da, channel_id, last_message_id);
  1813. gchar *url;
  1814. url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/channels/%" G_GUINT64_FORMAT "/messages/%" G_GUINT64_FORMAT "/ack", channel_id, last_message_id);
  1815. groupme_fetch_url(da, url, "{\"token\":null}", NULL, NULL);
  1816. g_free(url);
  1817. #endif
  1818. }
  1819. static void
  1820. groupme_mark_conv_seen(PurpleConversation *conv, PurpleConversationUpdateType type)
  1821. {
  1822. PurpleConnection *pc;
  1823. GroupMeAccount *ya;
  1824. if (type != PURPLE_CONVERSATION_UPDATE_UNSEEN) {
  1825. return;
  1826. }
  1827. pc = purple_conversation_get_connection(conv);
  1828. if (!PURPLE_CONNECTION_IS_CONNECTED(pc)) {
  1829. return;
  1830. }
  1831. if (!purple_strequal(purple_protocol_get_id(purple_connection_get_protocol(pc)), GROUPME_PLUGIN_ID)) {
  1832. return;
  1833. }
  1834. ya = purple_connection_get_protocol_data(pc);
  1835. guint64 *room_id_ptr = purple_conversation_get_data(conv, "id");
  1836. guint64 room_id = 0;
  1837. if (room_id_ptr) {
  1838. room_id = *room_id_ptr;
  1839. } else {
  1840. room_id = to_int(g_hash_table_lookup(ya->one_to_ones_rev, purple_conversation_get_name(conv)));
  1841. }
  1842. groupme_mark_room_messages_read(ya, room_id);
  1843. }
  1844. static guint
  1845. groupme_conv_send_typing(PurpleConversation *conv, PurpleIMTypingState state, GroupMeAccount *ya)
  1846. {
  1847. PurpleConnection *pc;
  1848. gchar *url;
  1849. if (state != PURPLE_IM_TYPING) {
  1850. return 0;
  1851. }
  1852. pc = ya ? ya->pc : purple_conversation_get_connection(conv);
  1853. if (!PURPLE_CONNECTION_IS_CONNECTED(pc)) {
  1854. return 0;
  1855. }
  1856. if (!purple_strequal(purple_protocol_get_id(purple_connection_get_protocol(pc)), GROUPME_PLUGIN_ID)) {
  1857. return 0;
  1858. }
  1859. if (ya == NULL) {
  1860. ya = purple_connection_get_protocol_data(pc);
  1861. }
  1862. printf("Send typing\n");
  1863. #if 0
  1864. guint64 *room_id_ptr = purple_conversation_get_data(conv, "id");
  1865. guint64 room_id = 0;
  1866. if (room_id_ptr) {
  1867. room_id = *room_id_ptr;
  1868. } else {
  1869. room_id = to_int(g_hash_table_lookup(ya->one_to_ones_rev, purple_conversation_get_name(conv)));
  1870. }
  1871. url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/channels/%" G_GUINT64_FORMAT "/typing", room_id);
  1872. groupme_fetch_url(ya, url, "", NULL, NULL);
  1873. g_free(url);
  1874. #endif
  1875. return 10;
  1876. }
  1877. static guint
  1878. groupme_send_typing(PurpleConnection *pc, const gchar *who, PurpleIMTypingState state)
  1879. {
  1880. PurpleConversation *conv;
  1881. conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, purple_connection_get_account(pc)));
  1882. g_return_val_if_fail(conv, -1);
  1883. return groupme_conv_send_typing(conv, state, NULL);
  1884. }
  1885. static gint
  1886. groupme_conversation_send_message(GroupMeAccount *da, guint64 room_id, const gchar *message, gboolean is_dm)
  1887. {
  1888. JsonObject *data = json_object_new();
  1889. JsonObject *msg = json_object_new();
  1890. gchar *url;
  1891. gchar *postdata;
  1892. gchar *stripped = purple_markup_strip_html(message);
  1893. gchar *rid = from_int(room_id);
  1894. gchar *uuid = g_uuid_string_random();
  1895. gchar *guid = g_strdup_printf("groupme-min-%s", uuid);
  1896. g_free(uuid);
  1897. /* Remember we sent it so we don't double display */
  1898. g_hash_table_replace(da->sent_message_ids, guid, TRUE);
  1899. json_object_set_string_member(msg, "text", stripped);
  1900. json_object_set_string_member(msg, "source_guid", guid);
  1901. json_object_set_object_member(data, is_dm ? "direct_message" : "message", msg);
  1902. if (is_dm) {
  1903. json_object_set_string_member(msg, "recipient_id", rid);
  1904. }
  1905. /* DMs use a different endpoint than group messages */
  1906. if (is_dm) {
  1907. url = g_strdup("https://" GROUPME_API_SERVER "/direct_messages?");
  1908. } else {
  1909. url = g_strdup_printf("https://" GROUPME_API_SERVER "/groups/%" G_GUINT64_FORMAT "/messages?", room_id);
  1910. }
  1911. postdata = json_object_to_string(data);
  1912. groupme_fetch_url(da, url, postdata, NULL, NULL);
  1913. g_free(stripped);
  1914. g_free(url);
  1915. g_free(postdata);
  1916. g_free(rid);
  1917. json_object_unref(data);
  1918. return 1;
  1919. }
  1920. static gint
  1921. groupme_chat_send(PurpleConnection *pc, gint id,
  1922. #if PURPLE_VERSION_CHECK(3, 0, 0)
  1923. PurpleMessage *msg)
  1924. {
  1925. const gchar *message = purple_message_get_contents(msg);
  1926. #else
  1927. const gchar *message, PurpleMessageFlags flags)
  1928. {
  1929. #endif
  1930. GroupMeAccount *da;
  1931. PurpleChatConversation *chatconv;
  1932. gint ret;
  1933. da = purple_connection_get_protocol_data(pc);
  1934. chatconv = purple_conversations_find_chat(pc, id);
  1935. guint64 *room_id_ptr = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
  1936. g_return_val_if_fail(room_id_ptr, -1);
  1937. guint64 room_id = *room_id_ptr;
  1938. ret = groupme_conversation_send_message(da, room_id, message, FALSE);
  1939. if (ret > 0) {
  1940. purple_serv_got_chat_in(pc, groupme_chat_hash(room_id), groupme_resolve_nick(da, da->self_user_id, room_id), PURPLE_MESSAGE_SEND, message, time(NULL));
  1941. }
  1942. return ret;
  1943. }
  1944. static int
  1945. groupme_send_im(PurpleConnection *pc,
  1946. #if PURPLE_VERSION_CHECK(3, 0, 0)
  1947. PurpleMessage *msg)
  1948. {
  1949. const gchar *who = purple_message_get_recipient(msg);
  1950. const gchar *message = purple_message_get_contents(msg);
  1951. #else
  1952. const gchar *who, const gchar *message, PurpleMessageFlags flags)
  1953. {
  1954. #endif
  1955. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  1956. gchar *room_id = g_hash_table_lookup(da->one_to_ones_rev, who);
  1957. int room_id_i;
  1958. /* Create DM if there isn't one */
  1959. if (room_id == NULL) {
  1960. #if !PURPLE_VERSION_CHECK(3, 0, 0)
  1961. PurpleMessage *msg = purple_message_new_outgoing(who, message, flags);
  1962. #endif
  1963. guint64 uid = to_int(who);
  1964. GroupMeUser *user = groupme_get_user(da, uid);
  1965. if (!user) {
  1966. purple_debug_error("groupme", "Bad user: %s\n", who);
  1967. return 1;
  1968. }
  1969. /* Cache it */
  1970. g_hash_table_replace(da->one_to_ones, from_int(uid), g_strdup(who));
  1971. g_hash_table_replace(da->one_to_ones_rev, g_strdup(who), from_int(uid));
  1972. room_id_i = user->id;
  1973. } else {
  1974. room_id_i = to_int(room_id);
  1975. }
  1976. return groupme_conversation_send_message(da, room_id_i, message, TRUE);
  1977. }
  1978. static void
  1979. groupme_chat_set_topic(PurpleConnection *pc, int id, const char *topic)
  1980. {
  1981. /* PATCH https:// GROUPME_API_SERVER /api/v6/channels/%s channel */
  1982. /*{ "name" : "test", "position" : 1, "topic" : "new topic", "bitrate" : 64000, "user_limit" : 0 } */
  1983. }
  1984. static void
  1985. groupme_got_avatar(GroupMeAccount *ya, JsonNode *node, gpointer user_data)
  1986. {
  1987. GroupMeUser *user = user_data;
  1988. gchar *username = user->id_s;
  1989. if (node != NULL) {
  1990. JsonObject *response = json_node_get_object(node);
  1991. const gchar *response_str;
  1992. gsize response_len;
  1993. gpointer response_dup;
  1994. response_str = g_dataset_get_data(node, "raw_body");
  1995. response_len = json_object_get_int_member(response, "len");
  1996. response_dup = g_memdup(response_str, response_len);
  1997. purple_buddy_icons_set_for_user(ya->account, username, response_dup, response_len, user->avatar);
  1998. }
  1999. }
  2000. static void
  2001. groupme_get_avatar(GroupMeAccount *da, GroupMeUser *user)
  2002. {
  2003. if (!user) {
  2004. return;
  2005. }
  2006. gchar *username = from_int(user->id);
  2007. const gchar *checksum = purple_buddy_icons_get_checksum_for_user(purple_blist_find_buddy(da->account, username));
  2008. g_free(username);
  2009. if (purple_strequal(checksum, user->avatar)) {
  2010. return;
  2011. }
  2012. /* Disable token for image requests */
  2013. gchar *token = da->token;
  2014. da->token = NULL;
  2015. groupme_fetch_url(da, user->avatar, NULL, groupme_got_avatar, user);
  2016. da->token = token;
  2017. }
  2018. static void
  2019. groupme_get_info(PurpleConnection *pc, const gchar *username)
  2020. {
  2021. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  2022. gchar *url;
  2023. GroupMeUser *user = groupme_get_user(da, to_int(username));
  2024. if (!user) {
  2025. return;
  2026. }
  2027. PurpleNotifyUserInfo *user_info;
  2028. user_info = purple_notify_user_info_new();
  2029. purple_notify_user_info_add_pair_html(user_info, _("ID"), username);
  2030. purple_notify_user_info_add_pair_html(user_info, _("Name"), user->name);
  2031. purple_notify_user_info_add_pair_html(user_info, _("Avatar"), user->avatar);
  2032. purple_notify_user_info_add_section_break(user_info);
  2033. purple_notify_user_info_add_pair_html(user_info, _("Mutual Groups"), "");
  2034. GHashTableIter iter;
  2035. gpointer key, value;
  2036. g_hash_table_iter_init(&iter, user->guild_memberships);
  2037. while (g_hash_table_iter_next(&iter, &key, &value)) {
  2038. GroupMeGuildMembership *membership = value;
  2039. GroupMeGuild *guild = groupme_get_guild(da, membership->id);
  2040. gchar *name = membership->nick;
  2041. gchar *str = g_strdup_printf("%s%s", name, membership->is_op ? "*" : "");
  2042. purple_notify_user_info_add_pair_html(user_info, guild->name, str);
  2043. g_free(str);
  2044. }
  2045. purple_notify_userinfo(da->pc, username, user_info, NULL, NULL);
  2046. }
  2047. static const char *
  2048. groupme_list_icon(PurpleAccount *account, PurpleBuddy *buddy)
  2049. {
  2050. return "groupme";
  2051. }
  2052. static GList *
  2053. groupme_status_types(PurpleAccount *account)
  2054. {
  2055. PurpleStatusType *status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE, "online", _("Online"), TRUE, FALSE, FALSE);
  2056. return g_list_append(NULL, status);
  2057. }
  2058. static gchar *
  2059. groupme_status_text(PurpleBuddy *buddy)
  2060. {
  2061. return NULL;
  2062. }
  2063. static void
  2064. groupme_block_user(PurpleConnection *pc, const char *who)
  2065. {
  2066. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  2067. gchar *url;
  2068. GroupMeUser *user = groupme_get_user_fullname(da, who);
  2069. if (!user) {
  2070. return;
  2071. }
  2072. url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/users/@me/relationships/%" G_GUINT64_FORMAT, user->id);
  2073. groupme_fetch_url_with_method(da, "PUT", url, "{\"type\":2}", NULL, NULL);
  2074. g_free(url);
  2075. }
  2076. static void
  2077. groupme_unblock_user(PurpleConnection *pc, const char *who)
  2078. {
  2079. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  2080. gchar *url;
  2081. GroupMeUser *user = groupme_get_user_fullname(da, who);
  2082. if (!user) {
  2083. return;
  2084. }
  2085. url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/users/@me/relationships/%" G_GUINT64_FORMAT, user->id);
  2086. groupme_fetch_url_with_method(da, "DELETE", url, NULL, NULL, NULL);
  2087. g_free(url);
  2088. }
  2089. const gchar *
  2090. groupme_list_emblem(PurpleBuddy *buddy)
  2091. {
  2092. PurpleAccount *account = purple_buddy_get_account(buddy);
  2093. if (purple_account_is_connected(account)) {
  2094. PurpleConnection *pc = purple_account_get_connection(account);
  2095. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  2096. GroupMeUser *user = groupme_get_user_fullname(da, purple_buddy_get_name(buddy));
  2097. if (user != NULL) {
  2098. if (user->bot) {
  2099. return "bot";
  2100. }
  2101. }
  2102. }
  2103. return NULL;
  2104. }
  2105. void
  2106. groupme_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full)
  2107. {
  2108. PurplePresence *presence = purple_buddy_get_presence(buddy);
  2109. PurpleStatus *status = purple_presence_get_active_status(presence);
  2110. const gchar *message = purple_status_get_attr_string(status, "message");
  2111. purple_notify_user_info_add_pair_html(user_info, _("Status"), purple_status_get_name(status));
  2112. if (message != NULL) {
  2113. gchar *escaped = g_markup_printf_escaped("%s", message);
  2114. purple_notify_user_info_add_pair_html(user_info, _("Playing"), escaped);
  2115. g_free(escaped);
  2116. }
  2117. }
  2118. static GHashTable *
  2119. groupme_get_account_text_table(PurpleAccount *unused)
  2120. {
  2121. GHashTable *table;
  2122. table = g_hash_table_new(g_str_hash, g_str_equal);
  2123. g_hash_table_insert(table, "login_label", (gpointer) _("Email address..."));
  2124. return table;
  2125. }
  2126. static GList *
  2127. groupme_add_account_options(GList *account_options)
  2128. {
  2129. PurpleAccountOption *option;
  2130. option = purple_account_option_bool_new(_("Use status message as in-game info"), "use-status-as-game", FALSE);
  2131. account_options = g_list_append(account_options, option);
  2132. option = purple_account_option_bool_new(_("Auto-create rooms on buddy list"), "populate-blist", TRUE);
  2133. account_options = g_list_append(account_options, option);
  2134. option = purple_account_option_int_new(_("Number of users in a large channel"), "large-channel-count", 20);
  2135. account_options = g_list_append(account_options, option);
  2136. return account_options;
  2137. }
  2138. void
  2139. groupme_join_server_text(gpointer user_data, const gchar *text)
  2140. {
  2141. GroupMeAccount *da = user_data;
  2142. gchar *url;
  2143. const gchar *invite_code;
  2144. invite_code = strrchr(text, '/');
  2145. if (invite_code == NULL) {
  2146. invite_code = text;
  2147. } else {
  2148. invite_code += 1;
  2149. }
  2150. url = g_strdup_printf("https://" GROUPME_API_SERVER "/api/v6/invite/%s", purple_url_encode(invite_code));
  2151. groupme_fetch_url(da, url, "", NULL, NULL);
  2152. g_free(url);
  2153. }
  2154. void
  2155. groupme_join_server(PurpleProtocolAction *action)
  2156. {
  2157. PurpleConnection *pc = purple_protocol_action_get_connection(action);
  2158. GroupMeAccount *da = purple_connection_get_protocol_data(pc);
  2159. purple_request_input(pc, _("Join a server"),
  2160. _("Join a server"),
  2161. _("Enter the join URL here"),
  2162. NULL, FALSE, FALSE, "https://groupme.gg/ABC123",
  2163. _("_Join"), G_CALLBACK(groupme_join_server_text),
  2164. _("_Cancel"), NULL,
  2165. purple_request_cpar_from_connection(pc),
  2166. da);
  2167. }
  2168. static GList *
  2169. groupme_actions(
  2170. #if !PURPLE_VERSION_CHECK(3, 0, 0)
  2171. PurplePlugin *plugin, gpointer context
  2172. #else
  2173. PurpleConnection *pc
  2174. #endif
  2175. )
  2176. {
  2177. GList *m = NULL;
  2178. PurpleProtocolAction *act;
  2179. act = purple_protocol_action_new(_("Join a server..."), groupme_join_server);
  2180. m = g_list_append(m, act);
  2181. return m;
  2182. }
  2183. static PurpleCmdRet
  2184. groupme_cmd_leave(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error, gpointer data)
  2185. {
  2186. PurpleConnection *pc = purple_conversation_get_connection(conv);
  2187. int id = purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv));
  2188. if (pc == NULL || id == -1) {
  2189. return PURPLE_CMD_RET_FAILED;
  2190. }
  2191. groupme_chat_leave(pc, id);
  2192. return PURPLE_CMD_RET_OK;
  2193. }
  2194. static PurpleCmdRet
  2195. groupme_cmd_nick(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error, gpointer data)
  2196. {
  2197. PurpleConnection *pc = purple_conversation_get_connection(conv);
  2198. int id = purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv));
  2199. if (pc == NULL || id == -1) {
  2200. return PURPLE_CMD_RET_FAILED;
  2201. }
  2202. groupme_chat_nick(pc, id, args[0]);
  2203. return PURPLE_CMD_RET_OK;
  2204. }
  2205. static gboolean
  2206. plugin_load(PurplePlugin *plugin, GError **error)
  2207. {
  2208. purple_cmd_register("nick", "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2209. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2210. GROUPME_PLUGIN_ID, groupme_cmd_nick,
  2211. _("nick <new nickname>: Changes nickname on a server"), NULL);
  2212. #if 0
  2213. purple_cmd_register("kick", "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2214. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2215. GROUPME_PLUGIN_ID, groupme_slash_command,
  2216. _("kick <username>: Remove someone from channel"), NULL);
  2217. #endif
  2218. purple_cmd_register("leave", "", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2219. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2220. GROUPME_PLUGIN_ID, groupme_cmd_leave,
  2221. _("leave: Leave the channel"), NULL);
  2222. purple_cmd_register("part", "", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2223. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2224. GROUPME_PLUGIN_ID, groupme_cmd_leave,
  2225. _("part: Leave the channel"), NULL);
  2226. #if 0
  2227. purple_cmd_register("mute", "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2228. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2229. GROUPME_PLUGIN_ID, groupme_slash_command,
  2230. _("mute <username>: Mute someone in channel"), NULL);
  2231. purple_cmd_register("unmute", "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2232. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2233. GROUPME_PLUGIN_ID, groupme_slash_command,
  2234. _("unmute <username>: Un-mute someone in channel"), NULL);
  2235. purple_cmd_register("topic", "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
  2236. PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
  2237. GROUPME_PLUGIN_ID, groupme_slash_command,
  2238. _("topic <description>: Set the channel topic description"), NULL);
  2239. #endif
  2240. return TRUE;
  2241. }
  2242. static gboolean
  2243. plugin_unload(PurplePlugin *plugin, GError **error)
  2244. {
  2245. purple_signals_disconnect_by_handle(plugin);
  2246. return TRUE;
  2247. }
  2248. /* Purple2 Plugin Load Functions */
  2249. #if !PURPLE_VERSION_CHECK(3, 0, 0)
  2250. static gboolean
  2251. libpurple2_plugin_load(PurplePlugin *plugin)
  2252. {
  2253. return plugin_load(plugin, NULL);
  2254. }
  2255. static gboolean
  2256. libpurple2_plugin_unload(PurplePlugin *plugin)
  2257. {
  2258. return plugin_unload(plugin, NULL);
  2259. }
  2260. static void
  2261. plugin_init(PurplePlugin *plugin)
  2262. {
  2263. #ifdef ENABLE_NLS
  2264. bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
  2265. bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
  2266. #endif
  2267. PurplePluginInfo *info;
  2268. PurplePluginProtocolInfo *prpl_info = g_new0(PurplePluginProtocolInfo, 1);
  2269. info = plugin->info;
  2270. if (info == NULL) {
  2271. plugin->info = info = g_new0(PurplePluginInfo, 1);
  2272. }
  2273. info->extra_info = prpl_info;
  2274. #if PURPLE_MINOR_VERSION >= 5
  2275. prpl_info->struct_size = sizeof(PurplePluginProtocolInfo);
  2276. #endif
  2277. prpl_info->options = OPT_PROTO_CHAT_TOPIC | OPT_PROTO_SLASH_COMMANDS_NATIVE | OPT_PROTO_UNIQUE_CHATNAME;
  2278. prpl_info->protocol_options = groupme_add_account_options(prpl_info->protocol_options);
  2279. prpl_info->icon_spec.format = "png,gif,jpeg";
  2280. prpl_info->icon_spec.min_width = 0;
  2281. prpl_info->icon_spec.min_height = 0;
  2282. prpl_info->icon_spec.max_width = 96;
  2283. prpl_info->icon_spec.max_height = 96;
  2284. prpl_info->icon_spec.max_filesize = 0;
  2285. prpl_info->icon_spec.scale_rules = PURPLE_ICON_SCALE_DISPLAY;
  2286. prpl_info->get_account_text_table = groupme_get_account_text_table;
  2287. prpl_info->list_emblem = groupme_list_emblem;
  2288. prpl_info->status_text = groupme_status_text;
  2289. prpl_info->tooltip_text = groupme_tooltip_text;
  2290. prpl_info->list_icon = groupme_list_icon;
  2291. prpl_info->status_types = groupme_status_types;
  2292. prpl_info->chat_info = groupme_chat_info;
  2293. prpl_info->chat_info_defaults = groupme_chat_info_defaults;
  2294. prpl_info->login = groupme_login;
  2295. prpl_info->close = groupme_close;
  2296. prpl_info->send_im = groupme_send_im;
  2297. prpl_info->send_typing = groupme_send_typing;
  2298. prpl_info->join_chat = groupme_join_chat;
  2299. prpl_info->get_chat_name = groupme_get_chat_name;
  2300. prpl_info->find_blist_chat = groupme_find_chat;
  2301. prpl_info->chat_invite = groupme_chat_invite;
  2302. prpl_info->chat_send = groupme_chat_send;
  2303. prpl_info->set_chat_topic = groupme_chat_set_topic;
  2304. prpl_info->get_cb_real_name = groupme_get_real_name;
  2305. prpl_info->get_info = groupme_get_info;
  2306. prpl_info->add_deny = groupme_block_user;
  2307. prpl_info->rem_deny = groupme_unblock_user;
  2308. prpl_info->roomlist_get_list = groupme_roomlist_get_list;
  2309. prpl_info->roomlist_room_serialize = groupme_roomlist_serialize;
  2310. }
  2311. static PurplePluginInfo info = {
  2312. PURPLE_PLUGIN_MAGIC,
  2313. /* PURPLE_MAJOR_VERSION,
  2314. PURPLE_MINOR_VERSION,
  2315. */
  2316. 2, 1,
  2317. PURPLE_PLUGIN_PROTOCOL, /* type */
  2318. NULL, /* ui_requirement */
  2319. 0, /* flags */
  2320. NULL, /* dependencies */
  2321. PURPLE_PRIORITY_DEFAULT, /* priority */
  2322. GROUPME_PLUGIN_ID, /* id */
  2323. "GroupMe", /* name */
  2324. GROUPME_PLUGIN_VERSION, /* version */
  2325. "", /* summary */
  2326. "", /* description */
  2327. "Alyssa Rosenzweig <alyssa@rosenzweig.io>", /* author */
  2328. GROUPME_PLUGIN_WEBSITE, /* homepage */
  2329. libpurple2_plugin_load, /* load */
  2330. libpurple2_plugin_unload, /* unload */
  2331. NULL, /* destroy */
  2332. NULL, /* ui_info */
  2333. NULL, /* extra_info */
  2334. NULL, /* prefs_info */
  2335. groupme_actions, /* actions */
  2336. NULL, /* padding */
  2337. NULL,
  2338. NULL,
  2339. NULL
  2340. };
  2341. PURPLE_INIT_PLUGIN(groupme, plugin_init, info);
  2342. #else
  2343. /* Purple 3 plugin load functions */
  2344. G_MODULE_EXPORT GType groupme_protocol_get_type(void);
  2345. #define GROUPME_TYPE_PROTOCOL (groupme_protocol_get_type())
  2346. #define GROUPME_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GROUPME_TYPE_PROTOCOL, GroupMeProtocol))
  2347. #define GROUPME_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GROUPME_TYPE_PROTOCOL, GroupMeProtocolClass))
  2348. #define GROUPME_IS_PROTOCOL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GROUPME_TYPE_PROTOCOL))
  2349. #define GROUPME_IS_PROTOCOL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GROUPME_TYPE_PROTOCOL))
  2350. #define GROUPME_PROTOCOL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GROUPME_TYPE_PROTOCOL, GroupMeProtocolClass))
  2351. typedef struct _GroupMeProtocol {
  2352. PurpleProtocol parent;
  2353. } GroupMeProtocol;
  2354. typedef struct _GroupMeProtocolClass {
  2355. PurpleProtocolClass parent_class;
  2356. } GroupMeProtocolClass;
  2357. static void
  2358. groupme_protocol_init(PurpleProtocol *prpl_info)
  2359. {
  2360. PurpleProtocol *info = prpl_info;
  2361. info->id = GROUPME_PLUGIN_ID;
  2362. info->name = "GroupMe";
  2363. info->options = OPT_PROTO_CHAT_TOPIC | OPT_PROTO_SLASH_COMMANDS_NATIVE | OPT_PROTO_UNIQUE_CHATNAME;
  2364. info->account_options = groupme_add_account_options(info->account_options);
  2365. }
  2366. static void
  2367. groupme_protocol_class_init(PurpleProtocolClass *prpl_info)
  2368. {
  2369. prpl_info->login = groupme_login;
  2370. prpl_info->close = groupme_close;
  2371. prpl_info->status_types = groupme_status_types;
  2372. prpl_info->list_icon = groupme_list_icon;
  2373. }
  2374. static void
  2375. groupme_protocol_im_iface_init(PurpleProtocolIMIface *prpl_info)
  2376. {
  2377. prpl_info->send = groupme_send_im;
  2378. prpl_info->send_typing = groupme_send_typing;
  2379. }
  2380. static void
  2381. groupme_protocol_chat_iface_init(PurpleProtocolChatIface *prpl_info)
  2382. {
  2383. prpl_info->send = groupme_chat_send;
  2384. prpl_info->info = groupme_chat_info;
  2385. prpl_info->info_defaults = groupme_chat_info_defaults;
  2386. prpl_info->join = groupme_join_chat;
  2387. prpl_info->get_name = groupme_get_chat_name;
  2388. prpl_info->invite = groupme_chat_invite;
  2389. prpl_info->set_topic = groupme_chat_set_topic;
  2390. prpl_info->get_user_real_name = groupme_get_real_name;
  2391. }
  2392. static void
  2393. groupme_protocol_server_iface_init(PurpleProtocolServerIface *prpl_info)
  2394. {
  2395. prpl_info->get_info = groupme_get_info;
  2396. }
  2397. static void
  2398. groupme_protocol_client_iface_init(PurpleProtocolClientIface *prpl_info)
  2399. {
  2400. prpl_info->get_account_text_table = groupme_get_account_text_table;
  2401. prpl_info->status_text = groupme_status_text;
  2402. prpl_info->get_actions = groupme_actions;
  2403. prpl_info->list_emblem = groupme_list_emblem;
  2404. prpl_info->tooltip_text = groupme_tooltip_text;
  2405. prpl_info->find_blist_chat = groupme_find_chat;
  2406. }
  2407. static void
  2408. groupme_protocol_privacy_iface_init(PurpleProtocolPrivacyIface *prpl_info)
  2409. {
  2410. prpl_info->add_deny = groupme_block_user;
  2411. prpl_info->rem_deny = groupme_unblock_user;
  2412. }
  2413. static void
  2414. groupme_protocol_roomlist_iface_init(PurpleProtocolRoomlistIface *prpl_info)
  2415. {
  2416. prpl_info->get_list = groupme_roomlist_get_list;
  2417. prpl_info->room_serialize = groupme_roomlist_serialize;
  2418. }
  2419. static PurpleProtocol *groupme_protocol;
  2420. PURPLE_DEFINE_TYPE_EXTENDED(
  2421. GroupMeProtocol, groupme_protocol, PURPLE_TYPE_PROTOCOL, 0,
  2422. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_IM_IFACE,
  2423. groupme_protocol_im_iface_init)
  2424. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CHAT_IFACE,
  2425. groupme_protocol_chat_iface_init)
  2426. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_SERVER_IFACE,
  2427. groupme_protocol_server_iface_init)
  2428. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CLIENT_IFACE,
  2429. groupme_protocol_client_iface_init)
  2430. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_PRIVACY_IFACE,
  2431. groupme_protocol_privacy_iface_init)
  2432. PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_ROOMLIST_IFACE,
  2433. groupme_protocol_roomlist_iface_init)
  2434. );
  2435. static gboolean
  2436. libpurple3_plugin_load(PurplePlugin *plugin, GError **error)
  2437. {
  2438. groupme_protocol_register_type(plugin);
  2439. groupme_protocol = purple_protocols_add(GROUPME_TYPE_PROTOCOL, error);
  2440. if (!groupme_protocol) {
  2441. return FALSE;
  2442. }
  2443. return plugin_load(plugin, error);
  2444. }
  2445. static gboolean
  2446. libpurple3_plugin_unload(PurplePlugin *plugin, GError **error)
  2447. {
  2448. if (!plugin_unload(plugin, error)) {
  2449. return FALSE;
  2450. }
  2451. if (!purple_protocols_remove(groupme_protocol, error)) {
  2452. return FALSE;
  2453. }
  2454. return TRUE;
  2455. }
  2456. static PurplePluginInfo *
  2457. plugin_query(GError **error)
  2458. {
  2459. #ifdef ENABLE_NLS
  2460. bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
  2461. bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
  2462. #endif
  2463. return purple_plugin_info_new(
  2464. "id", GROUPME_PLUGIN_ID,
  2465. "name", "GroupMe",
  2466. "version", GROUPME_PLUGIN_VERSION,
  2467. "category", _("Protocol"),
  2468. "summary", _("GroupMe Protocol Plugins."),
  2469. "description", _("Adds GroupMe protocol support to libpurple."),
  2470. "website", GROUPME_PLUGIN_WEBSITE,
  2471. "abi-version", PURPLE_ABI_VERSION,
  2472. "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
  2473. PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
  2474. NULL);
  2475. }
  2476. PURPLE_PLUGIN_INIT(groupme, plugin_query, libpurple3_plugin_load, libpurple3_plugin_unload);
  2477. #endif