commit d56d4be

grunfink  ·  2024-05-30 08:44:54 +0000 UTC
parents 5bc2017, af8f1ef
Merge pull request 'Mastodon PATCH API for user profile updates' (#169) from louis77/snac2:master into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/169
14 files changed,  +640, -336
M data.c
M html.c
M snac.c
M snac.h
M xs.h
+1, -1
1@@ -52,7 +52,7 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
2 snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode.h xs_json.h xs_curl.h \
3  xs_openssl.h xs_socket.h xs_url.h xs_httpd.h xs_mime.h xs_regex.h \
4  xs_set.h xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h \
5- snac.h
6+ snac.h http_codes.h
7 upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h
8 utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
9  xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h
+31, -25
  1@@ -96,19 +96,19 @@ int activitypub_request(snac *user, const char *url, xs_dict **data)
  2         ctype = xs_dict_get(response, "content-type");
  3 
  4         if (xs_is_null(ctype))
  5-            status = 400;
  6+            status = HTTP_STATUS_BAD_REQUEST;
  7         else
  8         if (xs_str_in(ctype, "application/activity+json") != -1 ||
  9             xs_str_in(ctype, "application/ld+json") != -1) {
 10 
 11             /* if there is no payload, fail */
 12             if (xs_is_null(payload))
 13-                status = 400;
 14+                status = HTTP_STATUS_BAD_REQUEST;
 15             else
 16                 *data = xs_json_loads(payload);
 17         }
 18         else
 19-            status = 500;
 20+            status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 21     }
 22 
 23     return status;
 24@@ -443,7 +443,7 @@ int send_to_actor(snac *snac, const char *actor, const xs_dict *msg,
 25                   xs_val **payload, int *p_size, int timeout)
 26 /* sends a message to an actor */
 27 {
 28-    int status = 400;
 29+    int status = HTTP_STATUS_BAD_REQUEST;
 30     xs *inbox = get_actor_inbox(actor);
 31 
 32     if (!xs_is_null(inbox))
 33@@ -1762,7 +1762,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 34     a_status = actor_request(snac, actor, &actor_o);
 35 
 36     /* do not retry permanent failures */
 37-    if (a_status == 404 || a_status == 410 || a_status < 0) {
 38+    if (a_status == HTTP_STATUS_NOT_FOUND
 39+        || a_status == HTTP_STATUS_GONE
 40+        || a_status < 0) {
 41         srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status));
 42         return -1;
 43     }
 44@@ -1905,7 +1907,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 45         }
 46         else
 47         if (strcmp(utype, "Announce") == 0) { /** **/
 48-            int status = 200;
 49+            int status = HTTP_STATUS_OK;
 50 
 51             /* commented out: if a followed user boosts something that
 52                is requested and then unboosts, the post remains here,
 53@@ -2015,7 +2017,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 54         if (xs_type(object) == XSTYPE_DICT)
 55             object = xs_dict_get(object, "id");
 56 
 57-        if (timeline_admire(snac, object, actor, 1) == 201)
 58+        if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED)
 59             snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
 60         else
 61             snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
 62@@ -2046,7 +2048,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 63                     xs *who_o = NULL;
 64 
 65                     if (valid_status(actor_request(snac, who, &who_o))) {
 66-                        if (timeline_admire(snac, object, actor, 0) == 201)
 67+                        if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
 68                             snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
 69                         else
 70                             snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
 71@@ -2383,11 +2385,15 @@ void process_queue_item(xs_dict *q_item)
 72 
 73             /* if it's not the first time it fails with a timeout,
 74                penalize the server by skipping one retry */
 75-            if (p_status == status && status == 499)
 76+            if (p_status == status && status == HTTP_STATUS_CLIENT_CLOSED_REQUEST)
 77                 retries++;
 78 
 79             /* error sending; requeue? */
 80-            if (status == 400 || status == 404 || status == 405 || status == 410 || status < 0)
 81+            if (status == HTTP_STATUS_BAD_REQUEST
 82+                || status == HTTP_STATUS_NOT_FOUND
 83+                || status == HTTP_STATUS_METHOD_NOT_ALLOWED
 84+                || status == HTTP_STATUS_GONE
 85+                || status < 0)
 86                 /* explicit error: discard */
 87                 srv_log(xs_fmt("output message: fatal error %s %d", inbox, status));
 88             else
 89@@ -2574,7 +2580,7 @@ int process_queue(void)
 90 int activitypub_get_handler(const xs_dict *req, const char *q_path,
 91                             char **body, int *b_size, char **ctype)
 92 {
 93-    int status = 200;
 94+    int status = HTTP_STATUS_OK;
 95     const char *accept = xs_dict_get(req, "accept");
 96     snac snac;
 97     xs *msg = NULL;
 98@@ -2594,7 +2600,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
 99     if (!user_open(&snac, uid)) {
100         /* invalid user */
101         srv_debug(1, xs_fmt("activitypub_get_handler bad user %s", uid));
102-        return 404;
103+        return HTTP_STATUS_NOT_FOUND;
104     }
105 
106     p_path = xs_list_get(l, 2);
107@@ -2652,12 +2658,12 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
108 
109         /* don't return non-public objects */
110         if (valid_status(status) && !is_msg_public(msg))
111-            status = 404;
112+            status = HTTP_STATUS_NOT_FOUND;
113     }
114     else
115-        status = 404;
116+        status = HTTP_STATUS_NOT_FOUND;
117 
118-    if (status == 200 && msg != NULL) {
119+    if (status == HTTP_STATUS_OK && msg != NULL) {
120         *body   = xs_json_dumps(msg, 4);
121         *b_size = strlen(*body);
122     }
123@@ -2677,7 +2683,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
124 {
125     (void)b_size;
126 
127-    int status = 202; /* accepted */
128+    int status = HTTP_STATUS_ACCEPTED;
129     const char *i_ctype = xs_dict_get(req, "content-type");
130     snac snac;
131     const char *v;
132@@ -2685,13 +2691,13 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
133     if (i_ctype == NULL) {
134         *body  = xs_str_new("no content-type");
135         *ctype = "text/plain";
136-        return 400;
137+        return HTTP_STATUS_BAD_REQUEST;
138     }
139 
140     if (xs_is_null(payload)) {
141         *body  = xs_str_new("no payload");
142         *ctype = "text/plain";
143-        return 400;
144+        return HTTP_STATUS_BAD_REQUEST;
145     }
146 
147     if (xs_str_in(i_ctype, "application/activity+json") == -1 &&
148@@ -2709,7 +2715,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
149 
150         *body  = xs_str_new("JSON error");
151         *ctype = "text/plain";
152-        return 400;
153+        return HTTP_STATUS_BAD_REQUEST;
154     }
155 
156     if (id && is_instance_blocked(id)) {
157@@ -2717,7 +2723,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
158 
159         *body  = xs_str_new("blocked");
160         *ctype = "text/plain";
161-        return 403;
162+        return HTTP_STATUS_FORBIDDEN;
163     }
164 
165     /* get the user and path */
166@@ -2725,20 +2731,20 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
167 
168     if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) {
169         enqueue_shared_input(msg, req, 0);
170-        return 202;
171+        return HTTP_STATUS_ACCEPTED;
172     }
173 
174     if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) {
175         /* strange q_path */
176         srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path));
177-        return 404;
178+        return HTTP_STATUS_NOT_FOUND;
179     }
180 
181     const char *uid = xs_list_get(l, 1);
182     if (!user_open(&snac, uid)) {
183         /* invalid user */
184         srv_debug(1, xs_fmt("activitypub_post_handler bad user %s", uid));
185-        return 404;
186+        return HTTP_STATUS_NOT_FOUND;
187     }
188 
189     /* if it has a digest, check it now, because
190@@ -2752,7 +2758,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
191 
192             *body  = xs_str_new("bad digest");
193             *ctype = "text/plain";
194-            status = 400;
195+            status = HTTP_STATUS_BAD_REQUEST;
196         }
197     }
198 
199@@ -2763,7 +2769,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
200 
201             *body  = xs_str_new("rejected");
202             *ctype = "text/plain";
203-            status = 403;
204+            status = HTTP_STATUS_FORBIDDEN;
205         }
206     }
207 
M data.c
+61, -34
  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 */
 35@@ -355,12 +382,12 @@ int is_md5_hex(const char *md5)
 36 int index_add_md5(const char *fn, const char *md5)
 37 /* adds an md5 to an index */
 38 {
 39-    int status = 201; /* Created */
 40+    int status = HTTP_STATUS_CREATED;
 41     FILE *f;
 42 
 43     if (!is_md5_hex(md5)) {
 44         srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
 45-        return 400;
 46+        return HTTP_STATUS_BAD_REQUEST;
 47     }
 48 
 49     pthread_mutex_lock(&data_mutex);
 50@@ -375,7 +402,7 @@ int index_add_md5(const char *fn, const char *md5)
 51         fclose(f);
 52     }
 53     else
 54-        status = 500;
 55+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 56 
 57     pthread_mutex_unlock(&data_mutex);
 58 
 59@@ -394,7 +421,7 @@ int index_add(const char *fn, const char *id)
 60 int index_del_md5(const char *fn, const char *md5)
 61 /* deletes an md5 from an index */
 62 {
 63-    int status = 404;
 64+    int status = HTTP_STATUS_NOT_FOUND;
 65     FILE *f;
 66 
 67     pthread_mutex_lock(&data_mutex);
 68@@ -411,7 +438,7 @@ int index_del_md5(const char *fn, const char *md5)
 69                    [yes: this breaks index_len()] */
 70                 fseek(f, -33, SEEK_CUR);
 71                 fwrite("-", 1, 1, f);
 72-                status = 200;
 73+                status = HTTP_STATUS_OK;
 74 
 75                 break;
 76             }
 77@@ -420,7 +447,7 @@ int index_del_md5(const char *fn, const char *md5)
 78         fclose(f);
 79     }
 80     else
 81-        status = 410;
 82+        status = HTTP_STATUS_GONE;
 83 
 84     pthread_mutex_unlock(&data_mutex);
 85 
 86@@ -660,7 +687,7 @@ int object_here(const char *id)
 87 int object_get_by_md5(const char *md5, xs_dict **obj)
 88 /* returns a stored object, optionally of the requested type */
 89 {
 90-    int status = 404;
 91+    int status = HTTP_STATUS_NOT_FOUND;
 92     xs *fn     = _object_fn_by_md5(md5, "object_get_by_md5");
 93     FILE *f;
 94 
 95@@ -669,7 +696,7 @@ int object_get_by_md5(const char *md5, xs_dict **obj)
 96         fclose(f);
 97 
 98         if (*obj)
 99-            status = 200;
100+            status = HTTP_STATUS_OK;
101     }
102     else
103         *obj = NULL;
104@@ -689,7 +716,7 @@ int object_get(const char *id, xs_dict **obj)
105 int _object_add(const char *id, const xs_dict *obj, int ow)
106 /* stores an object */
107 {
108-    int status = 201; /* Created */
109+    int status = HTTP_STATUS_CREATED; /* Created */
110     xs *fn     = _object_fn(id);
111     FILE *f;
112 
113@@ -697,10 +724,10 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
114         if (!ow) {
115             /* object already here */
116             srv_debug(1, xs_fmt("object_add object already here %s", id));
117-            return 204; /* No content */
118+            return HTTP_STATUS_NO_CONTENT;
119         }
120         else
121-            status = 200;
122+            status = HTTP_STATUS_OK;
123     }
124 
125     if ((f = fopen(fn, "w")) != NULL) {
126@@ -736,7 +763,7 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
127     }
128     else {
129         srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
130-        status = 500;
131+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
132     }
133 
134     srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
135@@ -762,11 +789,11 @@ int object_add_ow(const char *id, const xs_dict *obj)
136 int object_del_by_md5(const char *md5)
137 /* deletes an object by its md5 */
138 {
139-    int status = 404;
140+    int status = HTTP_STATUS_NOT_FOUND;
141     xs *fn     = _object_fn_by_md5(md5, "object_del_by_md5");
142 
143     if (unlink(fn) != -1) {
144-        status = 200;
145+        status = HTTP_STATUS_OK;
146 
147         /* also delete associated indexes */
148         xs *spec  = xs_dup(fn);
149@@ -907,7 +934,7 @@ int object_parent(const char *md5, char *buf, int size)
150 int object_admire(const char *id, const char *actor, int like)
151 /* actor likes or announces this object */
152 {
153-    int status = 200;
154+    int status = HTTP_STATUS_OK;
155     xs *fn     = _object_fn(id);
156 
157     fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
158@@ -1007,7 +1034,7 @@ int follower_add(snac *snac, const char *actor)
159 
160     snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
161 
162-    return ret == -1 ? 500 : 200;
163+    return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
164 }
165 
166 
167@@ -1018,7 +1045,7 @@ int follower_del(snac *snac, const char *actor)
168 
169     snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
170 
171-    return ret == -1 ? 404 : 200;
172+    return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
173 }
174 
175 
176@@ -1109,7 +1136,7 @@ int timeline_here(snac *snac, const char *md5)
177 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
178 /* gets a message from the timeline */
179 {
180-    int status = 404;
181+    int status = HTTP_STATUS_NOT_FOUND;
182     FILE *f    = NULL;
183 
184     xs *fn = timeline_fn_by_md5(snac, md5);
185@@ -1119,7 +1146,7 @@ int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
186         fclose(f);
187 
188         if (*msg != NULL)
189-            status = 200;
190+            status = HTTP_STATUS_OK;
191     }
192 
193     return status;
194@@ -1282,7 +1309,7 @@ xs_str *_following_fn(snac *snac, const char *actor)
195 int following_add(snac *snac, const char *actor, const xs_dict *msg)
196 /* adds to the following list */
197 {
198-    int ret = 201; /* created */
199+    int ret = HTTP_STATUS_CREATED;
200     xs *fn = _following_fn(snac, actor);
201     FILE *f;
202     xs *p_object = NULL;
203@@ -1295,7 +1322,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
204 
205         if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
206             snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
207-            return 200;
208+            return HTTP_STATUS_OK;
209         }
210     }
211 
212@@ -1311,7 +1338,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
213         link(actor_fn, fn);
214     }
215     else
216-        ret = 500;
217+        ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
218 
219     snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
220 
221@@ -1332,7 +1359,7 @@ int following_del(snac *snac, const char *actor)
222     fn = xs_replace_i(fn, ".json", "_a.json");
223     unlink(fn);
224 
225-    return 200;
226+    return HTTP_STATUS_OK;
227 }
228 
229 
230@@ -1350,14 +1377,14 @@ int following_get(snac *snac, const char *actor, xs_dict **data)
231 {
232     xs *fn = _following_fn(snac, actor);
233     FILE *f;
234-    int status = 200;
235+    int status = HTTP_STATUS_OK;
236 
237     if ((f = fopen(fn, "r")) != NULL) {
238         *data = xs_json_load(f);
239         fclose(f);
240     }
241     else
242-        status = 404;
243+        status = HTTP_STATUS_NOT_FOUND;
244 
245     return status;
246 }
247@@ -1576,7 +1603,7 @@ int actor_add(const char *actor, const xs_dict *msg)
248 int actor_get(const char *actor, xs_dict **data)
249 /* returns an already downloaded actor */
250 {
251-    int status = 200;
252+    int status = HTTP_STATUS_OK;
253     xs_dict *d = NULL;
254 
255     if (xs_startswith(actor, srv_baseurl)) {
256@@ -1590,10 +1617,10 @@ int actor_get(const char *actor, xs_dict **data)
257                 *data = msg_actor(&user);
258 
259             user_free(&user);
260-            return 200;
261+            return HTTP_STATUS_OK;
262         }
263         else
264-            return 404;
265+            return HTTP_STATUS_NOT_FOUND;
266     }
267 
268     /* read the object */
269@@ -1606,7 +1633,7 @@ int actor_get(const char *actor, xs_dict **data)
270     if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
271         srv_debug(1, xs_fmt("corrupted actor object %s", actor));
272         d = xs_free(d);
273-        return 404;
274+        return HTTP_STATUS_NOT_FOUND;
275     }
276 
277     if (data)
278@@ -1622,7 +1649,7 @@ int actor_get(const char *actor, xs_dict **data)
279 
280     if (mtime(fn) + max_time < (double) time(NULL)) {
281         /* actor data exists but also stinks */
282-        status = 205; /* "205: Reset Content" "110: Response Is Stale" */
283+        status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
284     }
285 
286     return status;
287@@ -1634,7 +1661,7 @@ int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
288 {
289     int status = actor_get(actor, data);
290 
291-    if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
292+    if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
293         enqueue_actor_refresh(user, actor, 0);
294 
295     return status;
296@@ -1953,7 +1980,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
297                         const char *inm, xs_str **etag)
298 /* loads a cached file */
299 {
300-    int status = 404;
301+    int status = HTTP_STATUS_NOT_FOUND;
302 
303     if (fn) {
304         double tm = mtime(fn);
305@@ -1965,7 +1992,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
306             /* if if-none-match is set, check if it's the same */
307             if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
308                 /* client has the newest version */
309-                status = 304;
310+                status = HTTP_STATUS_NOT_MODIFIED;
311             }
312             else {
313                 /* newer or never downloaded; read the full file */
314@@ -1976,7 +2003,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
315                     *data = xs_read(f, size);
316                     fclose(f);
317 
318-                    status = 200;
319+                    status = HTTP_STATUS_OK;
320                 }
321             }
322 
M html.c
+39, -59
  1@@ -2540,7 +2540,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
  2                      char **body, int *b_size, char **ctype, xs_str **etag)
  3 {
  4     const char *accept = xs_dict_get(req, "accept");
  5-    int status = 404;
  6+    int status = HTTP_STATUS_NOT_FOUND;
  7     snac snac;
  8     xs *uid = NULL;
  9     const char *p_path;
 10@@ -2553,7 +2553,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 11 
 12     if (xs_is_null(v)) {
 13         srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
 14-        return 404;
 15+        return HTTP_STATUS_NOT_FOUND;
 16     }
 17 
 18     uid = xs_dup(v);
 19@@ -2569,7 +2569,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 20     if (!uid || !user_open(&snac, uid)) {
 21         /* invalid user */
 22         srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
 23-        return 404;
 24+        return HTTP_STATUS_NOT_FOUND;
 25     }
 26 
 27     /* return the RSS if requested by Accept header */
 28@@ -2598,7 +2598,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 29             /** empty public timeline for private users **/
 30             *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1);
 31             *b_size = strlen(*body);
 32-            status  = 200;
 33+            status  = HTTP_STATUS_OK;
 34         }
 35         else
 36         if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
 37@@ -2617,7 +2617,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 38             *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1);
 39 
 40             *b_size = strlen(*body);
 41-            status  = 200;
 42+            status  = HTTP_STATUS_OK;
 43 
 44             if (save)
 45                 history_add(&snac, h, *body, *b_size, etag);
 46@@ -2627,7 +2627,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 47     if (strcmp(p_path, "admin") == 0) { /** private timeline **/
 48         if (!login(&snac, req)) {
 49             *body  = xs_dup(uid);
 50-            status = 401;
 51+            status = HTTP_STATUS_UNAUTHORIZED;
 52         }
 53         else {
 54             const char *q = xs_dict_get(q_vars, "q");
 55@@ -2649,7 +2649,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 56 
 57                     *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0);
 58                     *b_size = strlen(*body);
 59-                    status  = 200;
 60+                    status  = HTTP_STATUS_OK;
 61                 }
 62                 else {
 63                     /** search by content **/
 64@@ -2670,7 +2670,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 65 
 66                     *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0);
 67                     *b_size = strlen(*body);
 68-                    status  = 200;
 69+                    status  = HTTP_STATUS_OK;
 70                 }
 71             }
 72             else {
 73@@ -2699,7 +2699,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 74                             xs_list_len(next), NULL, "/admin", 1);
 75 
 76                     *b_size = strlen(*body);
 77-                    status  = 200;
 78+                    status  = HTTP_STATUS_OK;
 79 
 80                     if (save)
 81                         history_add(&snac, "timeline.html_", *body, *b_size, etag);
 82@@ -2711,7 +2711,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 83     if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
 84         if (!login(&snac, req)) {
 85             *body  = xs_dup(uid);
 86-            status = 401;
 87+            status = HTTP_STATUS_UNAUTHORIZED;
 88         }
 89         else {
 90             xs *l = xs_split(p_path, "/");
 91@@ -2722,7 +2722,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 92 
 93                 *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1);
 94                 *b_size = strlen(*body);
 95-                status  = 200;
 96+                status  = HTTP_STATUS_OK;
 97             }
 98         }
 99     }
