#pragma once #define _XOPEN_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #define DC_API_PREFIX "https://discord.com/api/v8/" /* this can be a format string, DO NOT use format characters inside */ #define DC_LOGIN_FORMAT "{\"login\":\"%s\",\"password\":\"%s\",\"undelete\":false,\"captcha_key\":null,\"login_source\":null,\"gift_code_sku_id\":null}" #define DC_USER_AGENT "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" #define DC_ERROR(e, s, m, ...) dc_push_error(e, s, __func__, __FILE__, __LINE__, 0##__VA_OPT__(1), m __VA_OPT__(,) __VA_ARGS__) #define DC_CLIENT_ERROR(c, m, ...) DC_ERROR(c->errors, &c->errors_sizeof, m __VA_OPT__(,) __VA_ARGS__) /* yeah, that m is not a typo */ #define DC_API(curl, body, endpoint, ...) dc_api(curl, body, 0##__VA_OPT__(1), endpoint __VA_OPT__(,) __VA_ARGS__) #define cJSON_GetObjectItem2(root, name1, name2) (cJSON_GetObjectItem(root, name1) ? cJSON_GetObjectItem(cJSON_GetObjectItem(root, name1), name2) : NULL) #define DC_TIMESTAMP_FORMAT "%Y-%m-%dT%H:%M:%S.XXXXXX%z" struct dc_error { size_t line; const char * function /* this is a static string, so it is not copied, do not free it */; char * file /* this is a static string, so it is not copied, do not free it */; char * message /* this is copied, caller can free it if it was dynamically created by it, it must be freed before destroying struct */; time_t time; short unsigned int reported /* 0: error was not yet shown to the user, 1: error was already shown to the user */; }; int dc_error_free(struct dc_error * e) { free(e->message); e->message = NULL; /* other strings are static */ e->line = 0; e->function = NULL; e->file = NULL; e->reported = 0; free(e); }; struct dc_message { char * username; int discriminator; char * content; time_t time; struct dc_channel * channel; unsigned short int tts; unsigned long long int id; }; int dc_message_free (struct dc_message * m) { free(m->username); m->username = NULL; free(m->content); m->content = NULL; m->channel = NULL; m->tts = 0; m->discriminator = -1; m->id = 0; /* i'm not sure how to "unset" time_t ... */ free(m); } int dc_message_compare (const void * a, const void * b) { struct dc_message * c = (struct dc_message *) a; struct dc_message * d = (struct dc_message *) b; /* strežnik v arrayu najprej pošlje sporočila iz sedanjosti, nato tista iz preteklosti, zato bo tudi sortiran array takšen */ return c->time > d->time ? -1 : c->time < d->time ? 1 : 0; } struct dc_channel { char * name; char * topic; unsigned long long int id; struct dc_guild * guild; struct dc_message * messages; size_t messages_sizeof; unsigned short int sent /* describes status, was message already sent (1) or is it to-be sent (0)? */; }; int dc_channel_free (struct dc_channel * ch) { free(ch->name); ch->name = NULL; free(ch->topic); ch->topic = NULL; ch->guild = NULL; ch->id = 0; for (int i = 0; i < ch->messages_sizeof; i++) dc_message_free(&ch->messages[i]); ch->messages_sizeof = 0; free(ch); } struct dc_guild { char * name; unsigned long long id; size_t channels_sizeof; struct dc_channel * channels; }; int dc_guild_free (struct dc_guild * g) { free(g->name); g->name = NULL; g->id = 0; for (int i = 0; i < g->channels_sizeof; i++) dc_channel_free(&g->channels[i]); g->channels_sizeof = 0; free(g); } struct dc_client { CURL * curl; struct curl_slist * curl_headers; char * authorization; char * email; /* email and password are copied, even though they are sometimes static ENV vars - they are not always! */ char * password; char * username; int discriminator; struct dc_guild * guilds; size_t guilds_sizeof; struct dc_channel * joinedchannel; struct dc_error * errors; size_t errors_sizeof; struct dc_message * sent_messages; size_t sent_messages_sizeof; }; int dc_client_free (struct dc_client * c) { curl_easy_cleanup(c->curl); curl_slist_free_all(c->curl_headers); free(c->authorization); c->authorization = NULL; free(c->email); c->email = NULL; free(c->password); c->password = NULL; free(c->username); c->username = NULL; c->discriminator = -1; c->joinedchannel = NULL; for (int i = 0; i < c->guilds_sizeof; i++) dc_guild_free(&c->guilds[i]); c->guilds_sizeof = 0; for (int i = 0; i < c->errors_sizeof; i++) dc_error_free(&c->errors[i]); c->errors_sizeof = 0; for (int i = 0; i < c->sent_messages_sizeof; c++) dc_message_free(&c->sent_messages[i]); c->sent_messages_sizeof = 0; free(c); } int dc_push_error (struct dc_error * e, size_t * s, const char * c, char * f, size_t l, unsigned short int isfmt, char * m, ...) { e = realloc(e, sizeof(struct dc_error)*++*s); /* note: format arguments are evaluated twice */ size_t strlenm = strlen(m); size_t va_count = parse_printf_format(m, 0, NULL); if (isfmt && va_count > 0) { va_list ap, ap2; va_start(ap, m); va_copy(ap2, ap); strlenm = vsnprintf(NULL, 0, m, ap); e[*s-1].message = malloc(sizeof(char)*strlenm+1); vsnprintf(e[*s-1].message, strlenm+1, m, ap2); va_end(ap); va_end(ap2); } else { e[*s-1].message = malloc(sizeof(char)*strlenm+1); strcpy(e[*s-1].message, m); } e[*s-1].file = f; e[*s-1].line = l; e[*s-1].function = c /* Caller */; e[*s-1].time = time(NULL); e[*s-1].reported = 0; return 1; } cJSON * dc_api (CURL * curl, char * body, int isfmt, char * endpoint, ...) { /* note: format arguments are evaluated twice */ if (!curl) return NULL; if (!endpoint) return NULL; cJSON * json = NULL; struct writefunc_string s; init_writefunc_string(&s); size_t va_count = parse_printf_format(endpoint, 0, NULL); char * endpoint_formatted = NULL; if (isfmt && va_count > 0) { va_list ap, ap2; va_start(ap, endpoint); va_copy(ap2, ap); size_t strlenm = vsnprintf(NULL, 0, endpoint, ap); endpoint_formatted = malloc(sizeof(char)*strlenm+1); vsnprintf(endpoint_formatted, strlenm, endpoint_formatted, ap2); /* sn instead of s because double evaulation may produce */ va_end(ap); /* larger output the next time and lead to overflow */ va_end(ap2); } curl_easy_setopt(curl, CURLOPT_URL, endpoint_formatted ? endpoint_formatted : endpoint); if (!body) curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); else curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); if (curl_easy_perform(curl) != CURLE_OK) goto rc; json = cJSON_Parse(s.ptr); rc: free(endpoint_formatted); free(s.ptr); s.ptr = NULL; return json; } int dc_login (struct dc_client * c) { int rs = 1; struct writefunc_string s; char * data = NULL; cJSON * json = NULL; if (!c) return -1; if (!c->email || !c->password) { DC_CLIENT_ERROR(c, DC_I18N_MISSING_EP); return -2; } if (!c->curl) c->curl = curl_easy_init(); if (!c->curl) { DC_CLIENT_ERROR(c, "curl_easy_init() " DC_I18N_FAILED); return -3; } init_writefunc_string(&s); data = malloc(strlen(DC_LOGIN_FORMAT)+strlen(c->email)+strlen(c->password)+1); CURLcode res; sprintf(data, DC_LOGIN_FORMAT, c->email, c->password); c->curl_headers = curl_slist_append(c->curl_headers, "Content-Type: application/json"); c->curl_headers = curl_slist_append(c->curl_headers, "User-Agent: " DC_USER_AGENT); curl_easy_setopt(c->curl, CURLOPT_URL, DC_API_PREFIX "auth/login"); curl_easy_setopt(c->curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(c->curl, CURLOPT_POSTFIELDS, data); curl_easy_setopt(c->curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(c->curl, CURLOPT_HTTPHEADER, c->curl_headers); curl_easy_setopt(c->curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(c->curl); if (res != CURLE_OK) { DC_CLIENT_ERROR(c, "curl_easy_perform() " DC_I18N_FAILED ": %s", curl_easy_strerror(res)); /* yeah, format strings are supported */ rs = -4; goto rc; } json = cJSON_Parse(s.ptr); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -5; goto rc; } cJSON * token = cJSON_GetObjectItem(json, "token"); if (!cJSON_IsString(token)) { DC_CLIENT_ERROR(c, "!cJSON_IsString(token)"); rs = -6; goto rc; } c->authorization = realloc(c->authorization, strlen(token->valuestring)+1); strcpy(c->authorization, token->valuestring); data = realloc(data, strlen(c->authorization)+strlen("Authorization: ")+1); strcpy(data, "Authorization: "); strcat(data, c->authorization); free(s.ptr); s.ptr = NULL; init_writefunc_string(&s); curl_easy_setopt(c->curl, CURLOPT_URL, DC_API_PREFIX "users/@me"); curl_easy_setopt(c->curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(c->curl, CURLOPT_WRITEDATA, &s); c->curl_headers = curl_slist_append(c->curl_headers, data); res = curl_easy_perform(c->curl); if (res != CURLE_OK) { DC_CLIENT_ERROR(c, "curl_easy_perform() " DC_I18N_FAILED ": %s", curl_easy_strerror(res)); rs = -7; goto rc; } cJSON_Delete(json); json = cJSON_Parse(s.ptr); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -8; goto rc; } token = cJSON_GetObjectItem(json, "username"); cJSON * token2 = cJSON_GetObjectItem(json, "discriminator"); if (!cJSON_IsString(token) || !cJSON_IsString(token2)) { DC_CLIENT_ERROR(c, "!cJSON_IsString(token) || !cJSON_IsString(token2)"); rs = -9; goto rc; } c->username = realloc(c->username, strlen(token->valuestring)+1); strcpy(c->username, token->valuestring); c->discriminator = strtol(token2->valuestring, NULL, 10); rc: free(s.ptr); s.ptr = NULL; free(data); data = NULL; cJSON_Delete(json); return rs; } int dc_fetch_guilds (struct dc_client * c) { if (!c) return -5; int rs = 1; struct writefunc_string s; char * value = NULL; char * value2 = NULL; CURLcode res; cJSON * json = NULL; if (!c->username || !c->username[0]) if ((rs = dc_login(c)) < 0) { DC_CLIENT_ERROR(c, "dc_login(c) " DC_I18N_FAILED " (%d)", rs); return -1; } else rs = 1; curl_easy_setopt(c->curl, CURLOPT_URL, DC_API_PREFIX "users/@me/guilds"); curl_easy_setopt(c->curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(c->curl, CURLOPT_WRITEDATA, &s); init_writefunc_string(&s); res = curl_easy_perform(c->curl); if (res != CURLE_OK) { DC_CLIENT_ERROR(c, "curl_easy_perform(c->curl) " DC_I18N_FAILED ": %s", curl_easy_strerror(res)); rs = -2; goto rc; } json = cJSON_Parse(s.ptr); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -3; goto rc; } if (!cJSON_IsArray(json)) { DC_CLIENT_ERROR(c, "!cJSON_IsArray(json)"); rs = -4; goto rc; } for (int i = 0; i < c->guilds_sizeof; i++) dc_guild_free(&c->guilds[i]); c->guilds = NULL; c->guilds_sizeof = 0; cJSON * guild = NULL; cJSON_ArrayForEach(guild, json) { value = cJSON_GetStringValue(cJSON_GetObjectItem(json, "id")); value2= cJSON_GetStringValue(cJSON_GetObjectItem(json, "name")); if (!value || !value2) { DC_CLIENT_ERROR(c, "!cJSON_GetStringValue(cJSON_GetObjectItem(json, \"id\" || \"name\"))"); continue; } c->guilds = realloc(c->guilds, ++c->guilds_sizeof); c->guilds[c->guilds_sizeof-1].name = malloc(strlen(value)+1); strcpy(c->guilds[c->guilds_sizeof-1].name, value); c->guilds[c->guilds_sizeof-1].id = strtoull(value2, NULL, 10); c->guilds[c->guilds_sizeof-1].channels_sizeof = 0; c->guilds[c->guilds_sizeof-1].channels = NULL; } rc: free(s.ptr); s.ptr = NULL; cJSON_Delete(json); json = NULL; return rs; } int dc_fetch_channels (struct dc_client * c, struct dc_guild * g) { int rs = 1; if (!c) return -1; if (!c->username || !c->username[0]) if ((rs = dc_login(c)) < 0) { DC_CLIENT_ERROR(c, "dc_login(c) " DC_I18N_FAILED " (%d)", rs); return -2; } else rs = 1; if (!g || !g->id) { DC_CLIENT_ERROR(c, "!g || !g->id"); return -3; } CURLcode res; struct writefunc_string s; init_writefunc_string(&s); cJSON * json = NULL; char * url = malloc(strlen(DC_API_PREFIX "guilds/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/channels")+1); sprintf(url, DC_API_PREFIX "guilds/%llu/channels", g->id); curl_easy_setopt(c->curl, CURLOPT_URL, url); curl_easy_setopt(c->curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(c->curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(c->curl); if (res != CURLE_OK) { DC_CLIENT_ERROR(c, "curl_easy_perform(c->curl) " DC_I18N_FAILED ": %s", curl_easy_strerror(res)); rs = -4; goto rc; } json = cJSON_Parse(s.ptr); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -5; goto rc; } if (!cJSON_IsArray(json)) { DC_CLIENT_ERROR(c, "!cJSON_IsArray(json), s.ptr = %s", s.ptr); rs = -6; goto rc; } cJSON * channel = NULL; for (int i = 0; i < g->channels_sizeof; i++) dc_channel_free(&g->channels[i]); g->channels_sizeof = 0; cJSON_ArrayForEach(channel, json) { char * topic = cJSON_GetStringValue(cJSON_GetObjectItem(json, "topic")); char * name = cJSON_GetStringValue(cJSON_GetObjectItem(json, "name")); char * id = cJSON_GetStringValue(cJSON_GetObjectItem(json, "id")); double type = cJSON_GetNumberValue(cJSON_GetObjectItem(json, "type")); if (!id || !name || type == NAN) { DC_CLIENT_ERROR(c, "!id || !name || type == NAN"); continue; } if (type != 0) /* if it's not a text channel (z. B. voice channel, category, ...) */ continue; if (!topic) topic = ""; g->channels = realloc(g->channels, ++g->channels_sizeof); g->channels[g->channels_sizeof-1].name = malloc(strlen(name)+1); strcpy(g->channels[g->channels_sizeof-1].name, name); g->channels[g->channels_sizeof-1].topic = malloc(strlen(topic)+1); strcpy(g->channels[g->channels_sizeof-1].topic, topic); g->channels[g->channels_sizeof-1].id = strtoull(id, NULL, 10); g->channels[g->channels_sizeof-1].guild = g; g->channels[g->channels_sizeof-1].messages = NULL; g->channels[g->channels_sizeof-1].messages_sizeof = 0; } rc: free(s.ptr); s.ptr = NULL; free(url); url = NULL; cJSON_Delete(json); json = NULL; return rs; } int dc_send_message (struct dc_client * c, struct dc_message * m) { int rs = 1; struct tm tm; /* not used at all, you can delete it, I am autistic xD */ if (!c) return -1; if (!c->username || !c->username[0]) if ((rs = dc_login(c)) < 0) { DC_CLIENT_ERROR(c, "dc_login(c) " DC_I18N_FAILED " (%d)", rs); return -2; } else rs = 1; /* it'd be bad to assume joinedchannel if the following is false though */ if (!m) { DC_CLIENT_ERROR(c, "!m"); return -3; } cJSON * json = cJSON_CreateObject(); cJSON * nons = cJSON_CreateNumber(rand()); cJSON_AddItemToObject(json, "nonce", nons); cJSON * content = cJSON_CreateString(m->content); cJSON_AddItemToObject(json, "content", content); cJSON * tts = m->tts ? cJSON_CreateTrue() : cJSON_CreateFalse(); cJSON_AddItemToObject(json, "tts", tts); char * body = cJSON_Print(json); if (!body) { DC_CLIENT_ERROR(c, "cJSON_Print " DC_I18N_FAILED); rs = -4; goto rc; } cJSON_Delete(json); json = NULL; /* {content: "yeet", nonce: "820762917392613376", tts: false} */ json = DC_API(c->curl, body, DC_API_PREFIX "channels/%llu/messages", m->channel->id); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -5; goto rc; } char * discriminator = cJSON_GetStringValue(cJSON_GetObjectItem2(json, "author", "discriminator")); if (!discriminator) { DC_CLIENT_ERROR(c, "!discriminator"); rs = -6; goto rc; } m->discriminator = strtol(discriminator, NULL, 10); char * username = cJSON_GetStringValue(cJSON_GetObjectItem2(json, "author", "username")); if (!username) { DC_CLIENT_ERROR(c, "!username"); rs = -6; goto rc; } m->username = malloc(strlen(username)+1); strcpy(m->username, username); /* we don't directly point as that changes when we delete */ rc: free(body); body = NULL; cJSON_Delete(json); json = NULL; return rs; } int dc_fetch_messages (struct dc_client * c, struct dc_channel * ch) { int rs = 1; struct tm tm; if (!c) return -1; if (!c->username || !c->username[0]) if ((rs = dc_login(c)) < 0) { DC_CLIENT_ERROR(c, "dc_login(c) " DC_I18N_FAILED " (%d)", rs); return -2; } else rs = 1; /* it'd be bad to assume joinedchannel if the following is false though */ if (!ch || !ch->id) { DC_CLIENT_ERROR(c, "!ch || !ch->id"); return -3; } cJSON * json = DC_API(c->curl, NULL, DC_API_PREFIX "channels/%llu/messages?limit=100", ch->id); if (!json) { const char *error_ptr = cJSON_GetErrorPtr(); if (!error_ptr) { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED ": " DC_I18N_JSON_ERROR_BEFORE ": %s", error_ptr); } else { DC_CLIENT_ERROR(c, "cJSON_Parse " DC_I18N_FAILED); } rs = -5; goto rc; } if (!cJSON_IsArray(json)) { DC_CLIENT_ERROR(c, "!cJSON_IsArray(json)"); rs = -6; goto rc; } cJSON * message = NULL; /* for (int i = 0; i < ch->messages_sizeof; i++) dc_message_free(&ch->messages[i]); ch->messages_sizeof = 0; */ /* we'll rather add messages to existing ones */ cJSON_ArrayForEach(message, json) { char * timestamp = cJSON_GetStringValue(cJSON_GetObjectItem(json, "timestamp")); char * content = cJSON_GetStringValue(cJSON_GetObjectItem(json, "content")); char * id = cJSON_GetStringValue(cJSON_GetObjectItem(json, "id")); char * discriminator = cJSON_GetStringValue(cJSON_GetObjectItem2(json, "author", "discriminator")); char * username = cJSON_GetStringValue(cJSON_GetObjectItem2(json, "author", "username")); if (!id || !timestamp || !content || !username || !discriminator) { DC_CLIENT_ERROR(c, "!id || !timestamp || !content || !username || discriminator < 0"); continue; } if (strlen(timestamp) < 26) { DC_CLIENT_ERROR(c, "strlen(timestamp) < 26"); continue; } for (int i = 20; i <= 25; i++) timestamp[i] == 'X'; /* because strptime does not have wildcard support and those numbers are sub-second fractions */ if (!strptime(timestamp, DC_TIMESTAMP_FORMAT, &tm)) { DC_CLIENT_ERROR(c, "strptime(timestamp, DC_TIMESTAMP_FORMAT, &tm) " DC_I18N_FAILED); continue; } unsigned long long int idull = strtoull(id, NULL, 10); for (int i = 0; i < ch->messages_sizeof; i++) if (idull == ch->messages[i].id) continue; /* remove duplicates */ ch->messages = realloc(ch->messages, ++ch->messages_sizeof); #define DC_FMTM /* fetch messages this message */ ch->messages[ch->messages_sizeof-1] DC_FMTM.time = mktime(&tm); DC_FMTM.content = malloc(strlen(content)+1); strcpy(DC_FMTM.content, content); DC_FMTM.username = malloc(strlen(username)+1); strcpy(DC_FMTM.username, username); DC_FMTM.id = idull; DC_FMTM.discriminator = strtol(discriminator, NULL, 10); DC_FMTM.channel = ch; } qsort(ch->messages, ch->messages_sizeof, sizeof(struct dc_message), dc_message_compare); /* we sort so that present messages are in the start of the array and old messages are to the end of the array */ rc: cJSON_Delete(json); json = NULL; return rs; } struct dc_api_thread_control { unsigned short int power; /* 1 if the thread should run, set it to 0 for the thread to return at the end of the loop */ struct dc_client * clients; /* "array" of clients the thread should manage, ONLY ONE dc_api_thread PER PROCESS! */ size_t dc_clients_sizeof; }; int dc_api_thread (struct dc_api_thread_control * t) { /* updates messages and sends messages when they are in the outbox */ return -1; } /* the thread shall use mutexes when doing things with shared memory - client structs */