commit 81cf309

Louis Brauer  ·  2024-05-27 10:24:17 +0000 UTC
parent 0e21d35
Implement Mastodon PATCH endpoint for account profile updates
5 files changed,  +279, -101
M data.c
M html.c
M snac.h
M data.c
+27, -0
 1@@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
 2     return 0;
 3 }
 4 
 5+int user_persist(snac *snac)
 6+/* store user */
 7+{
 8+    xs *fn  = xs_fmt("%s/user.json", snac->basedir);
 9+    xs *bfn = xs_fmt("%s.bak", fn);
10+    FILE *f;
11+
12+    rename(fn, bfn);
13+
14+    if ((f = fopen(fn, "w")) != NULL) {
15+        xs_json_dump(snac->config, 4, f);
16+        fclose(f);
17+    }
18+    else
19+        rename(bfn, fn);
20+
21+    history_del(snac, "timeline.html_");
22+
23+    xs *a_msg = msg_actor(snac);
24+    xs *u_msg = msg_update(snac, a_msg);
25+
26+    enqueue_message(snac, u_msg);
27+    enqueue_verify_links(snac);
28+
29+    return 0;
30+}
31+
32 
33 double mtime_nl(const char *fn, int *n_link)
34 /* returns the mtime and number of links of a file or directory, or 0.0 */
M html.c
+1, -21
 1@@ -3334,27 +3334,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
 2             snac.config = xs_dict_set(snac.config, "passwd", pw);
 3         }
 4 
 5-        xs *fn  = xs_fmt("%s/user.json", snac.basedir);
 6-        xs *bfn = xs_fmt("%s.bak", fn);
 7-        FILE *f;
 8-
 9-        rename(fn, bfn);