100@@ -2730,31 +2730,31 @@ int html_get_handler(const xs_dict *req, const char *q_path,
101     if (strcmp(p_path, "people") == 0) { /** the list of people **/
102         if (!login(&snac, req)) {
103             *body  = xs_dup(uid);
104-            status = 401;
105+            status = HTTP_STATUS_UNAUTHORIZED;
106         }
107         else {
108             *body   = html_people(&snac);
109             *b_size = strlen(*body);
110-            status  = 200;
111+            status  = HTTP_STATUS_OK;
112         }
113     }
114     else
115     if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
116         if (!login(&snac, req)) {
117             *body  = xs_dup(uid);
118-            status = 401;
119+            status = HTTP_STATUS_UNAUTHORIZED;
120         }
121         else {
122             *body   = html_notifications(&snac, skip, show);
123             *b_size = strlen(*body);
124-            status  = 200;
125+            status  = HTTP_STATUS_OK;
126         }
127     }
128     else
129     if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
130         if (!login(&snac, req)) {
131             *body  = xs_dup(uid);
132-            status = 401;
133+            status = HTTP_STATUS_UNAUTHORIZED;
134         }
135         else {
136             xs *list = timeline_instance_list(skip, show);
137@@ -2763,14 +2763,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
138             *body = html_timeline(&snac, list, 0, skip, show,
139                 xs_list_len(next), L("Showing instance timeline"), "/instance", 0);
140             *b_size = strlen(*body);
141-            status  = 200;
142+            status  = HTTP_STATUS_OK;
143         }
144     }
145     else
146     if (xs_startswith(p_path, "list/")) { /** list timelines **/
147         if (!login(&snac, req)) {
148             *body  = xs_dup(uid);
149-            status = 401;
150+            status = HTTP_STATUS_UNAUTHORIZED;
151         }
152         else {
153             xs *l = xs_split(p_path, "/");
154@@ -2787,14 +2787,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
155                 *body = html_timeline(&snac, list, 0, skip, show,
156                     xs_list_len(next), title, base, 1);
157                 *b_size = strlen(*body);
158-                status  = 200;
159+                status  = HTTP_STATUS_OK;
160             }
161         }
162     }
163     else
164     if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
165         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
166-            return 403;
167+            return HTTP_STATUS_FORBIDDEN;
168 
169         xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
170         xs *msg = NULL;
171@@ -2807,7 +2807,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
172 
173             *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1);
174             *b_size = strlen(*body);
175-            status  = 200;
176+            status  = HTTP_STATUS_OK;
177         }
178     }
179     else
180@@ -2829,10 +2829,10 @@ int html_get_handler(const xs_dict *req, const char *q_path,
181     else
182     if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
183         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
184-            return 403;
185+            return HTTP_STATUS_FORBIDDEN;
186 
187         if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
188-            return 403;
189+            return HTTP_STATUS_FORBIDDEN;
190 
191         xs *l = xs_split(p_path, "/");
192         const char *id = xs_list_get(l, 1);
193@@ -2841,7 +2841,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
194             if (xs_endswith(id, "timeline.html_")) {
195                 /* Don't let them in */
196                 *b_size = 0;
197-                status = 404;
198+                status = HTTP_STATUS_NOT_FOUND;
199             }
200             else
201                 status = history_get(&snac, id, body, b_size,
202@@ -2851,7 +2851,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
203     else
204     if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
205         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
206-            return 403;
207+            return HTTP_STATUS_FORBIDDEN;
208 
209         xs *elems = timeline_simple_list(&snac, "public", 0, 20);
210         xs *bio   = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
211@@ -2865,12 +2865,12 @@ int html_get_handler(const xs_dict *req, const char *q_path,
212         *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
213         *b_size = strlen(*body);
214         *ctype  = "application/rss+xml; charset=utf-8";
215-        status  = 200;
216+        status  = HTTP_STATUS_OK;
217 
218         snac_debug(&snac, 1, xs_fmt("serving RSS"));
219     }
220     else
221-        status = 404;
222+        status = HTTP_STATUS_NOT_FOUND;
223 
224     user_free(&snac);
225 
226@@ -2901,7 +2901,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
227     if (!uid || !user_open(&snac, uid)) {
228         /* invalid user */
229         srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
230-        return 404;
231+        return HTTP_STATUS_NOT_FOUND;
232     }
233 
234     p_path = xs_list_get(l, 2);
235@@ -2910,7 +2910,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
236     if (!login(&snac, req)) {
237         user_free(&snac);
238         *body  = xs_dup(uid);
239-        return 401;
240+        return HTTP_STATUS_UNAUTHORIZED;
241     }
242 
243     p_vars = xs_dict_get(req, "p_vars");
244@@ -3049,7 +3049,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
245             history_del(&snac, "timeline.html_");
246         }
247 
248-        status = 303;
249+        status = HTTP_STATUS_SEE_OTHER;
250     }
251     else
252     if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
253@@ -3060,11 +3060,11 @@ int html_post_handler(const xs_dict *req, const char *q_path,
254         const char *group  = xs_dict_get(p_vars, "group");
255 
256         if (action == NULL)
257-            return 404;
258+            return HTTP_STATUS_NOT_FOUND;
259 
260         snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
261 
262-        status = 303;
263+        status = HTTP_STATUS_SEE_OTHER;
264 
265         if (strcmp(action, L("Like")) == 0) { /** **/
266             xs *msg = msg_admiration(&snac, id, "Like");
267@@ -3216,10 +3216,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
268             timeline_touch(&snac);
269         }
270         else
271-            status = 404;
272+            status = HTTP_STATUS_NOT_FOUND;
273 
274         /* delete the cached timeline */
275-        if (status == 303)
276+        if (status == HTTP_STATUS_SEE_OTHER)
277             history_del(&snac, "timeline.html_");
278     }
279     else
280@@ -3334,36 +3334,16 @@ int html_post_handler(const xs_dict *req, const char *q_path,
281             snac.config = xs_dict_set(snac.config, "passwd", pw);
282         }
283 
284-        xs *fn  = xs_fmt("%s/user.json", snac.basedir);
285-        xs *bfn = xs_fmt("%s.bak", fn);
286-        FILE *f;
287-
288-        rename(fn, bfn);
289-
290-        if ((f = fopen(fn, "w")) != NULL) {
291-            xs_json_dump(snac.config, 4, f);
292-            fclose(f);
293-        }
294-        else
295-            rename(bfn, fn);
296-
297-        history_del(&snac, "timeline.html_");
298-
299-        xs *a_msg = msg_actor(&snac);
300-        xs *u_msg = msg_update(&snac, a_msg);
301-
302-        enqueue_message(&snac, u_msg);
303-
304-        enqueue_verify_links(&snac);
305+        user_persist(&snac);
306 
307-        status = 303;
308+        status = HTTP_STATUS_SEE_OTHER;
309     }
310     else
311     if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
312         notify_clear(&snac);
313         timeline_touch(&snac);
314 
315-        status = 303;
316+        status = HTTP_STATUS_SEE_OTHER;
317     }
318     else
319     if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
320@@ -3416,10 +3396,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
321             }
322         }
323 
324-        status = 303;
325+        status = HTTP_STATUS_SEE_OTHER;
326     }
327 
328-    if (status == 303) {
329+    if (status == HTTP_STATUS_SEE_OTHER) {
330         const char *redir = xs_dict_get(p_vars, "redir");
331 
332         if (xs_is_null(redir))
+45, -0
 1@@ -0,0 +1,45 @@
 2+HTTP_STATUS(100, CONTINUE, Continue)
 3+HTTP_STATUS(101, SWITCHING_PROTOCOLS, Switching Protocols)
 4+HTTP_STATUS(102, PROCESSING, Processing)
 5+HTTP_STATUS(103, EARLY_HINTS, Early Hints)
 6+HTTP_STATUS(200, OK, OK)
 7+HTTP_STATUS(201, CREATED, Created)
 8+HTTP_STATUS(202, ACCEPTED, Accepted)
 9+HTTP_STATUS(203, NON_AUTHORITATIVE_INFORMATION, Non Authoritative Information)
10+HTTP_STATUS(204, NO_CONTENT, No Content)
11+HTTP_STATUS(205, RESET_CONTENT, Reset Content)
12+HTTP_STATUS(206, PARTIAL_CONTENT, Partial Content)
13+HTTP_STATUS(207, MULTI_STATUS, Multi Status)
14+HTTP_STATUS(208, ALREADY_REPORTED, Already Reported)
15+HTTP_STATUS(218, THIS_IS_FINE, This Is Fine)
16+HTTP_STATUS(226, IM_USED, IM Used)
17+HTTP_STATUS(300, MULTIPLE_CHOICES, Multiple Choices)
18+HTTP_STATUS(301, MOVED_PERMANENTLY, Moved Permanently)
19+HTTP_STATUS(302, FOUND, Found)
20+HTTP_STATUS(303, SEE_OTHER, See Other)
21+HTTP_STATUS(304, NOT_MODIFIED, Not Modified)
22+HTTP_STATUS(305, USE_PROXY, Use Proxy)
23+HTTP_STATUS(306, SWITCH_PROXY, Switch Proxy)
24+HTTP_STATUS(307, TEMPORARY_REDIRECT, Temporary Redirect)
25+HTTP_STATUS(308, PERMANENT_REDIRECT, Permanent Redirect)
26+HTTP_STATUS(400, BAD_REQUEST, Bad Request)
27+HTTP_STATUS(401, UNAUTHORIZED, Unauthorized)
28+HTTP_STATUS(402, PAYMENT_REQUIRED, Payment Required)
29+HTTP_STATUS(403, FORBIDDEN, Forbidden)
30+HTTP_STATUS(404, NOT_FOUND, Not Found)
31+HTTP_STATUS(405, METHOD_NOT_ALLOWED, Method Not Allowed)
32+HTTP_STATUS(406, NOT_ACCEPTABLE, Not Acceptable)
33+HTTP_STATUS(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required)
34+HTTP_STATUS(408, REQUEST_TIMEOUT, Request Timeout)
35+HTTP_STATUS(409, CONFLICT, Conflict)
36+HTTP_STATUS(410, GONE, Gone)
37+HTTP_STATUS(421, MISDIRECTED_REQUEST, Misdirected Request)
38+HTTP_STATUS(422, UNPROCESSABLE_CONTENT, Unprocessable Content)
39+HTTP_STATUS(499, CLIENT_CLOSED_REQUEST, Client Closed Request)
40+HTTP_STATUS(500, INTERNAL_SERVER_ERROR, Internal Server Error)
41+HTTP_STATUS(501, NOT_IMPLEMENTED, Not Implemented)
42+HTTP_STATUS(502, BAD_GATEWAY, Bad Gateway)
43+HTTP_STATUS(503, SERVICE_UNAVAILABLE, Service Unavailable)
44+HTTP_STATUS(504, GATEWAY_TIMEOUT, Gateway Timeout)
45+HTTP_STATUS(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported)
46+HTTP_STATUS(507, INSUFFICIENT_STORAGE, Insufficient Storage)
+24, -14
  1@@ -217,17 +217,17 @@ int server_get_handler(xs_dict *req, const char *q_path,
  2             *body = greeting_html();
  3 
  4         if (*body)
  5-            status = 200;
  6+            status = HTTP_STATUS_OK;
  7     }
  8     else
  9     if (strcmp(q_path, "/susie.png") == 0 || strcmp(q_path, "/favicon.ico") == 0 ) {
 10-        status = 200;
 11+        status = HTTP_STATUS_OK;
 12         *body  = xs_base64_dec(default_avatar_base64(), b_size);
 13         *ctype = "image/png";
 14     }
 15     else
 16     if (strcmp(q_path, "/.well-known/nodeinfo") == 0) {
 17-        status = 200;
 18+        status = HTTP_STATUS_OK;
 19         *ctype = "application/json; charset=utf-8";
 20         *body  = xs_fmt("{\"links\":["
 21             "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\","
 22@@ -236,7 +236,7 @@ int server_get_handler(xs_dict *req, const char *q_path,
 23     }
 24     else
 25     if (strcmp(q_path, "/.well-known/host-meta") == 0) {
 26-        status = 200;
 27+        status = HTTP_STATUS_OK;
 28         *ctype = "application/xrd+xml";
 29         *body  = xs_fmt("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
 30                 "<XRD>"
 31@@ -245,13 +245,13 @@ int server_get_handler(xs_dict *req, const char *q_path,
 32     }
 33     else
 34     if (strcmp(q_path, "/nodeinfo_2_0") == 0) {
 35-        status = 200;
 36+        status = HTTP_STATUS_OK;
 37         *ctype = "application/json; charset=utf-8";
 38         *body  = nodeinfo_2_0();
 39     }
 40     else
 41     if (strcmp(q_path, "/robots.txt") == 0) {
 42-        status = 200;
 43+        status = HTTP_STATUS_OK;
 44         *ctype = "text/plain";
 45         *body  = xs_str_new("User-agent: *\n"
 46                             "Disallow: /\n");
 47@@ -360,10 +360,20 @@ void httpd_connection(FILE *f)
 48                         payload, p_size, &body, &b_size, &ctype);
 49 #endif
 50 
 51+    }
 52+    else
 53+    if (strcmp(method, "PATCH") == 0) {
 54+
 55+#ifndef NO_MASTODON_API
 56+        if (status == 0)
 57+            status = mastoapi_patch_handler(req, q_path,
 58+                        payload, p_size, &body, &b_size, &ctype);
 59+#endif
 60+
 61     }
 62     else
 63     if (strcmp(method, "OPTIONS") == 0) {
 64-        status = 200;
 65+        status = HTTP_STATUS_OK;
 66     }
 67     else
 68     if (strcmp(method, "DELETE") == 0) {
 69@@ -378,22 +388,22 @@ void httpd_connection(FILE *f)
 70     if (status == 0) {
 71         srv_archive_error("unattended_method", "unattended method", req, payload);
 72         srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path));
 73-        status = 404;
 74+        status = HTTP_STATUS_NOT_FOUND;
 75     }
 76 
 77-    if (status == 403)
 78+    if (status == HTTP_STATUS_FORBIDDEN)
 79         body = xs_str_new("<h1>403 Forbidden</h1>");
 80 
 81-    if (status == 404)
 82+    if (status == HTTP_STATUS_NOT_FOUND)
 83         body = xs_str_new("<h1>404 Not Found</h1>");
 84 
 85-    if (status == 400 && body != NULL)
 86+    if (status == HTTP_STATUS_BAD_REQUEST && body != NULL)
 87         body = xs_str_new("<h1>400 Bad Request</h1>");
 88 
 89-    if (status == 303)
 90+    if (status == HTTP_STATUS_SEE_OTHER)
 91         headers = xs_dict_append(headers, "location", body);
 92 
 93-    if (status == 401) {
 94+    if (status == HTTP_STATUS_UNAUTHORIZED) {
 95         xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"",
 96                                 body, xs_dict_get(srv_config, "host"));
 97 
 98@@ -432,7 +442,7 @@ void httpd_connection(FILE *f)
 99     if (p_state->use_fcgi)
100         xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
101     else
102-        xs_httpd_response(f, status, headers, body, b_size);
103+        xs_httpd_response(f, status, http_status_text(status), headers, body, b_size);
104 
105     fclose(f);
106 
+354, -172
   1@@ -32,9 +32,9 @@ int app_add(const char *id, const xs_dict *app)
   2 /* stores an app */
   3 {
   4     if (!xs_is_hex(id))
   5-        return 500;
   6+        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
   7 
   8-    int status = 201;
   9+    int status = HTTP_STATUS_CREATED;
  10     xs *fn     = xs_fmt("%s/app/", srv_basedir);
  11     FILE *f;
  12 
  13@@ -47,7 +47,7 @@ int app_add(const char *id, const xs_dict *app)
  14         fclose(f);
  15     }
  16     else
  17-        status = 500;
  18+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
  19 
  20     return status;
  21 }
  22@@ -95,9 +95,9 @@ int token_add(const char *id, const xs_dict *token)
  23 /* stores a token */
  24 {
  25     if (!xs_is_hex(id))
  26-        return 500;
  27+        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
  28 
  29-    int status = 201;
  30+    int status = HTTP_STATUS_CREATED;
  31     xs *fn     = xs_fmt("%s/token/", srv_basedir);
  32     FILE *f;
  33 
  34@@ -110,7 +110,7 @@ int token_add(const char *id, const xs_dict *token)
  35         fclose(f);
  36     }
  37     else
  38-        status = 500;
  39+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
  40 
  41     return status;
  42 }
  43@@ -174,7 +174,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
  44     if (!xs_startswith(q_path, "/oauth/"))
  45         return 0;
  46 
  47-    int status   = 404;
  48+    int status   = HTTP_STATUS_NOT_FOUND;
  49     const xs_dict *msg = xs_dict_get(req, "q_vars");
  50     xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
  51 
  52@@ -186,7 +186,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
  53         const char *rtype = xs_dict_get(msg, "response_type");
  54         const char *state = xs_dict_get(msg, "state");
  55 
  56-        status = 400;
  57+        status = HTTP_STATUS_BAD_REQUEST;
  58 
  59         if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
  60             xs *app = app_get(cid);
  61@@ -201,7 +201,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
  62                 *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
  63                                 ruri, cid, state, USER_AGENT);
  64                 *ctype = "text/html";
  65-                status = 200;
  66+                status = HTTP_STATUS_OK;
  67 
  68                 srv_debug(1, xs_fmt("oauth authorize: generating login page"));
  69             }
  70@@ -219,7 +219,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
  71         *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
  72                         "", "", "", USER_AGENT);
  73         *ctype = "text/html";
  74-        status = 200;
  75+        status = HTTP_STATUS_OK;
  76 
  77     }
  78 
  79@@ -237,7 +237,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
  80     if (!xs_startswith(q_path, "/oauth/"))
  81         return 0;
  82 
  83-    int status   = 404;
  84+    int status   = HTTP_STATUS_NOT_FOUND;
  85 
  86     const char *i_ctype = xs_dict_get(req, "content-type");
  87     xs *args      = NULL;
  88@@ -255,7 +255,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
  89         args = xs_dup(xs_dict_get(req, "p_vars"));
  90 
  91     if (args == NULL)
  92-        return 400;
  93+        return HTTP_STATUS_BAD_REQUEST;
  94 
  95     xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
  96 
  97@@ -274,7 +274,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
  98         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
  99                         redir, cid, state, USER_AGENT);
 100         *ctype = "text/html";
 101-        status = 200;
 102+        status = HTTP_STATUS_OK;
 103 
 104         if (login && passwd && redir && cid) {
 105             snac snac;
 106@@ -296,7 +296,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 107                         else
 108                             *body = xs_fmt("%s?code=%s", redir, code);
 109 
 110-                        status = 303;
 111+                        status = HTTP_STATUS_SEE_OTHER;
 112                     }
 113 
 114                     /* if there is a state, add it */
 115@@ -375,12 +375,12 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 116             xs *app = app_get(cid);
 117 
 118             if (app == NULL) {
 119-                status = 401;
 120+                status = HTTP_STATUS_UNAUTHORIZED;
 121                 srv_log(xs_fmt("oauth token: invalid app %s", cid));
 122             }
 123             else
 124             if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
 125-                status = 401;
 126+                status = HTTP_STATUS_UNAUTHORIZED;
 127                 srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
 128             }
 129             else {
 130@@ -397,7 +397,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 131 
 132                 *body  = xs_json_dumps(rsp, 4);
 133                 *ctype = "application/json";
 134-                status = 200;
 135+                status = HTTP_STATUS_OK;
 136 
 137                 const char *uid = xs_dict_get(app, "uid");
 138 
 139@@ -416,7 +416,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 140         }
 141         else {
 142             srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
 143-            status = 400;
 144+            status = HTTP_STATUS_BAD_REQUEST;
 145         }
 146     }
 147     else
 148@@ -433,12 +433,12 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 149 
 150             if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
 151                 srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
 152-                status = 403;
 153+                status = HTTP_STATUS_FORBIDDEN;
 154             }
 155             else {
 156                 token_del(tokid);
 157                 srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
 158-                status = 200;
 159+                status = HTTP_STATUS_OK;
 160 
 161                 /* also delete the app, as it serves no purpose from now on */
 162                 app_del(cid);
 163@@ -446,7 +446,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 164         }
 165         else {
 166             srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
 167-            status = 403;
 168+            status = HTTP_STATUS_FORBIDDEN;
 169         }
 170     }
 171     if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
 172@@ -459,7 +459,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 173         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
 174                         "", "", "", USER_AGENT);
 175         *ctype = "text/html";
 176-        status = 200;
 177+        status = HTTP_STATUS_OK;
 178 
 179         if (login && passwd) {
 180             snac user;
 181@@ -1150,109 +1150,123 @@ int process_auth_token(snac *snac, const xs_dict *req)
 182     return logged_in;
 183 }
 184 
 185-
 186-int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 187-                         char **body, int *b_size, char **ctype)
 188+void credentials_get(char **body, char **ctype, int *status, snac snac)
 189 {
 190-    (void)b_size;
 191+    xs *acct = xs_dict_new();
 192+
 193+    acct = xs_dict_append(acct, "id", snac.md5);
 194+    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
 195+    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
 196+    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
 197+    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
 198+    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
 199+    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
 200+    acct = xs_dict_append(acct, "url", snac.actor);
 201+    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
 202+    acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
 203 
 204-    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 205-        return 0;
 206+    xs *src = xs_json_loads("{\"privacy\":\"public\","
 207+        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
 208+    /* some apps take the note from the source object */
 209+    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
 210+    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
 211 
 212-    int status    = 404;
 213-    const xs_dict *args = xs_dict_get(req, "q_vars");
 214-    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
 215+    const xs_str *cw = xs_dict_get(snac.config, "cw");
 216+    src = xs_dict_set(src, "sensitive",
 217+        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 218 
 219-    snac snac1 = {0};
 220-    int logged_in = process_auth_token(&snac1, req);
 221+    src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
 222 
 223-    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
 224-        if (logged_in) {
 225-            xs *acct = xs_dict_new();
 226-
 227-            acct = xs_dict_append(acct, "id",           snac1.md5);
 228-            acct = xs_dict_append(acct, "username",     xs_dict_get(snac1.config, "uid"));
 229-            acct = xs_dict_append(acct, "acct",         xs_dict_get(snac1.config, "uid"));
 230-            acct = xs_dict_append(acct, "display_name", xs_dict_get(snac1.config, "name"));
 231-            acct = xs_dict_append(acct, "created_at",   xs_dict_get(snac1.config, "published"));
 232-            acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
 233-            acct = xs_dict_append(acct, "note",         xs_dict_get(snac1.config, "bio"));
 234-            acct = xs_dict_append(acct, "url",          snac1.actor);
 235-            acct = xs_dict_append(acct, "locked",       xs_stock(XSTYPE_FALSE));
 236-            acct = xs_dict_append(acct, "bot",          xs_dict_get(snac1.config, "bot"));
 237-
 238-            xs *src = xs_json_loads("{\"privacy\":\"public\","
 239-                    "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
 240-            acct = xs_dict_append(acct, "source", src);
 241-
 242-            xs *avatar = NULL;
 243-            const char *av   = xs_dict_get(snac1.config, "avatar");
 244-
 245-            if (xs_is_null(av) || *av == '\0')
 246-                avatar = xs_fmt("%s/susie.png", srv_baseurl);
 247-            else
 248-                avatar = xs_dup(av);
 249+    xs *avatar = NULL;
 250+    const char *av = xs_dict_get(snac.config, "avatar");
 251 
 252-            acct = xs_dict_append(acct, "avatar", avatar);
 253-            acct = xs_dict_append(acct, "avatar_static", avatar);
 254+    if (xs_is_null(av) || *av == '\0')
 255+        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 256+    else
 257+        avatar = xs_dup(av);
 258 
 259-            xs *header = NULL;
 260-            const char *hd = xs_dict_get(snac1.config, "header");
 261+    acct = xs_dict_append(acct, "avatar", avatar);
 262+    acct = xs_dict_append(acct, "avatar_static", avatar);
 263 
 264-            if (!xs_is_null(hd))
 265-                header = xs_dup(hd);
 266-            else
 267-                header = xs_fmt("%s/header.png", srv_baseurl);
 268+    xs *header = NULL;
 269+    const char *hd = xs_dict_get(snac.config, "header");
 270 
 271-            acct = xs_dict_append(acct, "header",        header);
 272-            acct = xs_dict_append(acct, "header_static", header);
 273+    if (!xs_is_null(hd))
 274+        header = xs_dup(hd);
 275+    else
 276+        header = xs_fmt("%s/header.png", srv_baseurl);
 277 
 278-            const xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
 279-            if (xs_type(metadata) == XSTYPE_DICT) {
 280-                xs *fields = xs_list_new();
 281-                const xs_str *k;
 282-                const xs_str *v;
 283+    acct = xs_dict_append(acct, "header", header);
 284+    acct = xs_dict_append(acct, "header_static", header);
 285 
 286-                xs_dict *val_links = snac1.links;
 287-                if (xs_is_null(val_links))
 288-                    val_links = xs_stock(XSTYPE_DICT);
 289+    const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
 290+    if (xs_type(metadata) == XSTYPE_DICT) {
 291+        xs *fields = xs_list_new();
 292+        const xs_str *k;
 293+        const xs_str *v;
 294 
 295-                int c = 0;
 296-                while (xs_dict_next(metadata, &k, &v, &c)) {
 297-                    xs *val_date = NULL;
 298+        xs_dict *val_links = snac.links;
 299+        if (xs_is_null(val_links))
 300+            val_links = xs_stock(XSTYPE_DICT);
 301 
 302-                    const xs_number *verified_time = xs_dict_get(val_links, v);
 303-                    if (xs_type(verified_time) == XSTYPE_NUMBER) {
 304-                        time_t t = xs_number_get(verified_time);
 305+        int c = 0;
 306+        while (xs_dict_next(metadata, &k, &v, &c)) {
 307+            xs *val_date = NULL;
 308 
 309-                        if (t > 0)
 310-                            val_date = xs_str_utctime(t, ISO_DATE_SPEC);
 311-                    }
 312+            const xs_number *verified_time = xs_dict_get(val_links, v);
 313+            if (xs_type(verified_time) == XSTYPE_NUMBER) {
 314+                time_t t = xs_number_get(verified_time);
 315 
 316-                    xs *d = xs_dict_new();
 317+                if (t > 0)
 318+                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
 319+            }
 320 
 321-                    d = xs_dict_append(d, "name", k);
 322-                    d = xs_dict_append(d, "value", v);
 323-                    d = xs_dict_append(d, "verified_at",
 324-                        xs_type(val_date) == XSTYPE_STRING && *val_date ?
 325-                            val_date : xs_stock(XSTYPE_NULL));
 326+            xs *d = xs_dict_new();
 327 
 328-                    fields = xs_list_append(fields, d);
 329-                }
 330+            d = xs_dict_append(d, "name", k);
 331+            d = xs_dict_append(d, "value", v);
 332+            d = xs_dict_append(d, "verified_at",
 333+                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
 334 
 335-                acct = xs_dict_set(acct, "fields", fields);
 336-            }
 337+            fields = xs_list_append(fields, d);
 338+        }
 339 
 340-            acct = xs_dict_append(acct, "followers_count", xs_stock(0));
 341-            acct = xs_dict_append(acct, "following_count", xs_stock(0));
 342-            acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
 343+        acct = xs_dict_set(acct, "fields", fields);
 344+        /* some apps take the fields from the source object */
 345+        src = xs_dict_set(src, "fields", fields);
 346+    }
 347 
 348-            *body  = xs_json_dumps(acct, 4);
 349-            *ctype = "application/json";
 350-            status = 200;
 351+    acct = xs_dict_append(acct, "source", src);
 352+    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
 353+    acct = xs_dict_append(acct, "following_count", xs_stock(0));
 354+    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
 355+
 356+    *body = xs_json_dumps(acct, 4);
 357+    *ctype = "application/json";
 358+    *status = HTTP_STATUS_OK;
 359+}
 360+
 361+int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 362+                         char **body, int *b_size, char **ctype)
 363+{
 364+    (void)b_size;
 365+
 366+    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 367+        return 0;
 368+
 369+    int status    = HTTP_STATUS_NOT_FOUND;
 370+    const xs_dict *args = xs_dict_get(req, "q_vars");
 371+    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
 372+
 373+    snac snac1 = {0};
 374+    int logged_in = process_auth_token(&snac1, req);
 375+
 376+    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
 377+        if (logged_in) {
 378+            credentials_get(body, ctype, &status, snac1);
 379         }
 380         else {
 381-            status = 422;   // "Unprocessable entity" (no login)
 382+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
 383         }
 384     }
 385     else
 386@@ -1279,10 +1293,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 387 
 388             *body  = xs_json_dumps(res, 4);
 389             *ctype = "application/json";
 390-            status = 200;
 391+            status = HTTP_STATUS_OK;
 392         }
 393         else
 394-            status = 422;
 395+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
 396     }
 397     else
 398     if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
 399@@ -1304,7 +1318,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 400 
 401                     *body  = xs_json_dumps(macct, 4);
 402                     *ctype = "application/json";
 403-                    status = 200;
 404+                    status = HTTP_STATUS_OK;
 405 
 406                     user_free(&user);
 407                 }
 408@@ -1429,6 +1443,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 409                         }
 410                     }
 411                 }
 412+                else
 413+                if (strcmp(opt, "featured_tags") == 0) {
 414+                    /* snac doesn't have features tags, yet? */
 415+                    /* implement empty response so apps like Tokodon don't show an error */
 416+                    *body  = xs_dup("[]");
 417+                    *ctype = "application/json";
 418+                    status = HTTP_STATUS_OK;
 419+                }
 420 
 421                 user_free(&snac2);
 422             }
 423@@ -1441,16 +1463,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 424                     }
 425                     else
 426                     if (strcmp(opt, "statuses") == 0) {
 427-                        /* we don't serve statuses of others; return the empty list */
 428+                        /* we don't serve statuses of others; return the empty list */ 
 429                         out = xs_list_new();
 430                     }
 431+                    else
 432+                    if (strcmp(opt, "featured_tags") == 0) {
 433+                        /* snac doesn't have features tags, yet? */
 434+                        /* implement empty response so apps like Tokodon don't show an error */
 435+                        *body  = xs_dup("[]");
 436+                        *ctype = "application/json";
 437+                        status = HTTP_STATUS_OK;
 438+                    }
 439                 }
 440             }
 441 
 442             if (out != NULL) {
 443                 *body  = xs_json_dumps(out, 4);
 444                 *ctype = "application/json";
 445-                status = 200;
 446+                status = HTTP_STATUS_OK;
 447             }
 448         }
 449     }
 450@@ -1554,12 +1584,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 451 
 452             *body  = xs_json_dumps(out, 4);
 453             *ctype = "application/json";
 454-            status = 200;
 455+            status = HTTP_STATUS_OK;
 456 
 457             srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
 458         }
 459         else {
 460-            status = 401; // unauthorized
 461+            status = HTTP_STATUS_UNAUTHORIZED;
 462         }
 463     }
 464     else
 465@@ -1612,7 +1642,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 466 
 467         *body  = xs_json_dumps(out, 4);
 468         *ctype = "application/json";
 469-        status = 200;
 470+        status = HTTP_STATUS_OK;
 471     }
 472     else
 473     if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
 474@@ -1661,7 +1691,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 475 
 476         *body  = xs_json_dumps(out, 4);
 477         *ctype = "application/json";
 478-        status = 200;
 479+        status = HTTP_STATUS_OK;
 480     }
 481     else
 482     if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
 483@@ -1729,17 +1759,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 484 
 485             *body  = xs_json_dumps(out, 4);
 486             *ctype = "application/json";
 487-            status = 200;
 488+            status = HTTP_STATUS_OK;
 489         }
 490         else
 491-            status = 421;
 492+            status = HTTP_STATUS_MISDIRECTED_REQUEST;
 493     }
 494     else
 495     if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
 496         /* TBD */
 497         *body  = xs_dup("[]");
 498         *ctype = "application/json";
 499-        status = 200;
 500+        status = HTTP_STATUS_OK;
 501     }
 502     else
 503     if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
 504@@ -1817,17 +1847,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 505 
 506             *body  = xs_json_dumps(out, 4);
 507             *ctype = "application/json";
 508-            status = 200;
 509+            status = HTTP_STATUS_OK;
 510         }
 511         else
 512-            status = 401;
 513+            status = HTTP_STATUS_UNAUTHORIZED;
 514     }
 515     else
 516     if (strcmp(cmd, "/v1/filters") == 0) { /** **/
 517         /* snac will never have filters */
 518         *body  = xs_dup("[]");
 519         *ctype = "application/json";
 520-        status = 200;
 521+        status = HTTP_STATUS_OK;
 522     }
 523     else
 524     if (strcmp(cmd, "/v2/filters") == 0) { /** **/
 525@@ -1836,21 +1866,21 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 526          * in some apps */
 527         *body  = xs_dup("[]");
 528         *ctype = "application/json";
 529-        status = 200;
 530+        status = HTTP_STATUS_OK;
 531     }
 532     else
 533     if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
 534         /* snac will never support a list of favourites */
 535         *body  = xs_dup("[]");
 536         *ctype = "application/json";
 537-        status = 200;
 538+        status = HTTP_STATUS_OK;
 539     }
 540     else
 541     if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
 542         /* snac does not support bookmarks */
 543         *body  = xs_dup("[]");
 544         *ctype = "application/json";
 545-        status = 200;
 546+        status = HTTP_STATUS_OK;
 547     }
 548     else
 549     if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
 550@@ -1873,7 +1903,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 551 
 552             *body  = xs_json_dumps(l, 4);
 553             *ctype = "application/json";
 554-            status = 200;
 555+            status = HTTP_STATUS_OK;
 556         }
 557     }
 558     else
 559@@ -1903,7 +1933,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 560 
 561                         *body  = xs_json_dumps(out, 4);
 562                         *ctype = "application/json";
 563-                        status = 200;
 564+                        status = HTTP_STATUS_OK;
 565                     }
 566                 }
 567                 else
 568@@ -1931,7 +1961,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 569 
 570                     *body  = xs_json_dumps(out, 4);
 571                     *ctype = "application/json";
 572-                    status = 200;
 573+                    status = HTTP_STATUS_OK;
 574                 }
 575             }
 576         }
 577@@ -1941,28 +1971,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 578         /* snac does not schedule notes */
 579         *body  = xs_dup("[]");
 580         *ctype = "application/json";
 581-        status = 200;
 582+        status = HTTP_STATUS_OK;
 583     }
 584     else
 585     if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
 586         /* snac does not support optional follow confirmations */
 587         *body  = xs_dup("[]");
 588         *ctype = "application/json";
 589-        status = 200;
 590+        status = HTTP_STATUS_OK;
 591     }
 592     else
 593     if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
 594         /* snac has no announcements (yet?) */
 595         *body  = xs_dup("[]");
 596         *ctype = "application/json";
 597-        status = 200;
 598+        status = HTTP_STATUS_OK;
 599     }
 600     else
 601     if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
 602         /* are you kidding me? */
 603         *body  = xs_dup("[]");
 604         *ctype = "application/json";
 605-        status = 200;
 606+        status = HTTP_STATUS_OK;
 607     }
 608     else
 609     if (strcmp(cmd, "/v1/instance") == 0) { /** **/
 610@@ -2017,7 +2047,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 611                 "\"max_characters\":100000,\"max_media_attachments\":8}");
 612             cfg = xs_dict_append(cfg, "statuses", d11);
 613 
 614-            xs *d12 = xs_json_loads("{\"max_featured_tags\":10}");
 615+            xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
 616             cfg = xs_dict_append(cfg, "accounts", d12);
 617 
 618             xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
 619@@ -2075,7 +2105,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 620 
 621         *body  = xs_json_dumps(ins, 4);
 622         *ctype = "application/json";
 623-        status = 200;
 624+        status = HTTP_STATUS_OK;
 625     }
 626     else
 627     if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
 628@@ -2188,30 +2218,30 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 629                 if (out != NULL) {
 630                     *body  = xs_json_dumps(out, 4);
 631                     *ctype = "application/json";
 632-                    status = 200;
 633+                    status = HTTP_STATUS_OK;
 634                 }
 635             }
 636         }
 637         else
 638-            status = 401;
 639+            status = HTTP_STATUS_UNAUTHORIZED;
 640     }
 641     else
 642     if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
 643         *body  = xs_dup("{}");
 644         *ctype = "application/json";
 645-        status = 200;
 646+        status = HTTP_STATUS_OK;
 647     }
 648     else
 649     if (strcmp(cmd, "/v1/markers") == 0) { /** **/
 650         *body  = xs_dup("{}");
 651         *ctype = "application/json";
 652-        status = 200;
 653+        status = HTTP_STATUS_OK;
 654     }
 655     else
 656     if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
 657         *body  = xs_dup("[]");
 658         *ctype = "application/json";
 659-        status = 200;
 660+        status = HTTP_STATUS_OK;
 661     }
 662     else
 663     if (strcmp(cmd, "/v2/search") == 0) { /** **/
 664@@ -2290,10 +2320,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 665 
 666             *body  = xs_json_dumps(res, 4);
 667             *ctype = "application/json";
 668-            status = 200;
 669+            status = HTTP_STATUS_OK;
 670         }
 671         else
 672-            status = 401;
 673+            status = HTTP_STATUS_UNAUTHORIZED;
 674     }
 675 
 676     /* user cleanup */
 677@@ -2316,7 +2346,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 678     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 679         return 0;
 680 
 681-    int status    = 404;
 682+    int status    = HTTP_STATUS_NOT_FOUND;
 683     xs *args      = NULL;
 684     const char *i_ctype = xs_dict_get(req, "content-type");
 685 
 686@@ -2336,7 +2366,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 687         args = xs_dup(xs_dict_get(req, "p_vars"));
 688 
 689     if (args == NULL)
 690-        return 400;
 691+        return HTTP_STATUS_BAD_REQUEST;
 692 
 693     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 694 
 695@@ -2378,7 +2408,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 696 
 697             *body  = xs_json_dumps(app, 4);
 698             *ctype = "application/json";
 699-            status = 200;
 700+            status = HTTP_STATUS_OK;
 701 
 702             app = xs_dict_append(app, "code", "");
 703 
 704@@ -2470,10 +2500,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 705 
 706             *body  = xs_json_dumps(st, 4);
 707             *ctype = "application/json";
 708-            status = 200;
 709+            status = HTTP_STATUS_OK;
 710         }
 711         else
 712-            status = 401;
 713+            status = HTTP_STATUS_UNAUTHORIZED;
 714     }
 715     else
 716     if (xs_startswith(cmd, "/v1/statuses")) { /** **/
 717@@ -2552,7 +2582,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 718                         if (pin(&snac, id))
 719                             out = mastoapi_status(&snac, msg);
 720                         else
 721-                            status = 422;
 722+                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
 723                     }
 724                     else
 725                     if (strcmp(op, "unpin") == 0) { /** **/
 726@@ -2573,12 +2603,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 727                 if (out != NULL) {
 728                     *body  = xs_json_dumps(out, 4);
 729                     *ctype = "application/json";
 730-                    status = 200;
 731+                    status = HTTP_STATUS_OK;
 732                 }
 733             }
 734         }
 735         else
 736-            status = 401;
 737+            status = HTTP_STATUS_UNAUTHORIZED;
 738     }
 739     else
 740     if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
 741@@ -2588,10 +2618,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 742 
 743             *body  = xs_dup("{}");
 744             *ctype = "application/json";
 745-            status = 200;
 746+            status = HTTP_STATUS_OK;
 747         }
 748         else
 749-            status = 401;
 750+            status = HTTP_STATUS_UNAUTHORIZED;
 751     }
 752     else
 753     if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
 754@@ -2616,10 +2646,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 755 
 756             *body  = xs_json_dumps(wpush, 4);
 757             *ctype = "application/json";
 758-            status = 200;
 759+            status = HTTP_STATUS_OK;
 760         }
 761         else
 762-            status = 401;
 763+            status = HTTP_STATUS_UNAUTHORIZED;
 764     }
 765     else
 766     if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
 767@@ -2630,7 +2660,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 768             if (xs_is_null(desc))
 769                 desc = "";
 770 
 771-            status = 400;
 772+            status = HTTP_STATUS_BAD_REQUEST;
 773 
 774             if (xs_type(file) == XSTYPE_LIST) {
 775                 const char *fn = xs_list_get(file, 0);
 776@@ -2659,12 +2689,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 777 
 778                     *body  = xs_json_dumps(rsp, 4);
 779                     *ctype = "application/json";
 780-                    status = 200;
 781+                    status = HTTP_STATUS_OK;
 782                 }
 783             }
 784         }
 785         else
 786-            status = 401;
 787+            status = HTTP_STATUS_UNAUTHORIZED;
 788     }
 789     else
 790     if (xs_startswith(cmd, "/v1/accounts")) { /** **/
 791@@ -2744,11 +2774,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 792             if (rsp != NULL) {
 793                 *body  = xs_json_dumps(rsp, 4);
 794                 *ctype = "application/json";
 795-                status = 200;
 796+                status = HTTP_STATUS_OK;
 797             }
 798         }
 799         else
 800-            status = 401;
 801+            status = HTTP_STATUS_UNAUTHORIZED;
 802     }
 803     else
 804     if (xs_startswith(cmd, "/v1/polls")) { /** **/
 805@@ -2810,12 +2840,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 806                 if (out != NULL) {
 807                     *body  = xs_json_dumps(out, 4);
 808                     *ctype = "application/json";
 809-                    status = 200;
 810+                    status = HTTP_STATUS_OK;
 811                 }
 812             }
 813         }
 814         else
 815-            status = 401;
 816+            status = HTTP_STATUS_UNAUTHORIZED;
 817     }
 818     else
 819     if (strcmp(cmd, "/v1/lists") == 0) {
 820@@ -2831,18 +2861,18 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 821                     out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
 822                     out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
 823 
 824-                    status = 200;
 825+                    status = HTTP_STATUS_OK;
 826                 }
 827                 else {
 828                     out = xs_dict_append(out, "error", "cannot create list");
 829-                    status = 422;
 830+                    status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
 831                 }
 832 
 833                 *body  = xs_json_dumps(out, 4);
 834                 *ctype = "application/json";
 835             }
 836             else
 837-                status = 422;
 838+                status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
 839         }
 840     }
 841     if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
 842@@ -2861,12 +2891,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 843                         list_content(&snac, id, v, 1);
 844                     }
 845 
 846-                    status = 200;
 847+                    status = HTTP_STATUS_OK;
 848                 }
 849             }
 850         }
 851         else
 852-            status = 422;
 853+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
 854     }
 855 
 856     /* user cleanup */
 857@@ -2891,7 +2921,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 858     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 859         return 0;
 860 
 861-    int status    = 404;
 862+    int status    = HTTP_STATUS_NOT_FOUND;
 863     xs *args      = NULL;
 864     const char *i_ctype = xs_dict_get(req, "content-type");
 865 
 866@@ -2911,7 +2941,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 867         args = xs_dup(xs_dict_get(req, "p_vars"));
 868 
 869     if (args == NULL)
 870-        return 400;
 871+        return HTTP_STATUS_BAD_REQUEST;
 872 
 873     snac snac = {0};
 874     int logged_in = process_auth_token(&snac, req);
 875@@ -2920,7 +2950,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 876 
 877     if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
 878         // pretend we deleted it, since it doesn't exist anyway
 879-        status = 200;
 880+        status = HTTP_STATUS_OK;
 881     }
 882     else
 883     if (xs_startswith(cmd, "/v1/lists/")) {
 884@@ -2948,10 +2978,10 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 885                 }
 886             }
 887 
 888-            status = 200;
 889+            status = HTTP_STATUS_OK;
 890         }
 891         else
 892-            status = 401;
 893+            status = HTTP_STATUS_UNAUTHORIZED;
 894     }
 895 
 896     /* user cleanup */
 897@@ -2974,7 +3004,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 898     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
 899         return 0;
 900 
 901-    int status    = 404;
 902+    int status    = HTTP_STATUS_NOT_FOUND;
 903     xs *args      = NULL;
 904     const char *i_ctype = xs_dict_get(req, "content-type");
 905 
 906@@ -2986,7 +3016,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 907         args = xs_dup(xs_dict_get(req, "p_vars"));
 908 
 909     if (args == NULL)
 910-        return 400;
 911+        return HTTP_STATUS_BAD_REQUEST;
 912 
 913     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 914 
 915@@ -3017,11 +3047,11 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 916 
 917                 *body  = xs_json_dumps(rsp, 4);
 918                 *ctype = "application/json";
 919-                status = 200;
 920+                status = HTTP_STATUS_OK;
 921             }
 922         }
 923         else
 924-            status = 401;
 925+            status = HTTP_STATUS_UNAUTHORIZED;
 926     }
 927     else
 928     if (xs_startswith(cmd, "/v1/statuses")) {
 929@@ -3060,12 +3090,12 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 930                 if (rsp != NULL) {
 931                     *body  = xs_json_dumps(rsp, 4);
 932                     *ctype = "application/json";
 933-                    status = 200;
 934+                    status = HTTP_STATUS_OK;
 935                 }
 936             }
 937         }
 938         else
 939-            status = 401;
 940+            status = HTTP_STATUS_UNAUTHORIZED;
 941     }
 942 
 943     /* user cleanup */
 944@@ -3077,6 +3107,158 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 945     return status;
 946 }
 947 
 948+void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
 949+/* Store header or avatar */
 950+{
 951+    if (data != NULL) {
 952+        if (xs_type(data) == XSTYPE_LIST) {
 953+            const char *fn = xs_list_get(data, 0);
 954+
 955+            if (fn && *fn) {
 956+                const char *ext = strrchr(fn, '.');
 957+
 958+                /* Mona iOS sends always jpg as application/octet-stream with no filename */
 959+                if (ext == NULL || strcmp(fn, key) == 0) {
 960+                    ext = ".jpg";
 961+                }
 962+
 963+                /* Make sure we have a unique file name, otherwise updated images will not be
 964+                 * re-loaded by clients. */
 965+                xs *rnd         = random_str();
 966+                xs *hash        = xs_md5_hex(rnd, strlen(rnd));
 967+                xs *id          = xs_fmt("%s%s", hash, ext);
 968+                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
 969+                int fo          = xs_number_get(xs_list_get(data, 1));
 970+                int fs          = xs_number_get(xs_list_get(data, 2));
 971+
 972+                /* store */
 973+                static_put(snac, id, payload + fo, fs);
 974+
 975+                snac->config = xs_dict_set(snac->config, key, url);
 976+            }
 977+        }
 978+    }
 979+}
 980+
 981+int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
 982+                          const char *payload, int p_size,
 983+                          char **body, int *b_size, char **ctype)
 984+/* Handle profile updates */
 985+{
 986+    (void)p_size;
 987+    (void)b_size;
 988+
 989+    if (!xs_startswith(q_path, "/api/v1/"))
 990+        return 0;
 991+
 992+    int status    = HTTP_STATUS_NOT_FOUND;
 993+    xs *args      = NULL;
 994+    const char *i_ctype = xs_dict_get(req, "content-type");
 995+
 996+    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
 997+        if (!xs_is_null(payload))
 998+            args = xs_json_loads(payload);
 999+    }
