mirror of
https://codeberg.org/grunfink/snac2.git
synced 2024-11-09 19:50:26 +03:00
Implement Mastodon PATCH endpoint for account profile updates
This commit is contained in:
parent
0e21d35e80
commit
81cf309e4d
27
data.c
27
data.c
@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int user_persist(snac *snac)
|
||||||
|
/* store user */
|
||||||
|
{
|
||||||
|
xs *fn = xs_fmt("%s/user.json", snac->basedir);
|
||||||
|
xs *bfn = xs_fmt("%s.bak", fn);
|
||||||
|
FILE *f;
|
||||||
|
|
||||||
|
rename(fn, bfn);
|
||||||
|
|
||||||
|
if ((f = fopen(fn, "w")) != NULL) {
|
||||||
|
xs_json_dump(snac->config, 4, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
rename(bfn, fn);
|
||||||
|
|
||||||
|
history_del(snac, "timeline.html_");
|
||||||
|
|
||||||
|
xs *a_msg = msg_actor(snac);
|
||||||
|
xs *u_msg = msg_update(snac, a_msg);
|
||||||
|
|
||||||
|
enqueue_message(snac, u_msg);
|
||||||
|
enqueue_verify_links(snac);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
double mtime_nl(const char *fn, int *n_link)
|
double mtime_nl(const char *fn, int *n_link)
|
||||||
/* returns the mtime and number of links of a file or directory, or 0.0 */
|
/* returns the mtime and number of links of a file or directory, or 0.0 */
|
||||||
|
22
html.c
22
html.c
@ -3334,27 +3334,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
|
|||||||
snac.config = xs_dict_set(snac.config, "passwd", pw);
|
snac.config = xs_dict_set(snac.config, "passwd", pw);
|
||||||
}
|
}
|
||||||
|
|
||||||
xs *fn = xs_fmt("%s/user.json", snac.basedir);
|
user_persist(&snac);
|
||||||
xs *bfn = xs_fmt("%s.bak", fn);
|
|
||||||
FILE *f;
|
|
||||||
|
|
||||||
rename(fn, bfn);
|
|
||||||
|
|
||||||
if ((f = fopen(fn, "w")) != NULL) {
|
|
||||||
xs_json_dump(snac.config, 4, f);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
rename(bfn, fn);
|
|
||||||
|
|
||||||
history_del(&snac, "timeline.html_");
|
|
||||||
|
|
||||||
xs *a_msg = msg_actor(&snac);
|
|
||||||
xs *u_msg = msg_update(&snac, a_msg);
|
|
||||||
|
|
||||||
enqueue_message(&snac, u_msg);
|
|
||||||
|
|
||||||
enqueue_verify_links(&snac);
|
|
||||||
|
|
||||||
status = HTTP_STATUS_SEE_OTHER;
|
status = HTTP_STATUS_SEE_OTHER;
|
||||||
}
|
}
|
||||||
|
10
httpd.c
10
httpd.c
@ -360,6 +360,16 @@ void httpd_connection(FILE *f)
|
|||||||
payload, p_size, &body, &b_size, &ctype);
|
payload, p_size, &body, &b_size, &ctype);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(method, "PATCH") == 0) {
|
||||||
|
|
||||||
|
#ifndef NO_MASTODON_API
|
||||||
|
if (status == 0)
|
||||||
|
status = mastoapi_patch_handler(req, q_path,
|
||||||
|
payload, p_size, &body, &b_size, &ctype);
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
if (strcmp(method, "OPTIONS") == 0) {
|
if (strcmp(method, "OPTIONS") == 0) {
|
||||||
|
320
mastoapi.c
320
mastoapi.c
@ -1150,6 +1150,101 @@ int process_auth_token(snac *snac, const xs_dict *req)
|
|||||||
return logged_in;
|
return logged_in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void credentials_get(char **body, char **ctype, int *status, snac snac)
|
||||||
|
{
|
||||||
|
xs *acct = xs_dict_new();
|
||||||
|
|
||||||
|
acct = xs_dict_append(acct, "id", snac.md5);
|
||||||
|
acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
|
||||||
|
acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
|
||||||
|
acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
|
||||||
|
acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
|
||||||
|
acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
|
||||||
|
acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
|
||||||
|
acct = xs_dict_append(acct, "url", snac.actor);
|
||||||
|
acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
|
||||||
|
acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
|
||||||
|
|
||||||
|
xs *src = xs_json_loads("{\"privacy\":\"public\","
|
||||||
|
"\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
|
||||||
|
/* some apps take the note from the source object */
|
||||||
|
src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
|
||||||
|
src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
|
||||||
|
|
||||||
|
const xs_str *cw = xs_dict_get(snac.config, "cw");
|
||||||
|
src = xs_dict_set(src, "sensitive",
|
||||||
|
strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
|
||||||
|
|
||||||
|
src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
|
||||||
|
|
||||||
|
xs *avatar = NULL;
|
||||||
|
const char *av = xs_dict_get(snac.config, "avatar");
|
||||||
|
|
||||||
|
if (xs_is_null(av) || *av == '\0')
|
||||||
|
avatar = xs_fmt("%s/susie.png", srv_baseurl);
|
||||||
|
else
|
||||||
|
avatar = xs_dup(av);
|
||||||
|
|
||||||
|
acct = xs_dict_append(acct, "avatar", avatar);
|
||||||
|
acct = xs_dict_append(acct, "avatar_static", avatar);
|
||||||
|
|
||||||
|
xs *header = NULL;
|
||||||
|
const char *hd = xs_dict_get(snac.config, "header");
|
||||||
|
|
||||||
|
if (!xs_is_null(hd))
|
||||||
|
header = xs_dup(hd);
|
||||||
|
else
|
||||||
|
header = xs_fmt("%s/header.png", srv_baseurl);
|
||||||
|
|
||||||
|
acct = xs_dict_append(acct, "header", header);
|
||||||
|
acct = xs_dict_append(acct, "header_static", header);
|
||||||
|
|
||||||
|
const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
|
||||||
|
if (xs_type(metadata) == XSTYPE_DICT) {
|
||||||
|
xs *fields = xs_list_new();
|
||||||
|
const xs_str *k;
|
||||||
|
const xs_str *v;
|
||||||
|
|
||||||
|
xs_dict *val_links = snac.links;
|
||||||
|
if (xs_is_null(val_links))
|
||||||
|
val_links = xs_stock(XSTYPE_DICT);
|
||||||
|
|
||||||
|
int c = 0;
|
||||||
|
while (xs_dict_next(metadata, &k, &v, &c)) {
|
||||||
|
xs *val_date = NULL;
|
||||||
|
|
||||||
|
const xs_number *verified_time = xs_dict_get(val_links, v);
|
||||||
|
if (xs_type(verified_time) == XSTYPE_NUMBER) {
|
||||||
|
time_t t = xs_number_get(verified_time);
|
||||||
|
|
||||||
|
if (t > 0)
|
||||||
|
val_date = xs_str_utctime(t, ISO_DATE_SPEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
xs *d = xs_dict_new();
|
||||||
|
|
||||||
|
d = xs_dict_append(d, "name", k);
|
||||||
|
d = xs_dict_append(d, "value", v);
|
||||||
|
d = xs_dict_append(d, "verified_at",
|
||||||
|
xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
|
||||||
|
|
||||||
|
fields = xs_list_append(fields, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
acct = xs_dict_set(acct, "fields", fields);
|
||||||
|
/* some apps take the fields from the source object */
|
||||||
|
src = xs_dict_set(src, "fields", fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
acct = xs_dict_append(acct, "source", src);
|
||||||
|
acct = xs_dict_append(acct, "followers_count", xs_stock(0));
|
||||||
|
acct = xs_dict_append(acct, "following_count", xs_stock(0));
|
||||||
|
acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
|
||||||
|
|
||||||
|
*body = xs_json_dumps(acct, 4);
|
||||||
|
*ctype = "application/json";
|
||||||
|
*status = HTTP_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int mastoapi_get_handler(const xs_dict *req, const char *q_path,
|
int mastoapi_get_handler(const xs_dict *req, const char *q_path,
|
||||||
char **body, int *b_size, char **ctype)
|
char **body, int *b_size, char **ctype)
|
||||||
@ -1168,88 +1263,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
|
|||||||
|
|
||||||
if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
|
if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
|
||||||
if (logged_in) {
|
if (logged_in) {
|
||||||
xs *acct = xs_dict_new();
|
credentials_get(body, ctype, &status, snac1);
|
||||||
|
|
||||||
acct = xs_dict_append(acct, "id", snac1.md5);
|
|
||||||
acct = xs_dict_append(acct, "username", xs_dict_get(snac1.config, "uid"));
|
|
||||||
acct = xs_dict_append(acct, "acct", xs_dict_get(snac1.config, "uid"));
|
|
||||||
acct = xs_dict_append(acct, "display_name", xs_dict_get(snac1.config, "name"));
|
|
||||||
acct = xs_dict_append(acct, "created_at", xs_dict_get(snac1.config, "published"));
|
|
||||||
acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
|
|
||||||
acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio"));
|
|
||||||
acct = xs_dict_append(acct, "url", snac1.actor);
|
|
||||||
acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
|
|
||||||
acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot"));
|
|
||||||
|
|
||||||
xs *src = xs_json_loads("{\"privacy\":\"public\","
|
|
||||||
"\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
|
|
||||||
acct = xs_dict_append(acct, "source", src);
|
|
||||||
|
|
||||||
xs *avatar = NULL;
|
|
||||||
const char *av = xs_dict_get(snac1.config, "avatar");
|
|
||||||
|
|
||||||
if (xs_is_null(av) || *av == '\0')
|
|
||||||
avatar = xs_fmt("%s/susie.png", srv_baseurl);
|
|
||||||
else
|
|
||||||
avatar = xs_dup(av);
|
|
||||||
|
|
||||||
acct = xs_dict_append(acct, "avatar", avatar);
|
|
||||||
acct = xs_dict_append(acct, "avatar_static", avatar);
|
|
||||||
|
|
||||||
xs *header = NULL;
|
|
||||||
const char *hd = xs_dict_get(snac1.config, "header");
|
|
||||||
|
|
||||||
if (!xs_is_null(hd))
|
|
||||||
header = xs_dup(hd);
|
|
||||||
else
|
|
||||||
header = xs_fmt("%s/header.png", srv_baseurl);
|
|
||||||
|
|
||||||
acct = xs_dict_append(acct, "header", header);
|
|
||||||
acct = xs_dict_append(acct, "header_static", header);
|
|
||||||
|
|
||||||
const xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
|
|
||||||
if (xs_type(metadata) == XSTYPE_DICT) {
|
|
||||||
xs *fields = xs_list_new();
|
|
||||||
const xs_str *k;
|
|
||||||
const xs_str *v;
|
|
||||||
|
|
||||||
xs_dict *val_links = snac1.links;
|
|
||||||
if (xs_is_null(val_links))
|
|
||||||
val_links = xs_stock(XSTYPE_DICT);
|
|
||||||
|
|
||||||
int c = 0;
|
|
||||||
while (xs_dict_next(metadata, &k, &v, &c)) {
|
|
||||||
xs *val_date = NULL;
|
|
||||||
|
|
||||||
const xs_number *verified_time = xs_dict_get(val_links, v);
|
|
||||||
if (xs_type(verified_time) == XSTYPE_NUMBER) {
|
|
||||||
time_t t = xs_number_get(verified_time);
|
|
||||||
|
|
||||||
if (t > 0)
|
|
||||||
val_date = xs_str_utctime(t, ISO_DATE_SPEC);
|
|
||||||
}
|
|
||||||
|
|
||||||
xs *d = xs_dict_new();
|
|
||||||
|
|
||||||
d = xs_dict_append(d, "name", k);
|
|
||||||
d = xs_dict_append(d, "value", v);
|
|
||||||
d = xs_dict_append(d, "verified_at",
|
|
||||||
xs_type(val_date) == XSTYPE_STRING && *val_date ?
|
|
||||||
val_date : xs_stock(XSTYPE_NULL));
|
|
||||||
|
|
||||||
fields = xs_list_append(fields, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
acct = xs_dict_set(acct, "fields", fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
acct = xs_dict_append(acct, "followers_count", xs_stock(0));
|
|
||||||
acct = xs_dict_append(acct, "following_count", xs_stock(0));
|
|
||||||
acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
|
|
||||||
|
|
||||||
*body = xs_json_dumps(acct, 4);
|
|
||||||
*ctype = "application/json";
|
|
||||||
status = HTTP_STATUS_OK;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
|
status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
|
||||||
@ -3077,6 +3091,148 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
|
||||||
|
/* Store header or avatar */
|
||||||
|
{
|
||||||
|
if (data != NULL) {
|
||||||
|
if (xs_type(data) == XSTYPE_LIST) {
|
||||||
|
const char *fn = xs_list_get(data, 0);
|
||||||
|
|
||||||
|
if (fn && *fn) {
|
||||||
|
const char *ext = strrchr(fn, '.');
|
||||||
|
/* Mona iOS sends JPG file as application/octet-stream with filename "header"
|
||||||
|
* Make sure we have a unique file name, otherwise updated images will not be
|
||||||
|
* loaded by clients.
|
||||||
|
*/
|
||||||
|
if (ext == NULL || strcmp(fn, key) == 0) {
|
||||||
|
fn = random_str();
|
||||||
|
ext = ".jpg";
|
||||||
|
}
|
||||||
|
xs *hash = xs_md5_hex(fn, strlen(fn));
|
||||||
|
xs *id = xs_fmt("%s%s", hash, ext);
|
||||||
|
xs *url = xs_fmt("%s/s/%s", snac->actor, id);
|
||||||
|
int fo = xs_number_get(xs_list_get(data, 1));
|
||||||
|
int fs = xs_number_get(xs_list_get(data, 2));
|
||||||
|
|
||||||
|
/* store */
|
||||||
|
static_put(snac, id, payload + fo, fs);
|
||||||
|
|
||||||
|
snac->config = xs_dict_set(snac->config, key, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
|
||||||
|
const char *payload, int p_size,
|
||||||
|
char **body, int *b_size, char **ctype)
|
||||||
|
/* Handle profile updates */
|
||||||
|
{
|
||||||
|
(void)p_size;
|
||||||
|
(void)b_size;
|
||||||
|
|
||||||
|
if (!xs_startswith(q_path, "/api/v1/"))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int status = HTTP_STATUS_NOT_FOUND;
|
||||||
|
xs *args = NULL;
|
||||||
|
const char *i_ctype = xs_dict_get(req, "content-type");
|
||||||
|
|
||||||
|
if (i_ctype && xs_startswith(i_ctype, "application/json")) {
|
||||||
|
if (!xs_is_null(payload))
|
||||||
|
args = xs_json_loads(payload);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
args = xs_dup(xs_dict_get(req, "p_vars"));
|
||||||
|
|
||||||
|
if (args == NULL)
|
||||||
|
return HTTP_STATUS_BAD_REQUEST;
|
||||||
|
|
||||||
|
xs *cmd = xs_replace_n(q_path, "/api", "", 1);
|
||||||
|
|
||||||
|
snac snac = {0};
|
||||||
|
int logged_in = process_auth_token(&snac, req);
|
||||||
|
|
||||||
|
if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
|
||||||
|
/* Update user profile fields */
|
||||||
|
if (logged_in) {
|
||||||
|
/*
|
||||||
|
xs_str *dump = xs_json_dumps(args, 4);
|
||||||
|
printf("%s\n\n", dump);
|
||||||
|
*/
|
||||||
|
int c = 0;
|
||||||
|
const xs_str *k;
|
||||||
|
const xs_val *v;
|
||||||
|
const xs_str *field_name = NULL;
|
||||||
|
xs_dict *new_fields = xs_dict_new();
|
||||||
|
while (xs_dict_next(args, &k, &v, &c)) {
|
||||||
|
if (strcmp(k, "display_name") == 0) {
|
||||||
|
if (v != NULL)
|
||||||
|
snac.config = xs_dict_set(snac.config, "name", v);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "note") == 0) {
|
||||||
|
if (v != NULL)
|
||||||
|
snac.config = xs_dict_set(snac.config, "bio", v);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "bot") == 0) {
|
||||||
|
if (v != NULL)
|
||||||
|
snac.config = xs_dict_set(snac.config, "bot",
|
||||||
|
strcmp(v, "true") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "source[sensitive]") == 0) {
|
||||||
|
if (v != NULL)
|
||||||
|
snac.config = xs_dict_set(snac.config, "cw",
|
||||||
|
strcmp(v, "true") == 0 ? "open" : "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "source[privacy]") == 0) {
|
||||||
|
if (v != NULL)
|
||||||
|
snac.config = xs_dict_set(snac.config, "private",
|
||||||
|
strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "header") == 0) {
|
||||||
|
persist_image("header", v, payload, &snac);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (strcmp(k, "avatar") == 0) {
|
||||||
|
persist_image("avatar", v, payload, &snac);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (xs_starts_and_ends("fields_attributes", k, "[name]")) {
|
||||||
|
field_name = strcmp(v, "") != 0 ? v : NULL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (xs_starts_and_ends("fields_attributes", k, "[value]")) {
|
||||||
|
if (field_name != NULL) {
|
||||||
|
new_fields = xs_dict_set(new_fields, field_name, v);
|
||||||
|
snac.config = xs_dict_set(snac.config, "metadata", new_fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Persist profile */
|
||||||
|
if (user_persist(&snac) == 0)
|
||||||
|
credentials_get(body, ctype, &status, snac);
|
||||||
|
else
|
||||||
|
status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
status = HTTP_STATUS_UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* user cleanup */
|
||||||
|
if (logged_in)
|
||||||
|
user_free(&snac);
|
||||||
|
|
||||||
|
srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void mastoapi_purge(void)
|
void mastoapi_purge(void)
|
||||||
{
|
{
|
||||||
|
5
snac.h
5
snac.h
@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
|
|||||||
void user_free(snac *snac);
|
void user_free(snac *snac);
|
||||||
xs_list *user_list(void);
|
xs_list *user_list(void);
|
||||||
int user_open_by_md5(snac *snac, const char *md5);
|
int user_open_by_md5(snac *snac, const char *md5);
|
||||||
|
int user_persist(snac *snac);
|
||||||
|
|
||||||
int validate_uid(const char *uid);
|
int validate_uid(const char *uid);
|
||||||
|
|
||||||
@ -358,6 +359,10 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
|
|||||||
int mastoapi_put_handler(const xs_dict *req, const char *q_path,
|
int mastoapi_put_handler(const xs_dict *req, const char *q_path,
|
||||||
const char *payload, int p_size,
|
const char *payload, int p_size,
|
||||||
char **body, int *b_size, char **ctype);
|
char **body, int *b_size, char **ctype);
|
||||||
|
void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac);
|
||||||
|
int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
|
||||||
|
const char *payload, int p_size,
|
||||||
|
char **body, int *b_size, char **ctype);
|
||||||
void mastoapi_purge(void);
|
void mastoapi_purge(void);
|
||||||
|
|
||||||
void verify_links(snac *user);
|
void verify_links(snac *user);
|
||||||
|
Loading…
Reference in New Issue
Block a user