Selaa lähdekoodia

Implement Mastodon PATCH endpoint for account profile updates

Louis Brauer 10 kuukautta sitten
vanhempi
commit
81cf309e4d
5 muutettua tiedostoa jossa 279 lisäystä ja 101 poistoa
  1. 27 0
      data.c
  2. 1 21
      html.c
  3. 10 0
      httpd.c
  4. 236 80
      mastoapi.c
  5. 5 0
      snac.h

+ 27 - 0
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 */

+ 1 - 21
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;
     }

+ 10 - 0
httpd.c

@@ -360,6 +360,16 @@ void httpd_connection(FILE *f)
                         payload, p_size, &body, &b_size, &ctype);
 #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
     if (strcmp(method, "OPTIONS") == 0) {

+ 236 - 80
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)
 {

+ 5 - 0
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);