From 70cf778548fc47d99fef83b892191902178d1242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Luka=20=C5=A0ijanec?= Date: Mon, 20 Sep 2021 21:12:28 +0200 Subject: untested, compiles --- src/api.c | 217 +++++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 131 insertions(+), 86 deletions(-) (limited to 'src/api.c') diff --git a/src/api.c b/src/api.c index c0dfc48..20d7b4f 100644 --- a/src/api.c +++ b/src/api.c @@ -1,7 +1,7 @@ #define DC_LOGIN_FORMAT "{\"login\":\"%s\",\"password\":\"%s\",\"undelete\":false,\"captcha_key\":null,\"login_source\":null,\"gift_code_sku_id\":null}" #define DC_COMPILE_INFO __VERSION__ " " __BASE_FILE__ " " __FILE__ " " __TIMESTAMP__ -#define DC_IDENTIFY_FORMAT "{\"op\":2,\"d\":{\"token\":\"%s\",\"intents\":%d" /* ",\"presence\":{\"since\":null,\"status\":\"online\",\"afk\":false,\"activities\":{\"name\":\":pager: Connected via discord.c\",\"type\":4}}" */ ",\"properties\":{\"$os\":\"posix\",\"$browser\":\"discord.c\",\"$device\":\"" DC_COMPILE_INFO "\"}}}" /* this exposes we are discord.c, if this turns out to get people banned, change to whatever an official client sends */ -#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" /* this is so we can fool the login system */ +#define DC_STR(x) x +#define DC_USER_AGENT "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" /* this is so we can fool the login system */ #define DC_SERVER_ADDRESS "discord.com" #define DC_SERVER_PORT 443 #define DC_SERVER_SSL 1 @@ -10,9 +10,14 @@ #define DC_WS_PORT DC_SERVER_PORT #define DC_WS_SSL DC_SERVER_SSL #define DC_WS_ORIGIN DC_SERVER_ORIGIN -#define DC_WS_PATH "/" /* add ?v=9&encoding=json in case of problems */ +#define DC_WS_PATH "/?encoding=json&v=9" #define DC_API_PREFIX "/api/v9/" #define DC_LWS_ABLE_TO_PARSE_HEADERS 1 +#define DC_RECONNECT_DELAY 10 +unsigned char dc_api_identify_u[] = { +#include +}; +char * dc_api_identify = (char *) dc_api_identify_u; /* libwebsockets information: libwebsockets works with event loops and discord.c primary targets debian for building (it must work on debian), but debian does not ship libwebsockets with glib or libevent event loop support, so loops are implemented in the classic poll() style. this reduces performance probably. if you use discord.c API with support for a platform specific event loop, you may rewrite LWS to do things differently. currently calls into API (dc_api_i and dc_api_o) will both do LWS stuff. */ void dc_api_stack (struct dc_api_io i) { /* stack output struct to be delivered via dc_api_o 2usr */ DC_MR(i.program->api_ios); @@ -22,12 +27,13 @@ void dc_api_stack (struct dc_api_io i) { /* stack output struct to be delivered } signed char dc_json_cb (struct lejp_ctx * ctx, char /* enum lejp_callbacks */ reason) { enum dc_json_paths path = ctx->path_match-1; /* we assume that the order of incoming data is */ - struct dc_lws_pass * pass = ctx->user; /* correct. op should come first, etc. */ - struct dc_client * client = pass->api_io.client; + struct dc_lws_pass * pass = ctx->user; /* correct. op and t should come first, etc. */ + struct dc_client * client = pass->api_io.client; struct dc_program * program = pass->api_io.program; - char * cp; pass->json_reason = reason; - if (reason == LEJPCB_FAILED) { + if (reason == LEJPCB_FAILED || reason == LEJPCB_COMPLETE || reason == LEJPCB_START) { + if (pass->parsing_status) + *pass->parsing_status &= ~DC_IN_PROGRESS; DC_API_IO_GC(pass->api_io); pass->packet = DC_NONE; pass->parsing_status = NULL; @@ -40,20 +46,34 @@ signed char dc_json_cb (struct lejp_ctx * ctx, char /* enum lejp_callbacks */ re pass->api_io.client->last_packet = strtoull(ctx->buf, NULL, 10); break; case DC_JSON_OP: - DC_API_IO_GC(pass->api_io); /* frees all unfinished parsing objects */ - pass->packet = atoi(ctx->buf); + switch (atoi(ctx->buf)) { + case DC_PING: + client->last_ping = 0; + dc_handle_ping(pass->api_io, NULL); + break; + default: + break; + } break; case DC_JSON_PING: - fprintf(stderr, "found ping interval %d\n", atoi(ctx->buf)/1000-5); - pass->api_io.client->ping_interval = atoi(ctx->buf)/1000-5; + pass->api_io.client->ping_interval = atoi(ctx->buf)/1000-1; + pass->api_io.client->last_ping = time(NULL); /* we don't ping b4 IDENT */ + break; + case DC_JSON_T: + for (size_t i = 0; i < sizeof(dc_ws_packet)/sizeof(dc_ws_packet[0]); i++) + if (strcmp(dc_ws_packet[i], ctx->buf)) { + pass->packet = DC_STRPKTOFF+i; + break; + } break; default: /* to prevent warning: enumeration value DC_JSON_* not handled */ break; } return '\0'; } - if (startswith(ctx->path, dc_json_paths[DC_JSON_ME]) && reason & LEJP_FLAG_CB_IS_VALUE) { - if (!client->user) { + if (startswith(ctx->path, dc_json_paths[DC_JSON_ME]) && reason & LEJP_FLAG_CB_IS_VALUE + && (!client->user || !client->user->username || !client->user->id || client->user->status & DC_INCOMPLETE)) { /* if filled, then it's someone else */ + if (!client->user) { /* on first d.user this is our user, subsequent are someone else! */ DC_MR(program->users); /* don't need DC_IN_PROGRESS, we have ref in cl */ program->users[program->users_length++] = client->user = dc_user_init(); client->user->status |= DC_INCOMPLETE; /* when ->disc is set, it's complete */ @@ -74,68 +94,81 @@ signed char dc_json_cb (struct lejp_ctx * ctx, char /* enum lejp_callbacks */ re default: break; } + return '\0'; /* because we use same checks for parsing other users GUILD_MEMBER_UPDATE */ } - if ((cp = startswith(ctx->path, dc_json_paths[DC_JSON_FRIEND]))) { - if (!client->user) /* for the client user */ - client->user = dc_user_init(); - if (path == DC_JSON_FRIEND && reason == LEJPCB_OBJECT_START) { - dc_user_free(pass->api_io.user); /* for the newly found friend (-; */ - pass->api_io.user = dc_user_init(); + if (startswith(ctx->path, dc_json_paths[DC_JSON_FRIEND]) || startswith(ctx->path, dc_json_paths[DC_JSON_ME])) { + if ((path == DC_JSON_FRIEND || path == DC_JSON_ME) && reason == LEJPCB_OBJECT_START) { + /* dc_user_free(pass->api_io.user); */ /* parser branches MUST ALWAYS set to 0 */ + pass->api_io.user = dc_user_init(); /* and possibly free api_io members after! */ pass->api_io.user->status |= DC_IN_PROGRESS; /* if we never get here again */ } - if (path == DC_JSON_FRIEND && reason == LEJPCB_OBJECT_END) { - if (dc_find_user(client->users, client->users_length, pass->api_io.user->id) != pass->api_io.user) - dc_user_free(pass->api_io.user); /* already have */ - else { + if (path == DC_JSON_FRIEND && reason == LEJPCB_OBJECT_END) { /* if that's a friend */ + pass->api_io.user = dc_addr_user(program, DC_ISAE(program->users), pass->api_io.user, DC_MAY_FREE); + if (!dc_find_user(client->users, client->users_length, pass->api_io.user->id)) { fprintf(stderr, "got a new friend (: %s#%d %llu\n", pass->api_io.user->username ? pass->api_io.user->username : "NULL", pass->api_io.user->discriminator, pass->api_io.user->id); - pass->api_io.user = dc_addr_user(program, DC_ISAE(client->users), pass->api_io.user, DC_MAY_FREE); + pass->api_io.user = dc_add_user(DC_ISAE(client->users), pass->api_io.user, DC_UNSET); /* already reported, already inserted in program, nof */ pass->api_io.user->status &= ~(DC_IN_PROGRESS); } pass->api_io.user = NULL; /* we're done, NULL it or PROBLEMS WILL APPEAR!!! */ } - if (reason & LEJP_FLAG_CB_IS_VALUE && cp[0] && pass->api_io.user) - switch(cp[1]) { - case 'u': /* sername */ - free(pass->api_io.user->username); /* yup, we don't trust serv */ - pass->api_io.user->username = strdup(ctx->buf); + if (path == DC_JSON_ME && reason == LEJPCB_OBJECT_END) { /* if it was just a user */ + pass->api_io.user = dc_addr_user(program, DC_ISAE(program->users), pass->api_io.user, DC_MAY_FREE); + pass->api_io.user = NULL; + } + if (reason & LEJP_FLAG_CB_IS_VALUE && pass->api_io.user) + switch(path) { + case DC_JSON_FRIEND_USERNAME: + if (!pass->api_io.user->username) /* yup, we don't trust serv */ + pass->api_io.user->username = strdup(ctx->buf); break; - case 'i': /* d */ + case DC_JSON_FRIEND_ID: pass->api_io.user->id = strtoll(ctx->buf, NULL, 10); break; - case 'd': /* iscriminator */ + case DC_JSON_FRIEND_DISCRIMINATOR: pass->api_io.user->discriminator = atoi(ctx->buf); break; /* yeah, we don't care about nicknames */ + default: + break; } } - if ((cp = startswith(ctx->path, dc_json_paths[DC_JSON_DM]))) { + if (startswith(ctx->path, dc_json_paths[DC_JSON_DM])) { if (!client->guilds_length) { DC_MR(program->guilds); DC_MR(client->guilds); program->guilds[program->guilds_length++] = client->guilds[0] = dc_guild_init(); - client->guilds_length = 1; + client->guilds_length = 1; /* we don't dc_add_guild because id=0 */ client->guilds[0]->name = strdup("Direct messages"); /* TODO: gettext for i18n */ client->guilds[0]->client = client; } if (path == DC_JSON_DM && reason == LEJPCB_OBJECT_START) { fprintf(stderr, "new DM start parsing object\n"); - dc_channel_free(pass->api_io.channel); - pass->api_io.channel = dc_channel_init(); + /* dc_channel_free(pass->api_io.channel); */ /* parser branches must not leave */ + pass->api_io.channel = dc_channel_init(); /* behind any content in api_io */ pass->api_io.channel->status |= DC_IN_PROGRESS; pass->api_io.channel->guild = client->guilds[0]; } if (path == DC_JSON_DM && reason == LEJPCB_OBJECT_END) { fprintf(stderr, "new DM (:\n"); - if (dc_find_ll_channel(client->guilds[0]->channel /* checks for NULL */, pass->api_io.channel->id)) { - dc_channel_free(pass->api_io.channel); - goto already_have; - } struct dc_channel ** channel = &client->guilds[0]->channel; /* 1. guild = DMs */ while (*channel) channel = &(*channel)->next; pass->api_io.channel->status &= ~(DC_IN_PROGRESS); - pass->api_io.channel = *channel = dc_addr_channel(program, DC_ISAN, pass->api_io.channel, DC_MAY_FREE); -already_have: - pass->api_io.channel->next = NULL; + free(pass->api_io.channel->name); + pass->api_io.channel->name = strdup(""); + for (size_t i = 0; i < pass->api_io.channel->users_length; i++) { + pass->api_io.channel->name = realloc(pass->api_io.channel->name, strlen(pass->api_io.channel->name)+strlen(pass->api_io.channel->users[i]->username)+1+2+1+4); + char buf[64]; + sprintf(buf, "#%d%s", pass->api_io.channel->users[i]->discriminator, i < pass->api_io.channel->users_length ? ", " : ""); + strcat(pass->api_io.channel->name, pass->api_io.channel->users[i]->username); + strcat(pass->api_io.channel->name, buf); + } + pass->api_io.channel = *channel = dc_addr_channel(program, DC_ISAN, pass->api_io.channel, DC_MAY_FREE | DC_REPLACE); + if ((*channel = dc_find_ll_channel(client->guilds[0]->channel, pass->api_io.channel->id))) + if (*channel != pass->api_io.channel) { + dc_channel_free(pass->api_io.channel, DC_REPLACE); + memmove(*channel, pass->api_io.channel, sizeof(**channel)); + } + pass->api_io.channel->guild = client->guilds[0]; pass->api_io.channel = NULL; /* we're done, NULL it or PROBLEMS WILL APPEAR!!! */ } if (reason & LEJP_FLAG_CB_IS_VALUE && pass->api_io.channel) @@ -150,19 +183,19 @@ already_have: break; } } - if ((cp = startswith(ctx->path, dc_json_paths[DC_JSON_DM_USER]))) { - if (path == DC_JSON_DM_USER && reason == LEJPCB_OBJECT_START) { - if (!pass->api_io.user) - pass->api_io.user = dc_user_init(); + if (startswith(ctx->path, dc_json_paths[DC_JSON_DM_USER])) { /* we don't DC_REPLACE here bcoz */ + if (path == DC_JSON_DM_USER && reason == LEJPCB_OBJECT_START) { /* users never update. */ + pass->api_io.user = dc_user_init(); pass->api_io.user->status |= DC_IN_PROGRESS; } if (path == DC_JSON_DM_USER && reason == LEJPCB_OBJECT_END) { fprintf(stderr, "new DM participant (:\n"); - pass->api_io.user = dc_addr_user(program, DC_ISAE(pass->api_io.channel->users), pass->api_io.user, DC_MAY_FREE); - pass->api_io.user->status &= ~(DC_IN_PROGRESS); + pass->api_io.user = dc_addr_user(program, DC_ISAE(program->users), pass->api_io.user, DC_MAY_FREE); /* add to program ISA or find existing */ + pass->api_io.user = dc_addr_user(program, DC_ISAE(pass->api_io.channel->users), pass->api_io.user, DC_UNSET); /* do not free, because it's already in program */ + pass->api_io.user->status &= ~(DC_IN_PROGRESS); /* somehow invalid 4 byte rw? */ pass->api_io.user = NULL; /* we're done, NULL it or PROBLEMS WILL APPEAR!!! */ } - if (reason & LEJP_FLAG_CB_IS_VALUE && cp[0] && pass->api_io.channel) + if (reason & LEJP_FLAG_CB_IS_VALUE && pass->api_io.channel) switch (path) { case DC_JSON_DM_USER_ID: pass->api_io.user->id = strtoull(ctx->buf, NULL, 10); @@ -178,24 +211,20 @@ already_have: break; } } - if (pass->packet == DC_NONE) /* useless to do anything BELOW if we haven't recvd packet type */ - return '\0'; - if (reason == LEJPCB_COMPLETE) { /* NOT USED FOR ANYTHING, REMOVE */ - if (!pass->parsing_status) { /* some stuff is fully handled in switch (pass->pkt) */ - pass->packet = DC_NONE; - return '\0'; + if (startswith(ctx->path, dc_json_paths[DC_JSON_GUILD])) { + if (path == DC_JSON_GUILD && reason == LEJPCB_OBJECT_START) { + pass->api_io.guild = dc_guild_init(); + pass->api_io.guild->status |= DC_IN_PROGRESS; + } + if (path == DC_JSON_GUILD && reason == LEJPCB_OBJECT_END) { + fprintf(stderr, "new guild"); + pass->api_io.guild = dc_addr_guild(program, DC_ISAE(program->guilds), pass->api_io.guild, DC_MAY_FREE); } - *pass->parsing_status &= ~DC_IN_PROGRESS; - pass->api_io.status |= DC_FROM_LWS; - // dc_api_io(pass->api_io); /* WE'RE NOT DOING IT THIS WAY */ - pass->api_io.status &= ~DC_FROM_LWS; - pass->parsing_status = NULL; - pass->packet = DC_NONE; - return '\0'; } + if (pass->packet == DC_NONE) /* useless to do anything BELOW if we haven't recvd packet type */ + return '\0'; switch (pass->packet) { /* we fill in structs, set DC_INCOMPLETE and pass->parsing_status */ - case DC_PING: - dc_handle_ping(pass->api_io, pass /* this is just so it's not NULL */); + case DC_MESSAGE_CREATE: break; default: break; @@ -296,12 +325,16 @@ static int dc_lws_cb (struct lws * wsi, enum lws_callback_reasons rs, void * us, pass->api_io.status &= ~DC_FROM_LWS; } pass->api_io.client->pass = NULL; - dc_lws_pass_free(pass); /* called at the final time us ptr and wsi is still */ + dc_lws_pass_free(pass, DC_UNSET); /* called the last time us ptr & wsi r still */ break; /* accessible - we can free the struct that was passed in as a heap ptr */ case LWS_CALLBACK_WSI_CREATE: /* first - outermost - call with wsi present */ - if (pass->api_io.client && pass->api_io.status & DC_SET_PASS) { - fprintf(stderr, "pass->api_io.client->pass = pass\n"); - pass->api_io.client->pass = pass; + if (pass->api_io.client) { + if (pass->api_io.status & DC_SET_PASS) { + fprintf(stderr, "pass->api_io.client->pass = pass\n"); + pass->api_io.client->pass = pass; + } + if (pass->api_io.status & DC_SET_WS_ACTIVE) /* how to get if wsi is ws */ + pass->api_io.client->status |= DC_WS_ACTIVE; /* or http? IDFK. */ } break; case LWS_CALLBACK_CLIENT_ESTABLISHED: /* websocket established */ @@ -323,6 +356,7 @@ static int dc_lws_cb (struct lws * wsi, enum lws_callback_reasons rs, void * us, fprintf(stderr, "with additional message: %.*s\n", len-2, (char *) in+2); break; case LWS_CALLBACK_CLIENT_CLOSED: /* websocket closed */ + pass->api_io.client->disconnect_time = time(NULL); DC_API_IO_GC(pass->api_io); /* frees all unfinished parsing objects */ pass->packet = DC_NONE; pass->api_io.status = DC_NET_ERROR; @@ -344,19 +378,20 @@ static int dc_lws_cb (struct lws * wsi, enum lws_callback_reasons rs, void * us, lejp_parse(&pass->json_ctx, (const unsigned char *) in, len); break; case LWS_CALLBACK_CLIENT_WRITEABLE: /* invoke with lws_callback_on_writeable(wsi) 4 ws */ - if (!pass->api_io.client) + if (!pass->api_io.client) /* we empty all payloads from 0 to finish */ break; - if (!pass->api_io.client->payloads_length) - break; - char * body = pass->api_io.client->payloads[--pass->api_io.client->payloads_length]->body; - size_t length = pass->api_io.client->payloads[pass->api_io.client->payloads_length]->length; - fprintf(stderr, "going to write to ws: %.*s\n", length, body); - if (lws_write(wsi, (unsigned char *) body, length, LWS_WRITE_BINARY) != (int) length) { /* body already has LWS_PRE bytes allocated before it */ - fprintf(stderr, "ws lws_write failed!\n"); - return -1; + for (size_t i = 0; i < pass->api_io.client->payloads_length; i++) { + char * body = pass->api_io.client->payloads[i]->body; + size_t length = pass->api_io.client->payloads[i]->length; + fprintf(stderr, "going to write to ws: %.*s\n", length, body); + if (lws_write(wsi, (unsigned char *) body, length, LWS_WRITE_BINARY) != (int) length) { /* body already has LWS_PRE bytes allocated before it */ + fprintf(stderr, "ws lws_write failed!\n"); + return -1; + } + dc_payload_free(pass->api_io.client->payloads[i], DC_UNSET); + pass->api_io.client->payloads[i] = NULL; } - dc_payload_free(pass->api_io.client->payloads[pass->api_io.client->payloads_length]); - pass->api_io.client->payloads[pass->api_io.client->payloads_length] = NULL; /* dc_ws_pop(); */ + pass->api_io.client->payloads_length = 0; break; default: break; @@ -396,7 +431,7 @@ void dc_api_i (struct dc_api_io i) { /* this function does not call attached fun pass = calloc(1, sizeof(struct dc_lws_pass)); /* cb frees */ fprintf(stderr, "allocated pass at %p\n", (void *) pass); i.status |= DC_DESTROY_CB; /* so that lws_cb will call api on destroy - fin rq */ - pass->body_length = smprintf(&pass->body, DC_LOGIN_FORMAT, i.client->email, i.client->password); + pass->body_length = asprintf(&pass->body, DC_LOGIN_FORMAT, i.client->email, i.client->password); fprintf(stderr, "length: %u string: %s\n", pass->body_length, pass->body); i.type = DC_API_LOGIN_CB; i.status &= ~DC_SET_PASS; /* this is a HTTP request, pass is for websockets */ @@ -443,6 +478,9 @@ void dc_api_i (struct dc_api_io i) { /* this function does not call attached fun goto ws; /* we could call dc_api_i but that just fills stack */ break; case DC_API_WS: + if (getenv("DC_R")) + raise(SIGINT); + fprintf(stderr, "DC_API_WS called\n"); ws: memset(&info, 0, sizeof(info)); info.context = i.program->lws_context; @@ -460,7 +498,7 @@ void dc_api_i (struct dc_api_io i) { /* this function does not call attached fun info.local_protocol_name = info.protocol; pass = calloc(1, sizeof(struct dc_lws_pass)); i.type = DC_API_WS_CB; - i.status |= DC_SET_PASS; + i.status |= DC_SET_PASS | DC_SET_WS_ACTIVE; i.status &= ~DC_DESTROY_CB; /* this is only for http requests */ memcpy(&pass->api_io, &i, sizeof(i)); pass->api_io.pass = pass; @@ -469,7 +507,6 @@ void dc_api_i (struct dc_api_io i) { /* this function does not call attached fun strcpy(pass->headers[DC_LWS_AUTHORIZATION], i.client->authorization); fprintf(stderr, "starting websocket session\n"); i.status = DC_UNSET; /* we clear the status */ - i.client->status |= DC_WS_ACTIVE; if (!lws_client_connect_via_info(&info)) { i.client->status = DC_NET_ERROR; dc_api_stack(i); @@ -481,10 +518,15 @@ void dc_api_i (struct dc_api_io i) { /* this function does not call attached fun if (i.status & DC_NET_ERROR) { /* pass and pass->body were freed on _DESTROY */ fprintf(stderr, "websocket connection was closed by the remote host\n"); i.pass = NULL; /* pass was/will be freed on _DESTROY, discard it! */ + } else if (!i.client->authorization) { + fprintf(stderr, "what the fuck?! DC_API_WS_CB called without token!\n"); } else { /* commerce identify */ struct dc_payload * p = calloc(1, sizeof(struct dc_payload)); - p->length = smprintf(&p->body, DC_IDENTIFY_FORMAT, i.client->authorization, DC_INTENTS); /* body buffer to identify packet */ - dc_ws_stack(i.client, p); + p->length = asprintf(&p->body, dc_api_identify, i.client->authorization); /* body buffer to identify packet */ + dc_ws_stack(i.client, p, DC_NO_WRITE); + p = calloc(1, sizeof(struct dc_payload)); /* official client sends a */ + p->length = asprintf(&p->body, DC_WS_PING_FORMAT, (unsigned long long int) 0); + dc_ws_stack(i.client, p, DC_UNSET); /* right after identify. */ } dc_api_stack(i); /* cl.stat is either NET_ERROR 4 closed or error or OK 4 esta */ break; @@ -508,8 +550,11 @@ struct dc_api_io dc_api_o (struct dc_api_io i /* for ->program */) { if (i.program->lws_context) lws_service(i.program->lws_context, 0); for (size_t x = 0; x < i.program->clients_length; x++) { - if (i.program->clients[x]->status & DC_WS_ACTIVE && !i.program->clients[x]->pass) { + if (i.program->clients[x]->status & DC_WS_ACTIVE && !i.program->clients[x]->pass + && i.program->clients[x]->disconnect_time+DC_RECONNECT_DELAYclients[x]; + i.client->status &= ~DC_WS_ACTIVE; /* because we create a new WSI */ i.type = DC_API_WS; dc_api_i(i); } @@ -521,7 +566,7 @@ struct dc_api_io dc_api_o (struct dc_api_io i /* for ->program */) { /* dc_api_stack pop: */ if (i.program->api_ios_length) { memcpy(&o, (i.program->api_ios[--i.program->api_ios_length]), sizeof(o)); - dc_api_io_free(i.program->api_ios[i.program->api_ios_length]); + dc_api_io_free(i.program->api_ios[i.program->api_ios_length], DC_UNSET); i.program->api_ios[i.program->api_ios_length] = NULL; } -- cgit v1.2.3