summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLouis Brauer <louis@openbooking.ch>2024-05-27 12:24:17 +0200
committerLouis Brauer <louis@openbooking.ch>2024-05-27 12:24:17 +0200
commit81cf309e4d0ba6c2debccc21ea4f85e1e6245dc5 (patch)
tree10528a18c859964fa11eabe15955c85dce7ecf25
parent0e21d35e802bf859aa14bce688cd9544458e9e9c (diff)
Implement Mastodon PATCH endpoint for account profile updates
-rw-r--r--data.c27
-rw-r--r--html.c22
-rw-r--r--httpd.c10
-rw-r--r--mastoapi.c316
-rw-r--r--snac.h5
5 files changed, 279 insertions, 101 deletions
diff --git a/data.c b/data.c
index 8fdd292..e24bf16 100644
--- a/data.c
+++ b/data.c
@@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
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)
/* returns the mtime and number of links of a file or directory, or 0.0 */
diff --git a/html.c b/html.c
index 1840799..6267adf 100644
--- a/html.c
+++ b/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);
}
- 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);
+ user_persist(&snac);
status = HTTP_STATUS_SEE_OTHER;
}
diff --git a/httpd.c b/httpd.c
index 60afe28..1c60a56 100644
--- a/httpd.c
+++ b/httpd.c
@@ -362,6 +362,16 @@ void httpd_connection(FILE *f)
}
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
if (strcmp(method, "OPTIONS") == 0) {
status = HTTP_STATUS_OK;
}
diff --git a/mastoapi.c b/mastoapi.c
index 8a54230..120c1aa 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -1150,106 +1150,120 @@ int process_auth_token(snac *snac, const xs_dict *req)
return logged_in;
}
-
-int mastoapi_get_handler(const xs_dict *req, const char *q_path,
- char **body, int *b_size, char **ctype)
+void credentials_get(char **body, char **ctype, int *status, snac snac)
{
- (void)b_size;
+ 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"));
- if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
- return 0;
+ 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");
- int status = HTTP_STATUS_NOT_FOUND;
- const xs_dict *args = xs_dict_get(req, "q_vars");
- xs *cmd = xs_replace_n(q_path, "/api", "", 1);
+ 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));
- snac snac1 = {0};
- int logged_in = process_auth_token(&snac1, req);
+ src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
- if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
- if (logged_in) {
- xs *acct = xs_dict_new();
-
- 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);
+ xs *avatar = NULL;
+ const char *av = xs_dict_get(snac.config, "avatar");
- acct = xs_dict_append(acct, "avatar", avatar);
- acct = xs_dict_append(acct, "avatar_static", avatar);
+ if (xs_is_null(av) || *av == '\0')
+ avatar = xs_fmt("%s/susie.png", srv_baseurl);
+ else
+ avatar = xs_dup(av);
- xs *header = NULL;
- const char *hd = xs_dict_get(snac1.config, "header");
+ acct = xs_dict_append(acct, "avatar", avatar);
+ acct = xs_dict_append(acct, "avatar_static", avatar);
- if (!xs_is_null(hd))
- header = xs_dup(hd);
- else
- header = xs_fmt("%s/header.png", srv_baseurl);
+ xs *header = NULL;
+ const char *hd = xs_dict_get(snac.config, "header");
- acct = xs_dict_append(acct, "header", header);
- acct = xs_dict_append(acct, "header_static", header);
+ if (!xs_is_null(hd))
+ header = xs_dup(hd);
+ else
+ header = xs_fmt("%s/header.png", srv_baseurl);
- 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;
+ acct = xs_dict_append(acct, "header", header);
+ acct = xs_dict_append(acct, "header_static", header);
- xs_dict *val_links = snac1.links;
- if (xs_is_null(val_links))
- val_links = xs_stock(XSTYPE_DICT);
+ 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;
- int c = 0;
- while (xs_dict_next(metadata, &k, &v, &c)) {
- xs *val_date = NULL;
+ xs_dict *val_links = snac.links;
+ if (xs_is_null(val_links))
+ val_links = xs_stock(XSTYPE_DICT);
- 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);
+ int c = 0;
+ while (xs_dict_next(metadata, &k, &v, &c)) {
+ xs *val_date = NULL;
- if (t > 0)
- val_date = xs_str_utctime(t, ISO_DATE_SPEC);
- }
+ 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);
- xs *d = xs_dict_new();
+ if (t > 0)
+ val_date = xs_str_utctime(t, ISO_DATE_SPEC);
+ }
- 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));
+ xs *d = xs_dict_new();
- fields = xs_list_append(fields, d);
- }
+ 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));
- acct = xs_dict_set(acct, "fields", fields);
- }
+ fields = xs_list_append(fields, d);
+ }
- 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));
+ acct = xs_dict_set(acct, "fields", fields);
+ /* some apps take the fields from the source object */
+ src = xs_dict_set(src, "fields", fields);
+ }
- *body = xs_json_dumps(acct, 4);
- *ctype = "application/json";
- status = HTTP_STATUS_OK;
+ 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,
+ char **body, int *b_size, char **ctype)
+{
+ (void)b_size;
+
+ if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
+ return 0;
+
+ int status = HTTP_STATUS_NOT_FOUND;
+ const xs_dict *args = xs_dict_get(req, "q_vars");
+ xs *cmd = xs_replace_n(q_path, "/api", "", 1);
+
+ snac snac1 = {0};
+ int logged_in = process_auth_token(&snac1, req);
+
+ if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
+ if (logged_in) {
+ credentials_get(body, ctype, &status, snac1);
}
else {
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;
}
+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)
{
diff --git a/snac.h b/snac.h
index d79f3ea..5c2f731 100644
--- a/snac.h
+++ b/snac.h
@@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
void user_free(snac *snac);
xs_list *user_list(void);
int user_open_by_md5(snac *snac, const char *md5);
+int user_persist(snac *snac);
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,
const char *payload, int p_size,
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 verify_links(snac *user);