1000+    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
1001+    {
1002+        // Some apps send form data instead of json so we should cater for those
1003+        if (!xs_is_null(payload)) {
1004+            xs *upl = xs_url_dec(payload);
1005+            args    = xs_url_vars(upl);
1006+        }
1007+    }
1008+    else
1009+        args = xs_dup(xs_dict_get(req, "p_vars"));
1010+
1011+    if (args == NULL)
1012+        return HTTP_STATUS_BAD_REQUEST;
1013+
1014+    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
1015+
1016+    snac snac = {0};
1017+    int logged_in = process_auth_token(&snac, req);
1018+
1019+    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
1020+        /* Update user profile fields */
1021+        if (logged_in) {
1022+            int c = 0;
1023+            const xs_str *k;
1024+            const xs_val *v;
1025+            const xs_str *field_name = NULL;
1026+            xs_dict *new_fields = xs_dict_new();
1027+            while (xs_dict_next(args, &k, &v, &c)) {
1028+                if (strcmp(k, "display_name") == 0) {
1029+                    if (v != NULL)
1030+                        snac.config = xs_dict_set(snac.config, "name", v);
1031+                }
1032+                else
1033+                if (strcmp(k, "note") == 0) {
1034+                    if (v != NULL)
1035+                        snac.config = xs_dict_set(snac.config, "bio", v);
1036+                }
1037+                else
1038+                if (strcmp(k, "bot") == 0) {
1039+                    if (v != NULL)
1040+                        snac.config = xs_dict_set(snac.config, "bot",
1041+                            (strcmp(v, "true") == 0 ||
1042+                                strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1043+                }
1044+                else
1045+                if (strcmp(k, "source[sensitive]") == 0) {
1046+                    if (v != NULL)
1047+                        snac.config = xs_dict_set(snac.config, "cw",
1048+                            strcmp(v, "true") == 0 ? "open" : "");
1049+                }
1050+                else
1051+                if (strcmp(k, "source[privacy]") == 0) {
1052+                    if (v != NULL)
1053+                        snac.config = xs_dict_set(snac.config, "private",
1054+                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1055+                }
1056+                else
1057+                if (strcmp(k, "header") == 0) {
1058+                    persist_image("header", v, payload, &snac);
1059+                }
1060+                else
1061+                if (strcmp(k, "avatar") == 0) {
1062+                    persist_image("avatar", v, payload, &snac);
1063+                }
1064+                else
1065+                if (xs_starts_and_ends("fields_attributes", k, "[name]")) {
1066+                    field_name = strcmp(v, "") != 0 ? v : NULL;
1067+                }
1068+                else
1069+                if (xs_starts_and_ends("fields_attributes", k, "[value]")) {
1070+                    if (field_name != NULL) {
1071+                        new_fields = xs_dict_set(new_fields, field_name, v);
1072+                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
1073+                    }
1074+                }
1075+                /* we don't have support for the following options, yet
1076+                   - discoverable (0/1)
1077+                   - locked (0/1)
1078+                 */
1079+            }
1080+
1081+            /* Persist profile */
1082+            if (user_persist(&snac) == 0)
1083+                credentials_get(body, ctype, &status, snac);
1084+            else
1085+                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
1086+        }
1087+        else
1088+            status = HTTP_STATUS_UNAUTHORIZED;
1089+    }
1090+
1091+    /* user cleanup */
1092+    if (logged_in)
1093+        user_free(&snac);
1094+
1095+    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
1096+
1097+    return status;
1098+}
1099+
1100 
1101 void mastoapi_purge(void)
1102 {
M snac.c
+12, -0
 1@@ -170,3 +170,15 @@ int check_password(const char *uid, const char *passwd, const char *hash)
 2 
 3     return ret;
 4 }
 5+
 6+
 7+const char *http_status_text(int status)
 8+/* translate status codes to canonical status texts */
 9+{
10+    switch (status) {
11+#define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text;
12+#include "http_codes.h"
13+#undef HTTP_STATUS
14+        default: return "Unknown";
15+    }
16+}
M snac.h
+14, -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,19 @@ 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);
20+
21+
22+typedef enum {
23+#define HTTP_STATUS(code, name, text) HTTP_STATUS_ ## name = code,
24+#include "http_codes.h"
25+#undef HTTP_STATUS
26+} http_status;
27+
28+const char *http_status_text(int status);
+4, -4
 1@@ -42,7 +42,7 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
 2     }
 3 
 4     if (host == NULL || resource == NULL)
 5-        return 400;
 6+        return HTTP_STATUS_BAD_REQUEST;
 7 
 8     headers = xs_dict_append(headers, "accept",     "application/json");
 9     headers = xs_dict_append(headers, "user-agent", USER_AGENT);