10-
11-        if ((f = fopen(fn, "w")) != NULL) {
12-            xs_json_dump(snac.config, 4, f);
13-            fclose(f);
14-        }
15-        else
16-            rename(bfn, fn);
17-
18-        history_del(&snac, "timeline.html_");
19-
20-        xs *a_msg = msg_actor(&snac);
21-        xs *u_msg = msg_update(&snac, a_msg);
22-
23-        enqueue_message(&snac, u_msg);
24-
25-        enqueue_verify_links(&snac);
26+        user_persist(&snac);
27 
28         status = HTTP_STATUS_SEE_OTHER;
29     }
+10, -0
 1@@ -360,6 +360,16 @@ void httpd_connection(FILE *f)
 2                         payload, p_size, &body, &b_size, &ctype);
 3 #endif
 4 
 5+    }
 6+    else
 7+    if (strcmp(method, "PATCH") == 0) {
 8+
 9+#ifndef NO_MASTODON_API
10+        if (status == 0)
11+            status = mastoapi_patch_handler(req, q_path,
12+                        payload, p_size, &body, &b_size, &ctype);
13+#endif
14+
15     }
16     else
17     if (strcmp(method, "OPTIONS") == 0) {
+236, -80
  1@@ -1150,106 +1150,120 @@ int process_auth_token(snac *snac, const xs_dict *req)
  2     return logged_in;
  3 }
  4 
  5-
  6-int mastoapi_get_handler(const xs_dict *req, const char *q_path,
  7-                         char **body, int *b_size, char **ctype)
  8+void credentials_get(char **body, char **ctype, int *status, snac snac)
  9 {
 10-    (void)b_size;
 11+    xs *acct = xs_dict_new();
 12+
 13+    acct = xs_dict_append(acct, "id", snac.md5);
 14+    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
 15+    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
 16+    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
 17+    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
 18+    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
 19+    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
 20+    acct = xs_dict_append(acct, "url", snac.actor);
 21+    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
 22+    acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
 23 
 24-    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 25-        return 0;
 26+    xs *src = xs_json_loads("{\"privacy\":\"public\","
 27+        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
 28+    /* some apps take the note from the source object */
 29+    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
 30+    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
 31 
 32-    int status    = HTTP_STATUS_NOT_FOUND;
 33-    const xs_dict *args = xs_dict_get(req, "q_vars");
 34-    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
 35+    const xs_str *cw = xs_dict_get(snac.config, "cw");
 36+    src = xs_dict_set(src, "sensitive",
 37+        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 38 
 39-    snac snac1 = {0};
 40-    int logged_in = process_auth_token(&snac1, req);
 41+    src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
 42 
 43-    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
 44-        if (logged_in) {
 45-            xs *acct = xs_dict_new();
 46-
 47-            acct = xs_dict_append(acct, "id",           snac1.md5);
 48-            acct = xs_dict_append(acct, "username",     xs_dict_get(snac1.config, "uid"));
 49-            acct = xs_dict_append(acct, "acct",         xs_dict_get(snac1.config, "uid"));
 50-            acct = xs_dict_append(acct, "display_name", xs_dict_get(snac1.config, "name"));
 51-            acct = xs_dict_append(acct, "created_at",   xs_dict_get(snac1.config, "published"));
 52-            acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
 53-            acct = xs_dict_append(acct, "note",         xs_dict_get(snac1.config, "bio"));
 54-            acct = xs_dict_append(acct, "url",          snac1.actor);
 55-            acct = xs_dict_append(acct, "locked",       xs_stock(XSTYPE_FALSE));
 56-            acct = xs_dict_append(acct, "bot",          xs_dict_get(snac1.config, "bot"));
 57-
 58-            xs *src = xs_json_loads("{\"privacy\":\"public\","
 59-                    "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
 60-            acct = xs_dict_append(acct, "source", src);
 61-
 62-            xs *avatar = NULL;
 63-            const char *av   = xs_dict_get(snac1.config, "avatar");
 64-
 65-            if (xs_is_null(av) || *av == '\0')
 66-                avatar = xs_fmt("%s/susie.png", srv_baseurl);
 67-            else
 68-                avatar = xs_dup(av);
 69+    xs *avatar = NULL;
 70+    const char *av = xs_dict_get(snac.config, "avatar");
 71 
 72-            acct = xs_dict_append(acct, "avatar", avatar);
 73-            acct = xs_dict_append(acct, "avatar_static", avatar);
 74+    if (xs_is_null(av) || *av == '\0')
 75+        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 76+    else
 77+        avatar = xs_dup(av);
 78 
 79-            xs *header = NULL;
 80-            const char *hd = xs_dict_get(snac1.config, "header");
 81+    acct = xs_dict_append(acct, "avatar", avatar);
 82+    acct = xs_dict_append(acct, "avatar_static", avatar);
 83 
 84-            if (!xs_is_null(hd))
 85-                header = xs_dup(hd);
 86-            else
 87-                header = xs_fmt("%s/header.png", srv_baseurl);
 88+    xs *header = NULL;
 89+    const char *hd = xs_dict_get(snac.config, "header");
 90 
 91-            acct = xs_dict_append(acct, "header",        header);
 92-            acct = xs_dict_append(acct, "header_static", header);
 93+    if (!xs_is_null(hd))
 94+        header = xs_dup(hd);
 95+    else
 96+        header = xs_fmt("%s/header.png", srv_baseurl);
 97 
 98-            const xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
 99-            if (xs_type(metadata) == XSTYPE_DICT) {
100-                xs *fields = xs_list_new();
101-                const xs_str *k;
102-                const xs_str *v;
103+    acct = xs_dict_append(acct, "header", header);
104+    acct = xs_dict_append(acct, "header_static", header);
105 
106-                xs_dict *val_links = snac1.links;
107-                if (xs_is_null(val_links))
108-                    val_links = xs_stock(XSTYPE_DICT);
109+    const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
110+    if (xs_type(metadata) == XSTYPE_DICT) {
111+        xs *fields = xs_list_new();
112+        const xs_str *k;
113+        const xs_str *v;
114 
115-                int c = 0;
116-                while (xs_dict_next(metadata, &k, &v, &c)) {
117-                    xs *val_date = NULL;
118+        xs_dict *val_links = snac.links;
119+        if (xs_is_null(val_links))
120+            val_links = xs_stock(XSTYPE_DICT);
121 
122-                    const xs_number *verified_time = xs_dict_get(val_links, v);
123-                    if (xs_type(verified_time) == XSTYPE_NUMBER) {
124-                        time_t t = xs_number_get(verified_time);
125+        int c = 0;
126+        while (xs_dict_next(metadata, &k, &v, &c)) {
127+            xs *val_date = NULL;
128 
129-                        if (t > 0)
130-                            val_date = xs_str_utctime(t, ISO_DATE_SPEC);
131-                    }
132+            const xs_number *verified_time = xs_dict_get(val_links, v);
133+            if (xs_type(verified_time) == XSTYPE_NUMBER) {
134+                time_t t = xs_number_get(verified_time);
135 
136-                    xs *d = xs_dict_new();
137+                if (t > 0)
138+                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
139+            }
140 
141-                    d = xs_dict_append(d, "name", k);
142-                    d = xs_dict_append(d, "value", v);
143-                    d = xs_dict_append(d, "verified_at",
144-                        xs_type(val_date) == XSTYPE_STRING && *val_date ?
145-                            val_date : xs_stock(XSTYPE_NULL));
146+            xs *d = xs_dict_new();
147 
148-                    fields = xs_list_append(fields, d);
149-                }
150+            d = xs_dict_append(d, "name", k);
151+            d = xs_dict_append(d, "value", v);
152+            d = xs_dict_append(d, "verified_at",
153+                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
154 
155-                acct = xs_dict_set(acct, "fields", fields);
156-            }
157+            fields = xs_list_append(fields, d);
158+        }
159 
160-            acct = xs_dict_append(acct, "followers_count", xs_stock(0));
161-            acct = xs_dict_append(acct, "following_count", xs_stock(0));
162-            acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
163+        acct = xs_dict_set(acct, "fields", fields);
164+        /* some apps take the fields from the source object */
165+        src = xs_dict_set(src, "fields", fields);
166+    }
167 
168-            *body  = xs_json_dumps(acct, 4);
169-            *ctype = "application/json";
170-            status = HTTP_STATUS_OK;
171+    acct = xs_dict_append(acct, "source", src);
172+    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
173+    acct = xs_dict_append(acct, "following_count", xs_stock(0));
174+    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
175+
176+    *body = xs_json_dumps(acct, 4);
177+    *ctype = "application/json";
178+    *status = HTTP_STATUS_OK;
179+}
180+
181+int mastoapi_get_handler(const xs_dict *req, const char *q_path,
182+                         char **body, int *b_size, char **ctype)
183+{
184+    (void)b_size;
185+
186+    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
187+        return 0;
188+
189+    int status    = HTTP_STATUS_NOT_FOUND;
190+    const xs_dict *args = xs_dict_get(req, "q_vars");
191+    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
192+
193+    snac snac1 = {0};
194+    int logged_in = process_auth_token(&snac1, req);
195+
196+    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
197+        if (logged_in) {
198+            credentials_get(body, ctype, &status, snac1);
199         }
200         else {
201             status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
202@@ -3077,6 +3091,148 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
203     return status;
204 }
205 
206+void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
207+/* Store header or avatar */
208+{
209+    if (data != NULL) {
210+        if (xs_type(data) == XSTYPE_LIST) {
211+            const char *fn = xs_list_get(data, 0);
212+
213+            if (fn && *fn) {
214+                const char *ext = strrchr(fn, '.');
215+                /* Mona iOS sends JPG file as application/octet-stream with filename "header"
216+                 * Make sure we have a unique file name, otherwise updated images will not be
217+                 * loaded by clients.
218+                 */
219+                if (ext == NULL || strcmp(fn, key) == 0) {
220+                    fn = random_str();
221+                    ext = ".jpg";
222+                }
223+                xs *hash        = xs_md5_hex(fn, strlen(fn));
224+                xs *id          = xs_fmt("%s%s", hash, ext);
225+                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
226+                int fo          = xs_number_get(xs_list_get(data, 1));
227+                int fs          = xs_number_get(xs_list_get(data, 2));
228+
229+                /* store */
230+                static_put(snac, id, payload + fo, fs);
231+
232+                snac->config = xs_dict_set(snac->config, key, url);
233+            }
234+        }
235+    }
236+}
237+
238+int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
239+                          const char *payload, int p_size,
240+                          char **body, int *b_size, char **ctype)
241+/* Handle profile updates */
242+{
243+    (void)p_size;
244+    (void)b_size;
245+
246+    if (!xs_startswith(q_path, "/api/v1/"))
247+        return 0;
248+
249+    int status    = HTTP_STATUS_NOT_FOUND;
250+    xs *args      = NULL;
251+    const char *i_ctype = xs_dict_get(req, "content-type");
252+
253+    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
254+        if (!xs_is_null(payload))
255+            args = xs_json_loads(payload);
256+    }
257+    else
258+        args = xs_dup(xs_dict_get(req, "p_vars"));
259+
260+    if (args == NULL)
261+        return HTTP_STATUS_BAD_REQUEST;
262+
263+    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
264+
265+    snac snac = {0};
266+    int logged_in = process_auth_token(&snac, req);
267+
268+    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
269+        /* Update user profile fields */
270+        if (logged_in) {
271+            /*
272+            xs_str *dump = xs_json_dumps(args, 4);
273+            printf("%s\n\n", dump);
274+            */
275+            int c = 0;
276+            const xs_str *k;
277+            const xs_val *v;
278+            const xs_str *field_name = NULL;
279+            xs_dict *new_fields = xs_dict_new();
280+            while (xs_dict_next(args, &k, &v, &c)) {
281+                if (strcmp(k, "display_name") == 0) {
282+                    if (v != NULL)
283+                        snac.config = xs_dict_set(snac.config, "name", v);
284+                }
285+                else
286+                if (strcmp(k, "note") == 0) {
287+                    if (v != NULL)
288+                        snac.config = xs_dict_set(snac.config, "bio", v);
289+                }
290+                else
291+                if (strcmp(k, "bot") == 0) {
292+                    if (v != NULL)
293+                        snac.config = xs_dict_set(snac.config, "bot",
294+                            strcmp(v, "true") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
295+                }
296+                else
297+                if (strcmp(k, "source[sensitive]") == 0) {
298+                    if (v != NULL)
299+                        snac.config = xs_dict_set(snac.config, "cw",
300+                            strcmp(v, "true") == 0 ? "open" : "");
301+                }
302+                else
303+                if (strcmp(k, "source[privacy]") == 0) {
304+                    if (v != NULL)
305+                        snac.config = xs_dict_set(snac.config, "private",
306+                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
307+                }
308+                else
309+                if (strcmp(k, "header") == 0) {
310+                    persist_image("header", v, payload, &snac);
311+                }
312+                else
313+                if (strcmp(k, "avatar") == 0) {
314+                    persist_image("avatar", v, payload, &snac);
315+                }
316+                else
317+                if (xs_starts_and_ends("fields_attributes", k, "[name]")) {
318+                    field_name = strcmp(v, "") != 0 ? v : NULL;
319+                }
320+                else
321+                if (xs_starts_and_ends("fields_attributes", k, "[value]")) {
322+                    if (field_name != NULL) {
323+                        new_fields = xs_dict_set(new_fields, field_name, v);
324+                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
325+                    }
326+                }
327+            }
328+
329+            /* Persist profile */
330+            if (user_persist(&snac) == 0)
331+                credentials_get(body, ctype, &status, snac);
332+            else
333+                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
334+        }
335+        else
336+            status = HTTP_STATUS_UNAUTHORIZED;
337+    }
338+
339+    /* user cleanup */
340+    if (logged_in)
341+        user_free(&snac);
342+
343+    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
344+
345+    return status;
346+}
347+
348 
349 void mastoapi_purge(void)
350 {
M snac.h
+5, -0
 1@@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
 2 void user_free(snac *snac);
 3 xs_list *user_list(void);
 4 int user_open_by_md5(snac *snac, const char *md5);
 5+int user_persist(snac *snac);
 6 
 7 int validate_uid(const char *uid);
 8 
 9@@ -358,6 +359,10 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
10 int mastoapi_put_handler(const xs_dict *req, const char *q_path,
11                           const char *payload, int p_size,
12                           char **body, int *b_size, char **ctype);
13+void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac);
14+int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
15+                          const char *payload, int p_size,
16+                          char **body, int *b_size, char **ctype);
17 void mastoapi_purge(void);
18 
19 void verify_links(snac *user);