From 7f5ceff688553c44fa556ce5338c78373573a5cf Mon Sep 17 00:00:00 2001 From: Florian Stinglmayr Date: Thu, 4 Jul 2019 19:25:38 +0200 Subject: [PATCH] full friends list support, add remove, and accept --- libdc/CMakeLists.txt | 2 + libdc/include/dc/account.h | 30 +++++++- libdc/include/dc/api.h | 17 +++++ libdc/src/account.c | 4 +- libdc/src/api-friends.c | 150 +++++++++++++++++++++++++++++++++++++ libdc/src/api-util.c | 53 +++++++++++++ libdc/src/api.c | 138 ---------------------------------- libdc/src/internal.h | 8 ++ ncdc/include/ncdc/cmds.h | 5 ++ ncdc/src/cmds.c | 25 ++++--- ncdc/src/friends.c | 113 +++++++++++++++++++++++++--- 11 files changed, 380 insertions(+), 165 deletions(-) create mode 100644 libdc/src/api-friends.c create mode 100644 libdc/src/api-util.c diff --git a/libdc/CMakeLists.txt b/libdc/CMakeLists.txt index 33514d0..6ec4911 100644 --- a/libdc/CMakeLists.txt +++ b/libdc/CMakeLists.txt @@ -17,6 +17,8 @@ SET(SOURCES "include/dc/util.h" "src/account.c" "src/api.c" + "src/api-friends.c" + "src/api-util.c" "src/apisync.c" "src/guild.c" "src/loop.c" diff --git a/libdc/include/dc/account.h b/libdc/include/dc/account.h index 8bf3367..a38e1fe 100644 --- a/libdc/include/dc/account.h +++ b/libdc/include/dc/account.h @@ -13,14 +13,35 @@ typedef enum { /* accountt is a mutual friend */ FRIEND_STATE_FRIEND = 1, - /* pending account, the other side hasn't accepted yet + /* pending friend request, the other side hasn't accepted yet */ - FRIEND_STATE_PENDING = 4, + FRIEND_STATE_PENDING = 3, } dc_account_friend_states; +/** + * Represents a given account within the discord system. To start your work, + * you will have to create an account object, give it email and password, and + * call the dc_api_authenticate() with it. This gives you a login token that + * you can use to call other API methods (such as dc_api_get_friends()) for + * that account. + * + * Accounts have a few important attributes that we store: + * * ID (or snowflake), a 64 bit ID of the user, that we store as a string. + * * username, a string that represents the accounts user name + * * discriminator, a number that differentiates users with the same name + * * email, for login accounts only + * * password, for login accounts only + * * friend_state, if the account is someone login account's friend, we store + * the relationship in this flag. See the dc_account_friend_state enum for + * details. + * And one compound attribute: + * * fullname, a combination of username and discriminator separated by the + * pound sign, e.g. nola#2457 + */ + dc_account_t dc_account_new(void); dc_account_t dc_account_new2(char const *email, char const *pass); -dc_account_t dc_account_from_fullid(char const *fullid); +dc_account_t dc_account_from_fullname(char const *fullid); void dc_account_set_email(dc_account_t a, char const *email); char const *dc_account_email(dc_account_t a); @@ -37,7 +58,7 @@ char const *dc_account_username(dc_account_t a); void dc_account_set_discriminator(dc_account_t a, char const *id); char const *dc_account_discriminator(dc_account_t a); -char const *dc_account_full_username(dc_account_t a); +char const *dc_account_fullname(dc_account_t a); void dc_account_set_token(dc_account_t a, char const *token); char const *dc_account_token(dc_account_t a); @@ -48,6 +69,7 @@ bool dc_account_has_token(dc_account_t a); void dc_account_set_friends(dc_account_t a, dc_account_t *ptr, size_t len); dc_account_t dc_account_nthfriend(dc_account_t a, size_t i); size_t dc_account_friends_size(dc_account_t a); + int dc_account_friend_state(dc_account_t a); void dc_account_set_friend_state(dc_account_t a, int state); diff --git a/libdc/include/dc/api.h b/libdc/include/dc/api.h index 80e4dfd..503abfd 100644 --- a/libdc/include/dc/api.h +++ b/libdc/include/dc/api.h @@ -70,4 +70,21 @@ bool dc_api_get_friends(dc_api_t api, dc_account_t login); */ bool dc_api_add_friend(dc_api_t api, dc_account_t login, dc_account_t friend); +/** + * Remove a given account as a friend to the friends list. Warning: The + * account ID (aka account snowflake) is required to perform this operation, + * so you cannot just do dc_account_from_fullname(). Suggestion: use an object + * from the actual friends list of the account login. + */ +bool dc_api_remove_friend(dc_api_t api, dc_account_t login, + dc_account_t friend); + +/** + * Accepts someone who has sent a friend request to you, as a friend. Warning: + * The object "friend" requires an account ID (aka snowflake) for this method + * to work. You should take this object perhaps from the "login"'s friend list. + */ +bool dc_api_accept_friend(dc_api_t api, dc_account_t login, + dc_account_t friend); + #endif diff --git a/libdc/src/account.c b/libdc/src/account.c index 7249de7..8a8a153 100644 --- a/libdc/src/account.c +++ b/libdc/src/account.c @@ -84,7 +84,7 @@ dc_account_t dc_account_new2(char const *email, char const *pass) return ptr; } -dc_account_t dc_account_from_fullid(char const *fullid) +dc_account_t dc_account_from_fullname(char const *fullid) { return_if_true(fullid == NULL, NULL); @@ -226,7 +226,7 @@ char const *dc_account_discriminator(dc_account_t a) return a->discriminator; } -char const *dc_account_full_username(dc_account_t a) +char const *dc_account_fullname(dc_account_t a) { return_if_true(a == NULL, NULL); return a->full; diff --git a/libdc/src/api-friends.c b/libdc/src/api-friends.c new file mode 100644 index 0000000..261530f --- /dev/null +++ b/libdc/src/api-friends.c @@ -0,0 +1,150 @@ +#include + +#include "internal.h" + +bool dc_api_get_friends(dc_api_t api, dc_account_t login) +{ + char const *url = "users/@me/relationships"; + json_t *reply = NULL, *c = NULL, *val = NULL; + bool ret = false; + size_t i = 0; + GPtrArray *f = g_ptr_array_new_with_free_func((GDestroyNotify)dc_unref); + + return_if_true(api == NULL, false); + return_if_true(login == NULL, false); + + reply = dc_api_call_sync(api, "GET", dc_account_token(login), url, NULL); + goto_if_true(reply == NULL, cleanup); + + goto_if_true(!json_is_array(reply), cleanup); + + json_array_foreach(reply, i, c) { + /* the return is an array of objects, with a "user" member + * type 1 is probably a friend + */ + val = json_object_get(c, "user"); + if (val == NULL) { + continue; + } + + dc_account_t a = dc_api_account_from_json(val); + if (a == NULL) { + continue; + } + + /* read the type also known as "typ" + */ + val = json_object_get(c, "type"); + if (val != NULL && json_is_integer(val)) { + int state = json_integer_value(val); + dc_account_set_friend_state(a, state); + } + + g_ptr_array_add(f, a); + } + + if (f->len == 0) { + /* me_irl :-( + */ + } + + dc_account_set_friends(login, (dc_account_t*)f->pdata, f->len); + ret = true; + +cleanup: + + g_ptr_array_free(f, FALSE); + json_decref(reply); + reply = NULL; + + return ret; +} + +bool dc_api_remove_friend(dc_api_t api, dc_account_t login, dc_account_t friend) +{ + char *url = NULL; + json_t *reply = NULL, *post = NULL; + bool ret = false; + + return_if_true(api == NULL, false); + return_if_true(login == NULL || friend == NULL, false); + return_if_true(dc_account_id(friend) == NULL, false); + + asprintf(&url, "users/@me/relationships/%s", dc_account_id(friend)); + + post = dc_api_account_to_json(friend); + return_if_true(post == NULL, false); + + reply = dc_api_call_sync(api, "DELETE", dc_account_token(login), url, post); + /* if no data comes back, then the whole thing was a success + */ + goto_if_true(reply != NULL, cleanup); + + ret = true; + +cleanup: + + free(url); + json_decref(post); + json_decref(reply); + + return ret; +} + +bool dc_api_accept_friend(dc_api_t api, dc_account_t login, dc_account_t friend) +{ + char *url = NULL; + json_t *reply = NULL, *post = NULL; + bool ret = false; + + return_if_true(api == NULL, false); + return_if_true(login == NULL || friend == NULL, false); + return_if_true(dc_account_id(friend) == NULL, false); + + asprintf(&url, "users/@me/relationships/%s", dc_account_id(friend)); + + post = dc_api_account_to_json(friend); + return_if_true(post == NULL, false); + + reply = dc_api_call_sync(api, "PUT", dc_account_token(login), url, post); + /* no data = successful + */ + goto_if_true(reply != NULL, cleanup); + + ret = true; + +cleanup: + + free(url); + json_decref(post); + json_decref(reply); + + return ret; +} + +bool dc_api_add_friend(dc_api_t api, dc_account_t login, dc_account_t friend) +{ + char const *url = "users/@me/relationships"; + json_t *reply = NULL, *post = NULL; + bool ret = false; + + return_if_true(api == NULL, false); + return_if_true(login == NULL, false); + + post = dc_api_account_to_json(friend); + return_if_true(post == NULL, false); + + reply = dc_api_call_sync(api, "POST", dc_account_token(login), url, post); + /* apparently if no data comes back, then the whole thing was a success + */ + goto_if_true(reply != NULL, cleanup); + + ret = true; + +cleanup: + + json_decref(post); + json_decref(reply); + + return ret; +} diff --git a/libdc/src/api-util.c b/libdc/src/api-util.c new file mode 100644 index 0000000..d90223c --- /dev/null +++ b/libdc/src/api-util.c @@ -0,0 +1,53 @@ +#include + +#include "internal.h" + +dc_account_t dc_api_account_from_json(json_t *j) +{ + dc_account_t user = dc_account_new(); + json_t *val = NULL; + + goto_if_true(!json_is_object(j), error); + + val = json_object_get(j, "username"); + goto_if_true(val == NULL || !json_is_string(val), error); + dc_account_set_username(user, json_string_value(val)); + + val = json_object_get(j, "discriminator"); + goto_if_true(val == NULL || !json_is_string(val), error); + dc_account_set_discriminator(user, json_string_value(val)); + + val = json_object_get(j, "id"); + goto_if_true(val == NULL || !json_is_string(val), error); + dc_account_set_id(user, json_string_value(val)); + + return user; + +error: + + dc_unref(user); + return NULL; +} + +json_t *dc_api_account_to_json(dc_account_t a) +{ + json_t *j = NULL; + + return_if_true(a == NULL, NULL); + return_if_true(dc_account_username(a) == NULL || + dc_account_discriminator(a) == NULL, + NULL + ); + + j = json_object(); + return_if_true(j == NULL, NULL); + + json_object_set_new(j, "username", + json_string(dc_account_username(a)) + ); + json_object_set_new(j, "discriminator", + json_string(dc_account_discriminator(a)) + ); + + return j; +} diff --git a/libdc/src/api.c b/libdc/src/api.c index c37c949..724a877 100644 --- a/libdc/src/api.c +++ b/libdc/src/api.c @@ -322,56 +322,6 @@ cleanup: return ret; } -static dc_account_t dc_api_account_from_json(json_t *j) -{ - dc_account_t user = dc_account_new(); - json_t *val = NULL; - - goto_if_true(!json_is_object(j), error); - - val = json_object_get(j, "username"); - goto_if_true(val == NULL || !json_is_string(val), error); - dc_account_set_username(user, json_string_value(val)); - - val = json_object_get(j, "discriminator"); - goto_if_true(val == NULL || !json_is_string(val), error); - dc_account_set_discriminator(user, json_string_value(val)); - - val = json_object_get(j, "id"); - goto_if_true(val == NULL || !json_is_string(val), error); - dc_account_set_id(user, json_string_value(val)); - - return user; - -error: - - dc_unref(user); - return NULL; -} - -static json_t *dc_api_user_to_json(dc_account_t a) -{ - json_t *j = NULL; - - return_if_true(a == NULL, NULL); - return_if_true(dc_account_username(a) == NULL || - dc_account_discriminator(a) == NULL, - NULL - ); - - j = json_object(); - return_if_true(j == NULL, NULL); - - json_object_set_new(j, "username", - json_string(dc_account_username(a)) - ); - json_object_set_new(j, "discriminator", - json_string(dc_account_discriminator(a)) - ); - - return j; -} - bool dc_api_get_userinfo(dc_api_t api, dc_account_t login, dc_account_t user) { @@ -414,94 +364,6 @@ cleanup: return ret; } -bool dc_api_get_friends(dc_api_t api, dc_account_t login) -{ - char const *url = "users/@me/relationships"; - json_t *reply = NULL, *c = NULL, *val = NULL; - bool ret = false; - size_t i = 0; - GPtrArray *f = g_ptr_array_new_with_free_func((GDestroyNotify)dc_unref); - - return_if_true(api == NULL, false); - return_if_true(login == NULL, false); - - reply = dc_api_call_sync(api, "GET", dc_account_token(login), url, NULL); - goto_if_true(reply == NULL, cleanup); - - goto_if_true(!json_is_array(reply), cleanup); - - json_array_foreach(reply, i, c) { - /* the return is an array of objects, with a "user" member - * type 1 is probably a friend - */ - val = json_object_get(c, "user"); - if (val == NULL) { - continue; - } - - dc_account_t a = dc_api_account_from_json(val); - if (a == NULL) { - continue; - } - - /* read the type also known as "typ" - */ - val = json_object_get(c, "type"); - if (val != NULL && json_is_integer(val)) { - int state = json_integer_value(val); - dc_account_set_friend_state(a, state); - } - - g_ptr_array_add(f, a); - } - - if (f->len == 0) { - /* me_irl :-( - */ - } - - dc_account_set_friends(login, (dc_account_t*)f->pdata, f->len); - ret = true; - -cleanup: - - g_ptr_array_free(f, FALSE); - json_decref(reply); - reply = NULL; - - return ret; -} - -/** - * Add a given account as a friend to the friends list - */ -bool dc_api_add_friend(dc_api_t api, dc_account_t login, dc_account_t friend) -{ - char const *url = "users/@me/relationships"; - json_t *reply = NULL, *post = NULL; - bool ret = false; - - return_if_true(api == NULL, false); - return_if_true(login == NULL, false); - - post = dc_api_user_to_json(friend); - return_if_true(post == NULL, false); - - reply = dc_api_call_sync(api, "POST", dc_account_token(login), url, post); - /* apparently if no data comes back, then the whole thing was a success - */ - goto_if_true(reply != NULL, cleanup); - - ret = true; - -cleanup: - - json_decref(post); - json_decref(reply); - - return ret; -} - bool dc_api_get_userguilds(dc_api_t api, dc_account_t login, GPtrArray **out) { char const *url = "users/@me/guilds"; diff --git a/libdc/src/internal.h b/libdc/src/internal.h index 4229468..9c967c4 100644 --- a/libdc/src/internal.h +++ b/libdc/src/internal.h @@ -22,10 +22,18 @@ #include #include +#include +#include //#define DEBUG #define return_if_true(v,r) do { if (v) return r; } while(0) #define goto_if_true(v,l) do { if (v) goto l; } while(0) +/* These are internal helper methods, their ABI, and API stability + * is not garuanteed. So please beware + */ +json_t *dc_api_account_to_json(dc_account_t a); +dc_account_t dc_api_account_from_json(json_t *j); + #endif diff --git a/ncdc/include/ncdc/cmds.h b/ncdc/include/ncdc/cmds.h index a4956c6..d1a5ef1 100644 --- a/ncdc/include/ncdc/cmds.h +++ b/ncdc/include/ncdc/cmds.h @@ -19,6 +19,11 @@ bool ncdc_dispatch_deinit(void); bool ncdc_dispatch(ncdc_mainwindow_t n, wchar_t const *s); +/* find a given command in a list of commands, helpful if your command has + * sub commands. for example usage see the friends command + */ +ncdc_commands_t *ncdc_find_cmd(ncdc_commands_t *cmds, wchar_t const *name); + bool ncdc_cmd_friends(ncdc_mainwindow_t n, size_t ac, wchar_t **av); bool ncdc_cmd_login(ncdc_mainwindow_t n, size_t ac, wchar_t **av); bool ncdc_cmd_quit(ncdc_mainwindow_t n, size_t ac, wchar_t **av); diff --git a/ncdc/src/cmds.c b/ncdc/src/cmds.c index 65d1059..2396db3 100644 --- a/ncdc/src/cmds.c +++ b/ncdc/src/cmds.c @@ -89,26 +89,29 @@ bool ncdc_dispatch_deinit(void) return true; } +ncdc_commands_t *ncdc_find_cmd(ncdc_commands_t *cmds, wchar_t const *name) +{ + ncdc_commands_t *it = NULL; + + for (it = cmds; it->name != NULL; it++) { + if (wcscmp(it->name, name) == 0) { + return it; + } + } + + return NULL; +} + bool ncdc_dispatch(ncdc_mainwindow_t n, wchar_t const *s) { wchar_t **tokens = NULL; - size_t i = 0, tokenlen = 0; ncdc_commands_t *it = NULL; queue_item *item = NULL; tokens = w_tokenise(s); return_if_true(tokens == NULL, false); - tokenlen = wcslen(tokens[0]); - - for (i = 0; cmds[i].name != NULL; i++) { - if (wcsncmp(cmds[i].name, tokens[0], tokenlen) == 0) { - it = cmds+i; - break; - } - } - - if (it == NULL) { + if ((it = ncdc_find_cmd(cmds, tokens[0])) == NULL) { /* no such command */ LOG(n, L"error: no such command \"%ls\"", tokens[0]); diff --git a/ncdc/src/friends.c b/ncdc/src/friends.c index 7357274..a02f7a5 100644 --- a/ncdc/src/friends.c +++ b/ncdc/src/friends.c @@ -6,7 +6,7 @@ ncdc_cmd_friends_list(ncdc_mainwindow_t n, size_t ac, wchar_t **av) { bool ret = false; size_t i = 0; - wchar_t c = ' '; + char c = ' '; ret = dc_api_get_friends(api, current_account); if (!ret) { @@ -21,7 +21,7 @@ ncdc_cmd_friends_list(ncdc_mainwindow_t n, size_t ac, wchar_t **av) case FRIEND_STATE_PENDING: c = 'P'; break; default: c = ' '; break; } - LOG(n, L"%lc %s", c, dc_account_full_username(acc)); + LOG(n, L" %c %s", c, dc_account_fullname(acc)); } LOG(n, L"End of /FRIENDS list"); @@ -42,7 +42,7 @@ ncdc_cmd_friends_add(ncdc_mainwindow_t n, size_t ac, wchar_t **av) name = w_convert(av[1]); return_if_true(name == NULL, false); - friend = dc_account_from_fullid(name); + friend = dc_account_from_fullname(name); if (friend == NULL) { LOG(n, L"friends: add: invalid username given, use the full ID"); goto cleanup; @@ -53,7 +53,7 @@ ncdc_cmd_friends_add(ncdc_mainwindow_t n, size_t ac, wchar_t **av) goto cleanup; } - LOG(n, L"friends: add: request for friendship sent"); + LOG(n, L"friends: add: request for friendship sent to %s", name); ret = true; cleanup: @@ -64,9 +64,105 @@ cleanup: return ret; } +static bool +ncdc_cmd_friends_remove(ncdc_mainwindow_t n, size_t ac, wchar_t **av) +{ + char *name = NULL; + dc_account_t friend = NULL; + bool ret = false; + size_t i = 0; + + if (ac <= 1) { + return false; + } + + name = w_convert(av[1]); + return_if_true(name == NULL, false); + + for (i = 0; i < dc_account_friends_size(current_account); i++) { + dc_account_t cur = dc_account_nthfriend(current_account, i); + if (strcmp(dc_account_fullname(cur), name) == 0) { + friend = cur; + break; + } + } + + if (friend == NULL) { + LOG(n, L"friends: remove: no such friend in friend's list"); + goto cleanup; + } + + if (!dc_api_remove_friend(api, current_account, friend)) { + LOG(n, L"friends: remove: failed to remove friend"); + goto cleanup; + } + + LOG(n, L"friends: remove: friend %s removed", name); + ret = true; + +cleanup: + + free(name); + + return ret; +} + +static bool +ncdc_cmd_friends_accept(ncdc_mainwindow_t n, size_t ac, wchar_t **av) +{ + char *name = NULL; + dc_account_t friend = NULL; + bool ret = false; + size_t i = 0; + + if (ac <= 1) { + return false; + } + + name = w_convert(av[1]); + return_if_true(name == NULL, false); + + for (i = 0; i < dc_account_friends_size(current_account); i++) { + dc_account_t cur = dc_account_nthfriend(current_account, i); + if (strcmp(dc_account_fullname(cur), name) == 0 && + dc_account_friend_state(cur) == FRIEND_STATE_PENDING) { + friend = cur; + break; + } + } + + if (friend == NULL) { + LOG(n, L"friends: accept: no such pending friend on the list"); + goto cleanup; + } + + if (!dc_api_accept_friend(api, current_account, friend)) { + LOG(n, L"friends: accept: failed to accept friend"); + goto cleanup; + } + + LOG(n, L"friends: accept: friend %s accepted", name); + ret = true; + +cleanup: + + free(name); + + return ret; +} + +static ncdc_commands_t subcmds[] = { + { L"accept", ncdc_cmd_friends_accept }, + { L"add", ncdc_cmd_friends_add }, + { L"list", ncdc_cmd_friends_list }, + { L"remove", ncdc_cmd_friends_remove }, + { NULL, NULL } +}; + bool ncdc_cmd_friends(ncdc_mainwindow_t n, size_t ac, wchar_t **av) { wchar_t *subcmd = NULL; + ncdc_commands_t *it = NULL; if (current_account == NULL || !dc_account_has_token(current_account)) { @@ -83,13 +179,10 @@ bool ncdc_cmd_friends(ncdc_mainwindow_t n, size_t ac, wchar_t **av) --ac; ++av; - if (wcscmp(subcmd, L"list") == 0) { - return ncdc_cmd_friends_list(n, ac, av); - } else if (wcscmp(subcmd, L"add") == 0) { - return ncdc_cmd_friends_add(n, ac, av); - } else { + if ((it = ncdc_find_cmd(subcmds, subcmd)) == NULL) { + LOG(n, L"friends: no such subcommand \"%ls\"", subcmd); return false; } - return true; + return it->handler(n, ac, av); }