10@@ -139,7 +139,7 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
11     const char *resource = xs_dict_get(q_vars, "resource");
12 
13     if (resource == NULL)
14-        return 400;
15+        return HTTP_STATUS_BAD_REQUEST;
16 
17     snac snac;
18     int found = 0;
19@@ -220,12 +220,12 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
20 
21         user_free(&snac);
22 
23-        status = 200;
24+        status = HTTP_STATUS_OK;
25         *body  = j;
26         *ctype = "application/jrd+json";
27     }
28     else
29-        status = 404;
30+        status = HTTP_STATUS_NOT_FOUND;
31 
32     srv_debug(1, xs_fmt("webfinger_get_handler resource=%s %d", resource, status));
33 
M xs.h
+1, -1
1@@ -52,7 +52,7 @@ typedef char xs_data;
2 
3 void *xs_free(void *ptr);
4 void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
5-#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__)
6+#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __func__)
7 int _xs_blk_size(int sz);
8 void _xs_destroy(char **var);
9 #define xs_debug() raise(SIGTRAP)
+3, -3
 1@@ -5,7 +5,7 @@
 2 #define _XS_HTTPD_H
 3 
 4 xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size);
 5-void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size);
 6+void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size);
 7 
 8 
 9 #ifdef XS_IMPLEMENTATION
