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
+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 }
M
httpd.c
+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);