10@@ -95,14 +95,14 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
11 }
12 
13 
14-void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size)
15+void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size)
16 /* sends an httpd response */
17 {
18     xs *proto;
19     const xs_str *k;
20     const xs_val *v;
21 
22-    proto = xs_fmt("HTTP/1.1 %d %s", status, status / 100 == 2 ? "OK" : "ERROR");
23+    proto = xs_fmt("HTTP/1.1 %d %s", status, status_text);
24     fprintf(f, "%s\r\n", proto);
25 
26     int c = 0;
+1, -1
1@@ -111,4 +111,4 @@ int xs_set_add(xs_set *s, const xs_val *data)
2 
3 #endif /* XS_IMPLEMENTATION */
4 
5-#endif /* XS_SET_H */
6+#endif /* XS_SET_H */
+50, -22
  1@@ -8,7 +8,6 @@ xs_str *xs_url_dec(const char *str);
  2 xs_dict *xs_url_vars(const char *str);
  3 xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
  4 
  5-
  6 #ifdef XS_IMPLEMENTATION
  7 
  8 xs_str *xs_url_dec(const char *str)
  9@@ -107,7 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
 10         if (xs_list_len(l1) != 2)
 11             return NULL;
 12 
 13-        boundary = xs_fmt("--%s", xs_list_get(l1, 1));
 14+        boundary = xs_dup(xs_list_get(l1, 1));
 15+
 16+        /* Tokodon sends the boundary header with double quotes surrounded */
 17+        if (xs_starts_and_ends("\"", boundary, "\"") != 0)
 18+            boundary = xs_strip_chars_i(boundary, "\"");
 19+
 20+        boundary = xs_fmt("--%s", boundary);
 21     }
 22 
 23     bsz = strlen(boundary);
 24@@ -120,6 +125,7 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
 25         xs *l1 = NULL;
 26         const char *vn = NULL;
 27         const char *fn = NULL;
 28+        const char *ct = NULL;
 29         char *q;
 30         int po, ps;
 31 
 32@@ -132,32 +138,47 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
 33         /* skip the \r\n */
 34         p += 2;
 35 
 36-        /* now on a Content-Disposition... line; get it */
 37-        q = strchr(p, '\r');
 38-        s1 = xs_realloc(NULL, q - p + 1);
 39-        memcpy(s1, p, q - p);
 40-        s1[q - p] = '\0';
 41-
 42-        /* move on (over a \r\n) */
 43-        p = q;
 44-
 45-        /* split by " like a primitive man */
 46-        l1 = xs_split(s1, "\"");
 47+        /* Tokodon sends also a Content-Type headers,
 48+           let's use it to determine the file type */
 49+        do {
 50+            if (p[0] == 13 && p[1] == 10)
 51+                break;
 52+            q = strchr(p, '\r');
 53+            s1 = xs_realloc(NULL, q - p + 1);
 54+            memcpy(s1, p, q - p);
 55+            s1[q - p] = '\0';
 56+
 57+            if (xs_startswith(s1, "Content-Disposition")) {
 58+                /* split by " like a primitive man */
 59+                l1 = xs_split(s1, "\"");
 60+
 61+                /* get the variable name */
 62+                vn = xs_list_get(l1, 1);
 63+
 64+                /* is it an attached file? */
 65+                if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
 66+                    /* get the file name */
 67+                    fn = xs_list_get(l1, 3);
 68+                }
 69+            }
 70+            else
 71+            if (xs_startswith(s1, "Content-Type")) {
 72+                l1 = xs_split(s1, ":");
 73 
 74-        /* get the variable name */
 75-        vn = xs_list_get(l1, 1);
 76+                if (xs_list_len(l1) >= 2) {
 77+                    ct = xs_lstrip_chars_i(xs_dup(xs_list_get(l1, 1)), " ");
 78+                }
 79+            }
 80 
 81-        /* is it an attached file? */
 82-        if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
 83-            /* get the file name */
 84-            fn = xs_list_get(l1, 3);
 85-        }
 86+            p += (q - p);
 87+            p += 2; // Skip /r/n
 88+        } while (1);
 89 
 90         /* find the start of the part content */
 91-        if ((p = xs_memmem(p, p_size - (p - payload), "\r\n\r\n", 4)) == NULL)
 92+        if ((p = xs_memmem(p, p_size - (p - payload), "\r\n", 2)) == NULL)
 93             break;
 94 
 95-        p += 4;
 96+        p += 2; // Skip empty line
 97 
 98         /* find the next boundary */
 99         if ((q = xs_memmem(p, p_size - (p - payload), boundary, bsz)) == NULL)
100@@ -169,6 +190,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
101         /* is it a filename? */
102         if (fn != NULL) {
103             /* p_var value is a list */
104+            /* if filename has no extension and content-type is image, attach extension to the filename */
105+            if (strchr(fn, '.') == NULL && xs_startswith(ct, "image/")) {
106+                char *ext = strchr(ct, '/');
107+                ext++;
108+                fn = xs_str_cat(xs_str_new(""), fn, ".", ext);
109+            }
110+
111             xs *l1 = xs_list_new();
112             xs *vpo = xs_number_new(po);
113             xs *vps = xs_number_new(ps);