Ver Fonte

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
grunfink há 2 anos atrás
pai
commit
d56d4beb90
14 ficheiros alterados com 640 adições e 336 exclusões
  1. 1 1
      Makefile
  2. 31 25
      activitypub.c
  3. 61 34
      data.c
  4. 39 59
      html.c
  5. 45 0
      http_codes.h
  6. 24 14
      httpd.c
  7. 354 172
      mastoapi.c
  8. 12 0
      snac.c
  9. 14 0
      snac.h
  10. 4 4
      webfinger.c
  11. 1 1
      xs.h
  12. 3 3
      xs_httpd.h
  13. 1 1
      xs_set.h
  14. 50 22
      xs_url.h

+ 1 - 1
Makefile

@@ -52,7 +52,7 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
 snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode.h xs_json.h xs_curl.h \
 snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode.h xs_json.h xs_curl.h \
  xs_openssl.h xs_socket.h xs_url.h xs_httpd.h xs_mime.h xs_regex.h \
  xs_openssl.h xs_socket.h xs_url.h xs_httpd.h xs_mime.h xs_regex.h \
  xs_set.h xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h \
  xs_set.h xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h \
- snac.h
+ snac.h http_codes.h
 upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h
 upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h
 utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
 utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
  xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h
  xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h

+ 31 - 25
activitypub.c

@@ -96,19 +96,19 @@ int activitypub_request(snac *user, const char *url, xs_dict **data)
         ctype = xs_dict_get(response, "content-type");
         ctype = xs_dict_get(response, "content-type");
 
 
         if (xs_is_null(ctype))
         if (xs_is_null(ctype))
-            status = 400;
+            status = HTTP_STATUS_BAD_REQUEST;
         else
         else
         if (xs_str_in(ctype, "application/activity+json") != -1 ||
         if (xs_str_in(ctype, "application/activity+json") != -1 ||
             xs_str_in(ctype, "application/ld+json") != -1) {
             xs_str_in(ctype, "application/ld+json") != -1) {
 
 
             /* if there is no payload, fail */
             /* if there is no payload, fail */
             if (xs_is_null(payload))
             if (xs_is_null(payload))
-                status = 400;
+                status = HTTP_STATUS_BAD_REQUEST;
             else
             else
                 *data = xs_json_loads(payload);
                 *data = xs_json_loads(payload);
         }
         }
         else
         else
-            status = 500;
+            status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
     }
     }
 
 
     return status;
     return status;
@@ -443,7 +443,7 @@ int send_to_actor(snac *snac, const char *actor, const xs_dict *msg,
                   xs_val **payload, int *p_size, int timeout)
                   xs_val **payload, int *p_size, int timeout)
 /* sends a message to an actor */
 /* sends a message to an actor */
 {
 {
-    int status = 400;
+    int status = HTTP_STATUS_BAD_REQUEST;
     xs *inbox = get_actor_inbox(actor);
     xs *inbox = get_actor_inbox(actor);
 
 
     if (!xs_is_null(inbox))
     if (!xs_is_null(inbox))
@@ -1762,7 +1762,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
     a_status = actor_request(snac, actor, &actor_o);
     a_status = actor_request(snac, actor, &actor_o);
 
 
     /* do not retry permanent failures */
     /* do not retry permanent failures */
-    if (a_status == 404 || a_status == 410 || a_status < 0) {
+    if (a_status == HTTP_STATUS_NOT_FOUND
+        || a_status == HTTP_STATUS_GONE
+        || a_status < 0) {
         srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status));
         srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status));
         return -1;
         return -1;
     }
     }
@@ -1905,7 +1907,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
         }
         }
         else
         else
         if (strcmp(utype, "Announce") == 0) { /** **/
         if (strcmp(utype, "Announce") == 0) { /** **/
-            int status = 200;
+            int status = HTTP_STATUS_OK;
 
 
             /* commented out: if a followed user boosts something that
             /* commented out: if a followed user boosts something that
                is requested and then unboosts, the post remains here,
                is requested and then unboosts, the post remains here,
@@ -2015,7 +2017,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
         if (xs_type(object) == XSTYPE_DICT)
         if (xs_type(object) == XSTYPE_DICT)
             object = xs_dict_get(object, "id");
             object = xs_dict_get(object, "id");
 
 
-        if (timeline_admire(snac, object, actor, 1) == 201)
+        if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED)
             snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
             snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
         else
         else
             snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
             snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
@@ -2046,7 +2048,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
                     xs *who_o = NULL;
                     xs *who_o = NULL;
 
 
                     if (valid_status(actor_request(snac, who, &who_o))) {
                     if (valid_status(actor_request(snac, who, &who_o))) {
-                        if (timeline_admire(snac, object, actor, 0) == 201)
+                        if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
                             snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
                             snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
                         else
                         else
                             snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
                             snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
@@ -2383,11 +2385,15 @@ void process_queue_item(xs_dict *q_item)
 
 
             /* if it's not the first time it fails with a timeout,
             /* if it's not the first time it fails with a timeout,
                penalize the server by skipping one retry */
                penalize the server by skipping one retry */
-            if (p_status == status && status == 499)
+            if (p_status == status && status == HTTP_STATUS_CLIENT_CLOSED_REQUEST)
                 retries++;
                 retries++;
 
 
             /* error sending; requeue? */
             /* error sending; requeue? */
-            if (status == 400 || status == 404 || status == 405 || status == 410 || status < 0)
+            if (status == HTTP_STATUS_BAD_REQUEST
+                || status == HTTP_STATUS_NOT_FOUND
+                || status == HTTP_STATUS_METHOD_NOT_ALLOWED
+                || status == HTTP_STATUS_GONE
+                || status < 0)
                 /* explicit error: discard */
                 /* explicit error: discard */
                 srv_log(xs_fmt("output message: fatal error %s %d", inbox, status));
                 srv_log(xs_fmt("output message: fatal error %s %d", inbox, status));
             else
             else
@@ -2574,7 +2580,7 @@ int process_queue(void)
 int activitypub_get_handler(const xs_dict *req, const char *q_path,
 int activitypub_get_handler(const xs_dict *req, const char *q_path,
                             char **body, int *b_size, char **ctype)
                             char **body, int *b_size, char **ctype)
 {
 {
-    int status = 200;
+    int status = HTTP_STATUS_OK;
     const char *accept = xs_dict_get(req, "accept");
     const char *accept = xs_dict_get(req, "accept");
     snac snac;
     snac snac;
     xs *msg = NULL;
     xs *msg = NULL;
@@ -2594,7 +2600,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
     if (!user_open(&snac, uid)) {
     if (!user_open(&snac, uid)) {
         /* invalid user */
         /* invalid user */
         srv_debug(1, xs_fmt("activitypub_get_handler bad user %s", uid));
         srv_debug(1, xs_fmt("activitypub_get_handler bad user %s", uid));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     p_path = xs_list_get(l, 2);
     p_path = xs_list_get(l, 2);
@@ -2652,12 +2658,12 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
 
 
         /* don't return non-public objects */
         /* don't return non-public objects */
         if (valid_status(status) && !is_msg_public(msg))
         if (valid_status(status) && !is_msg_public(msg))
-            status = 404;
+            status = HTTP_STATUS_NOT_FOUND;
     }
     }
     else
     else
-        status = 404;
+        status = HTTP_STATUS_NOT_FOUND;
 
 
-    if (status == 200 && msg != NULL) {
+    if (status == HTTP_STATUS_OK && msg != NULL) {
         *body   = xs_json_dumps(msg, 4);
         *body   = xs_json_dumps(msg, 4);
         *b_size = strlen(*body);
         *b_size = strlen(*body);
     }
     }
@@ -2677,7 +2683,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 {
 {
     (void)b_size;
     (void)b_size;
 
 
-    int status = 202; /* accepted */
+    int status = HTTP_STATUS_ACCEPTED;
     const char *i_ctype = xs_dict_get(req, "content-type");
     const char *i_ctype = xs_dict_get(req, "content-type");
     snac snac;
     snac snac;
     const char *v;
     const char *v;
@@ -2685,13 +2691,13 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
     if (i_ctype == NULL) {
     if (i_ctype == NULL) {
         *body  = xs_str_new("no content-type");
         *body  = xs_str_new("no content-type");
         *ctype = "text/plain";
         *ctype = "text/plain";
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
     }
     }
 
 
     if (xs_is_null(payload)) {
     if (xs_is_null(payload)) {
         *body  = xs_str_new("no payload");
         *body  = xs_str_new("no payload");
         *ctype = "text/plain";
         *ctype = "text/plain";
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
     }
     }
 
 
     if (xs_str_in(i_ctype, "application/activity+json") == -1 &&
     if (xs_str_in(i_ctype, "application/activity+json") == -1 &&
@@ -2709,7 +2715,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 
 
         *body  = xs_str_new("JSON error");
         *body  = xs_str_new("JSON error");
         *ctype = "text/plain";
         *ctype = "text/plain";
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
     }
     }
 
 
     if (id && is_instance_blocked(id)) {
     if (id && is_instance_blocked(id)) {
@@ -2717,7 +2723,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 
 
         *body  = xs_str_new("blocked");
         *body  = xs_str_new("blocked");
         *ctype = "text/plain";
         *ctype = "text/plain";
-        return 403;
+        return HTTP_STATUS_FORBIDDEN;
     }
     }
 
 
     /* get the user and path */
     /* get the user and path */
@@ -2725,20 +2731,20 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 
 
     if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) {
     if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) {
         enqueue_shared_input(msg, req, 0);
         enqueue_shared_input(msg, req, 0);
-        return 202;
+        return HTTP_STATUS_ACCEPTED;
     }
     }
 
 
     if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) {
     if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) {
         /* strange q_path */
         /* strange q_path */
         srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path));
         srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     const char *uid = xs_list_get(l, 1);
     const char *uid = xs_list_get(l, 1);
     if (!user_open(&snac, uid)) {
     if (!user_open(&snac, uid)) {
         /* invalid user */
         /* invalid user */
         srv_debug(1, xs_fmt("activitypub_post_handler bad user %s", uid));
         srv_debug(1, xs_fmt("activitypub_post_handler bad user %s", uid));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     /* if it has a digest, check it now, because
     /* if it has a digest, check it now, because
@@ -2752,7 +2758,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_str_new("bad digest");
             *body  = xs_str_new("bad digest");
             *ctype = "text/plain";
             *ctype = "text/plain";
-            status = 400;
+            status = HTTP_STATUS_BAD_REQUEST;
         }
         }
     }
     }
 
 
@@ -2763,7 +2769,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_str_new("rejected");
             *body  = xs_str_new("rejected");
             *ctype = "text/plain";
             *ctype = "text/plain";
-            status = 403;
+            status = HTTP_STATUS_FORBIDDEN;
         }
         }
     }
     }
 
 

+ 61 - 34
data.c

@@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
     return 0;
     return 0;
 }
 }
 
 
+int user_persist(snac *snac)
+/* store user */
+{
+    xs *fn  = xs_fmt("%s/user.json", snac->basedir);
+    xs *bfn = xs_fmt("%s.bak", fn);
+    FILE *f;
+
+    rename(fn, bfn);
+
+    if ((f = fopen(fn, "w")) != NULL) {
+        xs_json_dump(snac->config, 4, f);
+        fclose(f);
+    }
+    else
+        rename(bfn, fn);
+
+    history_del(snac, "timeline.html_");
+
+    xs *a_msg = msg_actor(snac);
+    xs *u_msg = msg_update(snac, a_msg);
+
+    enqueue_message(snac, u_msg);
+    enqueue_verify_links(snac);
+
+    return 0;
+}
+
 
 
 double mtime_nl(const char *fn, int *n_link)
 double mtime_nl(const char *fn, int *n_link)
 /* returns the mtime and number of links of a file or directory, or 0.0 */
 /* returns the mtime and number of links of a file or directory, or 0.0 */
@@ -355,12 +382,12 @@ int is_md5_hex(const char *md5)
 int index_add_md5(const char *fn, const char *md5)
 int index_add_md5(const char *fn, const char *md5)
 /* adds an md5 to an index */
 /* adds an md5 to an index */
 {
 {
-    int status = 201; /* Created */
+    int status = HTTP_STATUS_CREATED;
     FILE *f;
     FILE *f;
 
 
     if (!is_md5_hex(md5)) {
     if (!is_md5_hex(md5)) {
         srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
         srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
     }
     }
 
 
     pthread_mutex_lock(&data_mutex);
     pthread_mutex_lock(&data_mutex);
@@ -375,7 +402,7 @@ int index_add_md5(const char *fn, const char *md5)
         fclose(f);
         fclose(f);
     }
     }
     else
     else
-        status = 500;
+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
     pthread_mutex_unlock(&data_mutex);
     pthread_mutex_unlock(&data_mutex);
 
 
@@ -394,7 +421,7 @@ int index_add(const char *fn, const char *id)
 int index_del_md5(const char *fn, const char *md5)
 int index_del_md5(const char *fn, const char *md5)
 /* deletes an md5 from an index */
 /* deletes an md5 from an index */
 {
 {
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
     FILE *f;
     FILE *f;
 
 
     pthread_mutex_lock(&data_mutex);
     pthread_mutex_lock(&data_mutex);
@@ -411,7 +438,7 @@ int index_del_md5(const char *fn, const char *md5)
                    [yes: this breaks index_len()] */
                    [yes: this breaks index_len()] */
                 fseek(f, -33, SEEK_CUR);
                 fseek(f, -33, SEEK_CUR);
                 fwrite("-", 1, 1, f);
                 fwrite("-", 1, 1, f);
-                status = 200;
+                status = HTTP_STATUS_OK;
 
 
                 break;
                 break;
             }
             }
@@ -420,7 +447,7 @@ int index_del_md5(const char *fn, const char *md5)
         fclose(f);
         fclose(f);
     }
     }
     else
     else
-        status = 410;
+        status = HTTP_STATUS_GONE;
 
 
     pthread_mutex_unlock(&data_mutex);
     pthread_mutex_unlock(&data_mutex);
 
 
@@ -660,7 +687,7 @@ int object_here(const char *id)
 int object_get_by_md5(const char *md5, xs_dict **obj)
 int object_get_by_md5(const char *md5, xs_dict **obj)
 /* returns a stored object, optionally of the requested type */
 /* returns a stored object, optionally of the requested type */
 {
 {
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
     xs *fn     = _object_fn_by_md5(md5, "object_get_by_md5");
     xs *fn     = _object_fn_by_md5(md5, "object_get_by_md5");
     FILE *f;
     FILE *f;
 
 
@@ -669,7 +696,7 @@ int object_get_by_md5(const char *md5, xs_dict **obj)
         fclose(f);
         fclose(f);
 
 
         if (*obj)
         if (*obj)
-            status = 200;
+            status = HTTP_STATUS_OK;
     }
     }
     else
     else
         *obj = NULL;
         *obj = NULL;
@@ -689,7 +716,7 @@ int object_get(const char *id, xs_dict **obj)
 int _object_add(const char *id, const xs_dict *obj, int ow)
 int _object_add(const char *id, const xs_dict *obj, int ow)
 /* stores an object */
 /* stores an object */
 {
 {
-    int status = 201; /* Created */
+    int status = HTTP_STATUS_CREATED; /* Created */
     xs *fn     = _object_fn(id);
     xs *fn     = _object_fn(id);
     FILE *f;
     FILE *f;
 
 
@@ -697,10 +724,10 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
         if (!ow) {
         if (!ow) {
             /* object already here */
             /* object already here */
             srv_debug(1, xs_fmt("object_add object already here %s", id));
             srv_debug(1, xs_fmt("object_add object already here %s", id));
-            return 204; /* No content */
+            return HTTP_STATUS_NO_CONTENT;
         }
         }
         else
         else
-            status = 200;
+            status = HTTP_STATUS_OK;
     }
     }
 
 
     if ((f = fopen(fn, "w")) != NULL) {
     if ((f = fopen(fn, "w")) != NULL) {
@@ -736,7 +763,7 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
     }
     }
     else {
     else {
         srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
         srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
-        status = 500;
+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
     }
     }
 
 
     srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
     srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
@@ -762,11 +789,11 @@ int object_add_ow(const char *id, const xs_dict *obj)
 int object_del_by_md5(const char *md5)
 int object_del_by_md5(const char *md5)
 /* deletes an object by its md5 */
 /* deletes an object by its md5 */
 {
 {
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
     xs *fn     = _object_fn_by_md5(md5, "object_del_by_md5");
     xs *fn     = _object_fn_by_md5(md5, "object_del_by_md5");
 
 
     if (unlink(fn) != -1) {
     if (unlink(fn) != -1) {
-        status = 200;
+        status = HTTP_STATUS_OK;
 
 
         /* also delete associated indexes */
         /* also delete associated indexes */
         xs *spec  = xs_dup(fn);
         xs *spec  = xs_dup(fn);
@@ -907,7 +934,7 @@ int object_parent(const char *md5, char *buf, int size)
 int object_admire(const char *id, const char *actor, int like)
 int object_admire(const char *id, const char *actor, int like)
 /* actor likes or announces this object */
 /* actor likes or announces this object */
 {
 {
-    int status = 200;
+    int status = HTTP_STATUS_OK;
     xs *fn     = _object_fn(id);
     xs *fn     = _object_fn(id);
 
 
     fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
     fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
@@ -1007,7 +1034,7 @@ int follower_add(snac *snac, const char *actor)
 
 
     snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
     snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
 
 
-    return ret == -1 ? 500 : 200;
+    return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
 }
 }
 
 
 
 
@@ -1018,7 +1045,7 @@ int follower_del(snac *snac, const char *actor)
 
 
     snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
     snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
 
 
-    return ret == -1 ? 404 : 200;
+    return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
 }
 }
 
 
 
 
@@ -1109,7 +1136,7 @@ int timeline_here(snac *snac, const char *md5)
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
 /* gets a message from the timeline */
 /* gets a message from the timeline */
 {
 {
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
     FILE *f    = NULL;
     FILE *f    = NULL;
 
 
     xs *fn = timeline_fn_by_md5(snac, md5);
     xs *fn = timeline_fn_by_md5(snac, md5);
@@ -1119,7 +1146,7 @@ int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
         fclose(f);
         fclose(f);
 
 
         if (*msg != NULL)
         if (*msg != NULL)
-            status = 200;
+            status = HTTP_STATUS_OK;
     }
     }
 
 
     return status;
     return status;
@@ -1282,7 +1309,7 @@ xs_str *_following_fn(snac *snac, const char *actor)
 int following_add(snac *snac, const char *actor, const xs_dict *msg)
 int following_add(snac *snac, const char *actor, const xs_dict *msg)
 /* adds to the following list */
 /* adds to the following list */
 {
 {
-    int ret = 201; /* created */
+    int ret = HTTP_STATUS_CREATED;
     xs *fn = _following_fn(snac, actor);
     xs *fn = _following_fn(snac, actor);
     FILE *f;
     FILE *f;
     xs *p_object = NULL;
     xs *p_object = NULL;
@@ -1295,7 +1322,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
 
 
         if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
         if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
             snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
             snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
-            return 200;
+            return HTTP_STATUS_OK;
         }
         }
     }
     }
 
 
@@ -1311,7 +1338,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
         link(actor_fn, fn);
         link(actor_fn, fn);
     }
     }
     else
     else
-        ret = 500;
+        ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
     snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
     snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
 
 
@@ -1332,7 +1359,7 @@ int following_del(snac *snac, const char *actor)
     fn = xs_replace_i(fn, ".json", "_a.json");
     fn = xs_replace_i(fn, ".json", "_a.json");
     unlink(fn);
     unlink(fn);
 
 
-    return 200;
+    return HTTP_STATUS_OK;
 }
 }
 
 
 
 
@@ -1350,14 +1377,14 @@ int following_get(snac *snac, const char *actor, xs_dict **data)
 {
 {
     xs *fn = _following_fn(snac, actor);
     xs *fn = _following_fn(snac, actor);
     FILE *f;
     FILE *f;
-    int status = 200;
+    int status = HTTP_STATUS_OK;
 
 
     if ((f = fopen(fn, "r")) != NULL) {
     if ((f = fopen(fn, "r")) != NULL) {
         *data = xs_json_load(f);
         *data = xs_json_load(f);
         fclose(f);
         fclose(f);
     }
     }
     else
     else
-        status = 404;
+        status = HTTP_STATUS_NOT_FOUND;
 
 
     return status;
     return status;
 }
 }
@@ -1576,7 +1603,7 @@ int actor_add(const char *actor, const xs_dict *msg)
 int actor_get(const char *actor, xs_dict **data)
 int actor_get(const char *actor, xs_dict **data)
 /* returns an already downloaded actor */
 /* returns an already downloaded actor */
 {
 {
-    int status = 200;
+    int status = HTTP_STATUS_OK;
     xs_dict *d = NULL;
     xs_dict *d = NULL;
 
 
     if (xs_startswith(actor, srv_baseurl)) {
     if (xs_startswith(actor, srv_baseurl)) {
@@ -1590,10 +1617,10 @@ int actor_get(const char *actor, xs_dict **data)
                 *data = msg_actor(&user);
                 *data = msg_actor(&user);
 
 
             user_free(&user);
             user_free(&user);
-            return 200;
+            return HTTP_STATUS_OK;
         }
         }
         else
         else
-            return 404;
+            return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     /* read the object */
     /* read the object */
@@ -1606,7 +1633,7 @@ int actor_get(const char *actor, xs_dict **data)
     if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
     if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
         srv_debug(1, xs_fmt("corrupted actor object %s", actor));
         srv_debug(1, xs_fmt("corrupted actor object %s", actor));
         d = xs_free(d);
         d = xs_free(d);
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     if (data)
     if (data)
@@ -1622,7 +1649,7 @@ int actor_get(const char *actor, xs_dict **data)
 
 
     if (mtime(fn) + max_time < (double) time(NULL)) {
     if (mtime(fn) + max_time < (double) time(NULL)) {
         /* actor data exists but also stinks */
         /* actor data exists but also stinks */
-        status = 205; /* "205: Reset Content" "110: Response Is Stale" */
+        status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
     }
     }
 
 
     return status;
     return status;
@@ -1634,7 +1661,7 @@ int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
 {
 {
     int status = actor_get(actor, data);
     int status = actor_get(actor, data);
 
 
-    if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
+    if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
         enqueue_actor_refresh(user, actor, 0);
         enqueue_actor_refresh(user, actor, 0);
 
 
     return status;
     return status;
@@ -1953,7 +1980,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
                         const char *inm, xs_str **etag)
                         const char *inm, xs_str **etag)
 /* loads a cached file */
 /* loads a cached file */
 {
 {
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
 
 
     if (fn) {
     if (fn) {
         double tm = mtime(fn);
         double tm = mtime(fn);
@@ -1965,7 +1992,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
             /* if if-none-match is set, check if it's the same */
             /* if if-none-match is set, check if it's the same */
             if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
             if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
                 /* client has the newest version */
                 /* client has the newest version */
-                status = 304;
+                status = HTTP_STATUS_NOT_MODIFIED;
             }
             }
             else {
             else {
                 /* newer or never downloaded; read the full file */
                 /* newer or never downloaded; read the full file */
@@ -1976,7 +2003,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
                     *data = xs_read(f, size);
                     *data = xs_read(f, size);
                     fclose(f);
                     fclose(f);
 
 
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
 
 

+ 39 - 59
html.c

@@ -2540,7 +2540,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
                      char **body, int *b_size, char **ctype, xs_str **etag)
                      char **body, int *b_size, char **ctype, xs_str **etag)
 {
 {
     const char *accept = xs_dict_get(req, "accept");
     const char *accept = xs_dict_get(req, "accept");
-    int status = 404;
+    int status = HTTP_STATUS_NOT_FOUND;
     snac snac;
     snac snac;
     xs *uid = NULL;
     xs *uid = NULL;
     const char *p_path;
     const char *p_path;
@@ -2553,7 +2553,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
     if (xs_is_null(v)) {
     if (xs_is_null(v)) {
         srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
         srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     uid = xs_dup(v);
     uid = xs_dup(v);
@@ -2569,7 +2569,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     if (!uid || !user_open(&snac, uid)) {
     if (!uid || !user_open(&snac, uid)) {
         /* invalid user */
         /* invalid user */
         srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
         srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     /* return the RSS if requested by Accept header */
     /* return the RSS if requested by Accept header */
@@ -2598,7 +2598,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
             /** empty public timeline for private users **/
             /** empty public timeline for private users **/
             *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1);
             *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1);
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
         }
         }
         else
         else
         if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
         if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
@@ -2617,7 +2617,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
             *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1);
             *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1);
 
 
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
 
 
             if (save)
             if (save)
                 history_add(&snac, h, *body, *b_size, etag);
                 history_add(&snac, h, *body, *b_size, etag);
@@ -2627,7 +2627,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     if (strcmp(p_path, "admin") == 0) { /** private timeline **/
     if (strcmp(p_path, "admin") == 0) { /** private timeline **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             const char *q = xs_dict_get(q_vars, "q");
             const char *q = xs_dict_get(q_vars, "q");
@@ -2649,7 +2649,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
                     *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0);
                     *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0);
                     *b_size = strlen(*body);
                     *b_size = strlen(*body);
-                    status  = 200;
+                    status  = HTTP_STATUS_OK;
                 }
                 }
                 else {
                 else {
                     /** search by content **/
                     /** search by content **/
@@ -2670,7 +2670,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
                     *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0);
                     *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0);
                     *b_size = strlen(*body);
                     *b_size = strlen(*body);
-                    status  = 200;
+                    status  = HTTP_STATUS_OK;
                 }
                 }
             }
             }
             else {
             else {
@@ -2699,7 +2699,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
                             xs_list_len(next), NULL, "/admin", 1);
                             xs_list_len(next), NULL, "/admin", 1);
 
 
                     *b_size = strlen(*body);
                     *b_size = strlen(*body);
-                    status  = 200;
+                    status  = HTTP_STATUS_OK;
 
 
                     if (save)
                     if (save)
                         history_add(&snac, "timeline.html_", *body, *b_size, etag);
                         history_add(&snac, "timeline.html_", *body, *b_size, etag);
@@ -2711,7 +2711,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
     if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             xs *l = xs_split(p_path, "/");
             xs *l = xs_split(p_path, "/");
@@ -2722,7 +2722,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
                 *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1);
                 *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1);
                 *b_size = strlen(*body);
                 *b_size = strlen(*body);
-                status  = 200;
+                status  = HTTP_STATUS_OK;
             }
             }
         }
         }
     }
     }
@@ -2730,31 +2730,31 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     if (strcmp(p_path, "people") == 0) { /** the list of people **/
     if (strcmp(p_path, "people") == 0) { /** the list of people **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             *body   = html_people(&snac);
             *body   = html_people(&snac);
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
         }
         }
     }
     }
     else
     else
     if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
     if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             *body   = html_notifications(&snac, skip, show);
             *body   = html_notifications(&snac, skip, show);
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
         }
         }
     }
     }
     else
     else
     if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
     if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             xs *list = timeline_instance_list(skip, show);
             xs *list = timeline_instance_list(skip, show);
@@ -2763,14 +2763,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
             *body = html_timeline(&snac, list, 0, skip, show,
             *body = html_timeline(&snac, list, 0, skip, show,
                 xs_list_len(next), L("Showing instance timeline"), "/instance", 0);
                 xs_list_len(next), L("Showing instance timeline"), "/instance", 0);
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
         }
         }
     }
     }
     else
     else
     if (xs_startswith(p_path, "list/")) { /** list timelines **/
     if (xs_startswith(p_path, "list/")) { /** list timelines **/
         if (!login(&snac, req)) {
         if (!login(&snac, req)) {
             *body  = xs_dup(uid);
             *body  = xs_dup(uid);
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
         else {
         else {
             xs *l = xs_split(p_path, "/");
             xs *l = xs_split(p_path, "/");
@@ -2787,14 +2787,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
                 *body = html_timeline(&snac, list, 0, skip, show,
                 *body = html_timeline(&snac, list, 0, skip, show,
                     xs_list_len(next), title, base, 1);
                     xs_list_len(next), title, base, 1);
                 *b_size = strlen(*body);
                 *b_size = strlen(*body);
-                status  = 200;
+                status  = HTTP_STATUS_OK;
             }
             }
         }
         }
     }
     }
     else
     else
     if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
     if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
-            return 403;
+            return HTTP_STATUS_FORBIDDEN;
 
 
         xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
         xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
         xs *msg = NULL;
         xs *msg = NULL;
@@ -2807,7 +2807,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1);
             *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1);
             *b_size = strlen(*body);
             *b_size = strlen(*body);
-            status  = 200;
+            status  = HTTP_STATUS_OK;
         }
         }
     }
     }
     else
     else
@@ -2829,10 +2829,10 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     else
     else
     if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
     if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
-            return 403;
+            return HTTP_STATUS_FORBIDDEN;
 
 
         if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
         if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
-            return 403;
+            return HTTP_STATUS_FORBIDDEN;
 
 
         xs *l = xs_split(p_path, "/");
         xs *l = xs_split(p_path, "/");
         const char *id = xs_list_get(l, 1);
         const char *id = xs_list_get(l, 1);
@@ -2841,7 +2841,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
             if (xs_endswith(id, "timeline.html_")) {
             if (xs_endswith(id, "timeline.html_")) {
                 /* Don't let them in */
                 /* Don't let them in */
                 *b_size = 0;
                 *b_size = 0;
-                status = 404;
+                status = HTTP_STATUS_NOT_FOUND;
             }
             }
             else
             else
                 status = history_get(&snac, id, body, b_size,
                 status = history_get(&snac, id, body, b_size,
@@ -2851,7 +2851,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
     else
     else
     if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
     if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
         if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
-            return 403;
+            return HTTP_STATUS_FORBIDDEN;
 
 
         xs *elems = timeline_simple_list(&snac, "public", 0, 20);
         xs *elems = timeline_simple_list(&snac, "public", 0, 20);
         xs *bio   = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
         xs *bio   = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
@@ -2865,12 +2865,12 @@ int html_get_handler(const xs_dict *req, const char *q_path,
         *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
         *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
         *b_size = strlen(*body);
         *b_size = strlen(*body);
         *ctype  = "application/rss+xml; charset=utf-8";
         *ctype  = "application/rss+xml; charset=utf-8";
-        status  = 200;
+        status  = HTTP_STATUS_OK;
 
 
         snac_debug(&snac, 1, xs_fmt("serving RSS"));
         snac_debug(&snac, 1, xs_fmt("serving RSS"));
     }
     }
     else
     else
-        status = 404;
+        status = HTTP_STATUS_NOT_FOUND;
 
 
     user_free(&snac);
     user_free(&snac);
 
 
@@ -2901,7 +2901,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
     if (!uid || !user_open(&snac, uid)) {
     if (!uid || !user_open(&snac, uid)) {
         /* invalid user */
         /* invalid user */
         srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
         srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
-        return 404;
+        return HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
     p_path = xs_list_get(l, 2);
     p_path = xs_list_get(l, 2);
@@ -2910,7 +2910,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
     if (!login(&snac, req)) {
     if (!login(&snac, req)) {
         user_free(&snac);
         user_free(&snac);
         *body  = xs_dup(uid);
         *body  = xs_dup(uid);
-        return 401;
+        return HTTP_STATUS_UNAUTHORIZED;
     }
     }
 
 
     p_vars = xs_dict_get(req, "p_vars");
     p_vars = xs_dict_get(req, "p_vars");
@@ -3049,7 +3049,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
             history_del(&snac, "timeline.html_");
             history_del(&snac, "timeline.html_");
         }
         }
 
 
-        status = 303;
+        status = HTTP_STATUS_SEE_OTHER;
     }
     }
     else
     else
     if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
     if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
@@ -3060,11 +3060,11 @@ int html_post_handler(const xs_dict *req, const char *q_path,
         const char *group  = xs_dict_get(p_vars, "group");
         const char *group  = xs_dict_get(p_vars, "group");
 
 
         if (action == NULL)
         if (action == NULL)
-            return 404;
+            return HTTP_STATUS_NOT_FOUND;
 
 
         snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
         snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
 
 
-        status = 303;
+        status = HTTP_STATUS_SEE_OTHER;
 
 
         if (strcmp(action, L("Like")) == 0) { /** **/
         if (strcmp(action, L("Like")) == 0) { /** **/
             xs *msg = msg_admiration(&snac, id, "Like");
             xs *msg = msg_admiration(&snac, id, "Like");
@@ -3216,10 +3216,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
             timeline_touch(&snac);
             timeline_touch(&snac);
         }
         }
         else
         else
-            status = 404;
+            status = HTTP_STATUS_NOT_FOUND;
 
 
         /* delete the cached timeline */
         /* delete the cached timeline */
-        if (status == 303)
+        if (status == HTTP_STATUS_SEE_OTHER)
             history_del(&snac, "timeline.html_");
             history_del(&snac, "timeline.html_");
     }
     }
     else
     else
@@ -3334,36 +3334,16 @@ int html_post_handler(const xs_dict *req, const char *q_path,
             snac.config = xs_dict_set(snac.config, "passwd", pw);
             snac.config = xs_dict_set(snac.config, "passwd", pw);
         }
         }
 
 
-        xs *fn  = xs_fmt("%s/user.json", snac.basedir);
-        xs *bfn = xs_fmt("%s.bak", fn);
-        FILE *f;
-
-        rename(fn, bfn);
-
-        if ((f = fopen(fn, "w")) != NULL) {
-            xs_json_dump(snac.config, 4, f);
-            fclose(f);
-        }
-        else
-            rename(bfn, fn);
-
-        history_del(&snac, "timeline.html_");
-
-        xs *a_msg = msg_actor(&snac);
-        xs *u_msg = msg_update(&snac, a_msg);
-
-        enqueue_message(&snac, u_msg);
-
-        enqueue_verify_links(&snac);
+        user_persist(&snac);
 
 
-        status = 303;
+        status = HTTP_STATUS_SEE_OTHER;
     }
     }
     else
     else
     if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
     if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
         notify_clear(&snac);
         notify_clear(&snac);
         timeline_touch(&snac);
         timeline_touch(&snac);
 
 
-        status = 303;
+        status = HTTP_STATUS_SEE_OTHER;
     }
     }
     else
     else
     if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
     if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
@@ -3416,10 +3396,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
             }
             }
         }
         }
 
 
-        status = 303;
+        status = HTTP_STATUS_SEE_OTHER;
     }
     }
 
 
-    if (status == 303) {
+    if (status == HTTP_STATUS_SEE_OTHER) {
         const char *redir = xs_dict_get(p_vars, "redir");
         const char *redir = xs_dict_get(p_vars, "redir");
 
 
         if (xs_is_null(redir))
         if (xs_is_null(redir))

+ 45 - 0
http_codes.h

@@ -0,0 +1,45 @@
+HTTP_STATUS(100, CONTINUE, Continue)
+HTTP_STATUS(101, SWITCHING_PROTOCOLS, Switching Protocols)
+HTTP_STATUS(102, PROCESSING, Processing)
+HTTP_STATUS(103, EARLY_HINTS, Early Hints)
+HTTP_STATUS(200, OK, OK)
+HTTP_STATUS(201, CREATED, Created)
+HTTP_STATUS(202, ACCEPTED, Accepted)
+HTTP_STATUS(203, NON_AUTHORITATIVE_INFORMATION, Non Authoritative Information)
+HTTP_STATUS(204, NO_CONTENT, No Content)
+HTTP_STATUS(205, RESET_CONTENT, Reset Content)
+HTTP_STATUS(206, PARTIAL_CONTENT, Partial Content)
+HTTP_STATUS(207, MULTI_STATUS, Multi Status)
+HTTP_STATUS(208, ALREADY_REPORTED, Already Reported)
+HTTP_STATUS(218, THIS_IS_FINE, This Is Fine)
+HTTP_STATUS(226, IM_USED, IM Used)
+HTTP_STATUS(300, MULTIPLE_CHOICES, Multiple Choices)
+HTTP_STATUS(301, MOVED_PERMANENTLY, Moved Permanently)
+HTTP_STATUS(302, FOUND, Found)
+HTTP_STATUS(303, SEE_OTHER, See Other)
+HTTP_STATUS(304, NOT_MODIFIED, Not Modified)
+HTTP_STATUS(305, USE_PROXY, Use Proxy)
+HTTP_STATUS(306, SWITCH_PROXY, Switch Proxy)
+HTTP_STATUS(307, TEMPORARY_REDIRECT, Temporary Redirect)
+HTTP_STATUS(308, PERMANENT_REDIRECT, Permanent Redirect)
+HTTP_STATUS(400, BAD_REQUEST, Bad Request)
+HTTP_STATUS(401, UNAUTHORIZED, Unauthorized)
+HTTP_STATUS(402, PAYMENT_REQUIRED, Payment Required)
+HTTP_STATUS(403, FORBIDDEN, Forbidden)
+HTTP_STATUS(404, NOT_FOUND, Not Found)
+HTTP_STATUS(405, METHOD_NOT_ALLOWED, Method Not Allowed)
+HTTP_STATUS(406, NOT_ACCEPTABLE, Not Acceptable)
+HTTP_STATUS(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required)
+HTTP_STATUS(408, REQUEST_TIMEOUT, Request Timeout)
+HTTP_STATUS(409, CONFLICT, Conflict)
+HTTP_STATUS(410, GONE, Gone)
+HTTP_STATUS(421, MISDIRECTED_REQUEST, Misdirected Request)
+HTTP_STATUS(422, UNPROCESSABLE_CONTENT, Unprocessable Content)
+HTTP_STATUS(499, CLIENT_CLOSED_REQUEST, Client Closed Request)
+HTTP_STATUS(500, INTERNAL_SERVER_ERROR, Internal Server Error)
+HTTP_STATUS(501, NOT_IMPLEMENTED, Not Implemented)
+HTTP_STATUS(502, BAD_GATEWAY, Bad Gateway)
+HTTP_STATUS(503, SERVICE_UNAVAILABLE, Service Unavailable)
+HTTP_STATUS(504, GATEWAY_TIMEOUT, Gateway Timeout)
+HTTP_STATUS(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported)
+HTTP_STATUS(507, INSUFFICIENT_STORAGE, Insufficient Storage)

+ 24 - 14
httpd.c

@@ -217,17 +217,17 @@ int server_get_handler(xs_dict *req, const char *q_path,
             *body = greeting_html();
             *body = greeting_html();
 
 
         if (*body)
         if (*body)
-            status = 200;
+            status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(q_path, "/susie.png") == 0 || strcmp(q_path, "/favicon.ico") == 0 ) {
     if (strcmp(q_path, "/susie.png") == 0 || strcmp(q_path, "/favicon.ico") == 0 ) {
-        status = 200;
+        status = HTTP_STATUS_OK;
         *body  = xs_base64_dec(default_avatar_base64(), b_size);
         *body  = xs_base64_dec(default_avatar_base64(), b_size);
         *ctype = "image/png";
         *ctype = "image/png";
     }
     }
     else
     else
     if (strcmp(q_path, "/.well-known/nodeinfo") == 0) {
     if (strcmp(q_path, "/.well-known/nodeinfo") == 0) {
-        status = 200;
+        status = HTTP_STATUS_OK;
         *ctype = "application/json; charset=utf-8";
         *ctype = "application/json; charset=utf-8";
         *body  = xs_fmt("{\"links\":["
         *body  = xs_fmt("{\"links\":["
             "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\","
             "{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\","
@@ -236,7 +236,7 @@ int server_get_handler(xs_dict *req, const char *q_path,
     }
     }
     else
     else
     if (strcmp(q_path, "/.well-known/host-meta") == 0) {
     if (strcmp(q_path, "/.well-known/host-meta") == 0) {
-        status = 200;
+        status = HTTP_STATUS_OK;
         *ctype = "application/xrd+xml";
         *ctype = "application/xrd+xml";
         *body  = xs_fmt("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
         *body  = xs_fmt("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                 "<XRD>"
                 "<XRD>"
@@ -245,13 +245,13 @@ int server_get_handler(xs_dict *req, const char *q_path,
     }
     }
     else
     else
     if (strcmp(q_path, "/nodeinfo_2_0") == 0) {
     if (strcmp(q_path, "/nodeinfo_2_0") == 0) {
-        status = 200;
+        status = HTTP_STATUS_OK;
         *ctype = "application/json; charset=utf-8";
         *ctype = "application/json; charset=utf-8";
         *body  = nodeinfo_2_0();
         *body  = nodeinfo_2_0();
     }
     }
     else
     else
     if (strcmp(q_path, "/robots.txt") == 0) {
     if (strcmp(q_path, "/robots.txt") == 0) {
-        status = 200;
+        status = HTTP_STATUS_OK;
         *ctype = "text/plain";
         *ctype = "text/plain";
         *body  = xs_str_new("User-agent: *\n"
         *body  = xs_str_new("User-agent: *\n"
                             "Disallow: /\n");
                             "Disallow: /\n");
@@ -360,10 +360,20 @@ void httpd_connection(FILE *f)
                         payload, p_size, &body, &b_size, &ctype);
                         payload, p_size, &body, &b_size, &ctype);
 #endif
 #endif
 
 
+    }
+    else
+    if (strcmp(method, "PATCH") == 0) {
+
+#ifndef NO_MASTODON_API
+        if (status == 0)
+            status = mastoapi_patch_handler(req, q_path,
+                        payload, p_size, &body, &b_size, &ctype);
+#endif
+
     }
     }
     else
     else
     if (strcmp(method, "OPTIONS") == 0) {
     if (strcmp(method, "OPTIONS") == 0) {
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(method, "DELETE") == 0) {
     if (strcmp(method, "DELETE") == 0) {
@@ -378,22 +388,22 @@ void httpd_connection(FILE *f)
     if (status == 0) {
     if (status == 0) {
         srv_archive_error("unattended_method", "unattended method", req, payload);
         srv_archive_error("unattended_method", "unattended method", req, payload);
         srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path));
         srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path));
-        status = 404;
+        status = HTTP_STATUS_NOT_FOUND;
     }
     }
 
 
-    if (status == 403)
+    if (status == HTTP_STATUS_FORBIDDEN)
         body = xs_str_new("<h1>403 Forbidden</h1>");
         body = xs_str_new("<h1>403 Forbidden</h1>");
 
 
-    if (status == 404)
+    if (status == HTTP_STATUS_NOT_FOUND)
         body = xs_str_new("<h1>404 Not Found</h1>");
         body = xs_str_new("<h1>404 Not Found</h1>");
 
 
-    if (status == 400 && body != NULL)
+    if (status == HTTP_STATUS_BAD_REQUEST && body != NULL)
         body = xs_str_new("<h1>400 Bad Request</h1>");
         body = xs_str_new("<h1>400 Bad Request</h1>");
 
 
-    if (status == 303)
+    if (status == HTTP_STATUS_SEE_OTHER)
         headers = xs_dict_append(headers, "location", body);
         headers = xs_dict_append(headers, "location", body);
 
 
-    if (status == 401) {
+    if (status == HTTP_STATUS_UNAUTHORIZED) {
         xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"",
         xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"",
                                 body, xs_dict_get(srv_config, "host"));
                                 body, xs_dict_get(srv_config, "host"));
 
 
@@ -432,7 +442,7 @@ void httpd_connection(FILE *f)
     if (p_state->use_fcgi)
     if (p_state->use_fcgi)
         xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
         xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
     else
     else
-        xs_httpd_response(f, status, headers, body, b_size);
+        xs_httpd_response(f, status, http_status_text(status), headers, body, b_size);
 
 
     fclose(f);
     fclose(f);
 
 

+ 354 - 172
mastoapi.c

@@ -32,9 +32,9 @@ int app_add(const char *id, const xs_dict *app)
 /* stores an app */
 /* stores an app */
 {
 {
     if (!xs_is_hex(id))
     if (!xs_is_hex(id))
-        return 500;
+        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
-    int status = 201;
+    int status = HTTP_STATUS_CREATED;
     xs *fn     = xs_fmt("%s/app/", srv_basedir);
     xs *fn     = xs_fmt("%s/app/", srv_basedir);
     FILE *f;
     FILE *f;
 
 
@@ -47,7 +47,7 @@ int app_add(const char *id, const xs_dict *app)
         fclose(f);
         fclose(f);
     }
     }
     else
     else
-        status = 500;
+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
     return status;
     return status;
 }
 }
@@ -95,9 +95,9 @@ int token_add(const char *id, const xs_dict *token)
 /* stores a token */
 /* stores a token */
 {
 {
     if (!xs_is_hex(id))
     if (!xs_is_hex(id))
-        return 500;
+        return HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
-    int status = 201;
+    int status = HTTP_STATUS_CREATED;
     xs *fn     = xs_fmt("%s/token/", srv_basedir);
     xs *fn     = xs_fmt("%s/token/", srv_basedir);
     FILE *f;
     FILE *f;
 
 
@@ -110,7 +110,7 @@ int token_add(const char *id, const xs_dict *token)
         fclose(f);
         fclose(f);
     }
     }
     else
     else
-        status = 500;
+        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 
 
     return status;
     return status;
 }
 }
@@ -174,7 +174,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
     if (!xs_startswith(q_path, "/oauth/"))
     if (!xs_startswith(q_path, "/oauth/"))
         return 0;
         return 0;
 
 
-    int status   = 404;
+    int status   = HTTP_STATUS_NOT_FOUND;
     const xs_dict *msg = xs_dict_get(req, "q_vars");
     const xs_dict *msg = xs_dict_get(req, "q_vars");
     xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
     xs *cmd      = xs_replace_n(q_path, "/oauth", "", 1);
 
 
@@ -186,7 +186,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
         const char *rtype = xs_dict_get(msg, "response_type");
         const char *rtype = xs_dict_get(msg, "response_type");
         const char *state = xs_dict_get(msg, "state");
         const char *state = xs_dict_get(msg, "state");
 
 
-        status = 400;
+        status = HTTP_STATUS_BAD_REQUEST;
 
 
         if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
         if (cid && ruri && rtype && strcmp(rtype, "code") == 0) {
             xs *app = app_get(cid);
             xs *app = app_get(cid);
@@ -201,7 +201,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
                 *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
                 *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-login",
                                 ruri, cid, state, USER_AGENT);
                                 ruri, cid, state, USER_AGENT);
                 *ctype = "text/html";
                 *ctype = "text/html";
-                status = 200;
+                status = HTTP_STATUS_OK;
 
 
                 srv_debug(1, xs_fmt("oauth authorize: generating login page"));
                 srv_debug(1, xs_fmt("oauth authorize: generating login page"));
             }
             }
@@ -219,7 +219,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
         *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
         *body  = xs_fmt(login_page, host, host, "", proto, host, "oauth/x-snac-get-token",
                         "", "", "", USER_AGENT);
                         "", "", "", USER_AGENT);
         *ctype = "text/html";
         *ctype = "text/html";
-        status = 200;
+        status = HTTP_STATUS_OK;
 
 
     }
     }
 
 
@@ -237,7 +237,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
     if (!xs_startswith(q_path, "/oauth/"))
     if (!xs_startswith(q_path, "/oauth/"))
         return 0;
         return 0;
 
 
-    int status   = 404;
+    int status   = HTTP_STATUS_NOT_FOUND;
 
 
     const char *i_ctype = xs_dict_get(req, "content-type");
     const char *i_ctype = xs_dict_get(req, "content-type");
     xs *args      = NULL;
     xs *args      = NULL;
@@ -255,7 +255,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         args = xs_dup(xs_dict_get(req, "p_vars"));
         args = xs_dup(xs_dict_get(req, "p_vars"));
 
 
     if (args == NULL)
     if (args == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
     xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
 
 
@@ -274,7 +274,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-login",
                         redir, cid, state, USER_AGENT);
                         redir, cid, state, USER_AGENT);
         *ctype = "text/html";
         *ctype = "text/html";
-        status = 200;
+        status = HTTP_STATUS_OK;
 
 
         if (login && passwd && redir && cid) {
         if (login && passwd && redir && cid) {
             snac snac;
             snac snac;
@@ -296,7 +296,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
                         else
                         else
                             *body = xs_fmt("%s?code=%s", redir, code);
                             *body = xs_fmt("%s?code=%s", redir, code);
 
 
-                        status = 303;
+                        status = HTTP_STATUS_SEE_OTHER;
                     }
                     }
 
 
                     /* if there is a state, add it */
                     /* if there is a state, add it */
@@ -375,12 +375,12 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
             xs *app = app_get(cid);
             xs *app = app_get(cid);
 
 
             if (app == NULL) {
             if (app == NULL) {
-                status = 401;
+                status = HTTP_STATUS_UNAUTHORIZED;
                 srv_log(xs_fmt("oauth token: invalid app %s", cid));
                 srv_log(xs_fmt("oauth token: invalid app %s", cid));
             }
             }
             else
             else
             if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
             if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) {
-                status = 401;
+                status = HTTP_STATUS_UNAUTHORIZED;
                 srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
                 srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid));
             }
             }
             else {
             else {
@@ -397,7 +397,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 
 
                 *body  = xs_json_dumps(rsp, 4);
                 *body  = xs_json_dumps(rsp, 4);
                 *ctype = "application/json";
                 *ctype = "application/json";
-                status = 200;
+                status = HTTP_STATUS_OK;
 
 
                 const char *uid = xs_dict_get(app, "uid");
                 const char *uid = xs_dict_get(app, "uid");
 
 
@@ -416,7 +416,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         }
         }
         else {
         else {
             srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
             srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
-            status = 400;
+            status = HTTP_STATUS_BAD_REQUEST;
         }
         }
     }
     }
     else
     else
@@ -433,12 +433,12 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
 
 
             if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
             if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) {
                 srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
                 srv_debug(1, xs_fmt("oauth revoke: bad secret for token %s", tokid));
-                status = 403;
+                status = HTTP_STATUS_FORBIDDEN;
             }
             }
             else {
             else {
                 token_del(tokid);
                 token_del(tokid);
                 srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
                 srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
-                status = 200;
+                status = HTTP_STATUS_OK;
 
 
                 /* also delete the app, as it serves no purpose from now on */
                 /* also delete the app, as it serves no purpose from now on */
                 app_del(cid);
                 app_del(cid);
@@ -446,7 +446,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         }
         }
         else {
         else {
             srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
             srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
-            status = 403;
+            status = HTTP_STATUS_FORBIDDEN;
         }
         }
     }
     }
     if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
     if (strcmp(cmd, "/x-snac-get-token") == 0) { /** **/
@@ -459,7 +459,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
         *body  = xs_fmt(login_page, host, host, "LOGIN INCORRECT", proto, host, "oauth/x-snac-get-token",
                         "", "", "", USER_AGENT);
                         "", "", "", USER_AGENT);
         *ctype = "text/html";
         *ctype = "text/html";
-        status = 200;
+        status = HTTP_STATUS_OK;
 
 
         if (login && passwd) {
         if (login && passwd) {
             snac user;
             snac user;
@@ -1150,109 +1150,123 @@ int process_auth_token(snac *snac, const xs_dict *req)
     return logged_in;
     return logged_in;
 }
 }
 
 
-
-int mastoapi_get_handler(const xs_dict *req, const char *q_path,
-                         char **body, int *b_size, char **ctype)
+void credentials_get(char **body, char **ctype, int *status, snac snac)
 {
 {
-    (void)b_size;
+    xs *acct = xs_dict_new();
+
+    acct = xs_dict_append(acct, "id", snac.md5);
+    acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
+    acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
+    acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
+    acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
+    acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
+    acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
+    acct = xs_dict_append(acct, "url", snac.actor);
+    acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
+    acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
 
 
-    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
-        return 0;
+    xs *src = xs_json_loads("{\"privacy\":\"public\","
+        "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
+    /* some apps take the note from the source object */
+    src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
+    src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
 
 
-    int status    = 404;
-    const xs_dict *args = xs_dict_get(req, "q_vars");
-    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
+    const xs_str *cw = xs_dict_get(snac.config, "cw");
+    src = xs_dict_set(src, "sensitive",
+        strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
 
 
-    snac snac1 = {0};
-    int logged_in = process_auth_token(&snac1, req);
+    src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
 
 
-    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
-        if (logged_in) {
-            xs *acct = xs_dict_new();
-
-            acct = xs_dict_append(acct, "id",           snac1.md5);
-            acct = xs_dict_append(acct, "username",     xs_dict_get(snac1.config, "uid"));
-            acct = xs_dict_append(acct, "acct",         xs_dict_get(snac1.config, "uid"));
-            acct = xs_dict_append(acct, "display_name", xs_dict_get(snac1.config, "name"));
-            acct = xs_dict_append(acct, "created_at",   xs_dict_get(snac1.config, "published"));
-            acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
-            acct = xs_dict_append(acct, "note",         xs_dict_get(snac1.config, "bio"));
-            acct = xs_dict_append(acct, "url",          snac1.actor);
-            acct = xs_dict_append(acct, "locked",       xs_stock(XSTYPE_FALSE));
-            acct = xs_dict_append(acct, "bot",          xs_dict_get(snac1.config, "bot"));
-
-            xs *src = xs_json_loads("{\"privacy\":\"public\","
-                    "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
-            acct = xs_dict_append(acct, "source", src);
-
-            xs *avatar = NULL;
-            const char *av   = xs_dict_get(snac1.config, "avatar");
-
-            if (xs_is_null(av) || *av == '\0')
-                avatar = xs_fmt("%s/susie.png", srv_baseurl);
-            else
-                avatar = xs_dup(av);
+    xs *avatar = NULL;
+    const char *av = xs_dict_get(snac.config, "avatar");
 
 
-            acct = xs_dict_append(acct, "avatar", avatar);
-            acct = xs_dict_append(acct, "avatar_static", avatar);
+    if (xs_is_null(av) || *av == '\0')
+        avatar = xs_fmt("%s/susie.png", srv_baseurl);
+    else
+        avatar = xs_dup(av);
 
 
-            xs *header = NULL;
-            const char *hd = xs_dict_get(snac1.config, "header");
+    acct = xs_dict_append(acct, "avatar", avatar);
+    acct = xs_dict_append(acct, "avatar_static", avatar);
 
 
-            if (!xs_is_null(hd))
-                header = xs_dup(hd);
-            else
-                header = xs_fmt("%s/header.png", srv_baseurl);
+    xs *header = NULL;
+    const char *hd = xs_dict_get(snac.config, "header");
 
 
-            acct = xs_dict_append(acct, "header",        header);
-            acct = xs_dict_append(acct, "header_static", header);
+    if (!xs_is_null(hd))
+        header = xs_dup(hd);
+    else
+        header = xs_fmt("%s/header.png", srv_baseurl);
 
 
-            const xs_dict *metadata = xs_dict_get(snac1.config, "metadata");
-            if (xs_type(metadata) == XSTYPE_DICT) {
-                xs *fields = xs_list_new();
-                const xs_str *k;
-                const xs_str *v;
+    acct = xs_dict_append(acct, "header", header);
+    acct = xs_dict_append(acct, "header_static", header);
 
 
-                xs_dict *val_links = snac1.links;
-                if (xs_is_null(val_links))
-                    val_links = xs_stock(XSTYPE_DICT);
+    const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
+    if (xs_type(metadata) == XSTYPE_DICT) {
+        xs *fields = xs_list_new();
+        const xs_str *k;
+        const xs_str *v;
 
 
-                int c = 0;
-                while (xs_dict_next(metadata, &k, &v, &c)) {
-                    xs *val_date = NULL;
+        xs_dict *val_links = snac.links;
+        if (xs_is_null(val_links))
+            val_links = xs_stock(XSTYPE_DICT);
 
 
-                    const xs_number *verified_time = xs_dict_get(val_links, v);
-                    if (xs_type(verified_time) == XSTYPE_NUMBER) {
-                        time_t t = xs_number_get(verified_time);
+        int c = 0;
+        while (xs_dict_next(metadata, &k, &v, &c)) {
+            xs *val_date = NULL;
 
 
-                        if (t > 0)
-                            val_date = xs_str_utctime(t, ISO_DATE_SPEC);
-                    }
+            const xs_number *verified_time = xs_dict_get(val_links, v);
+            if (xs_type(verified_time) == XSTYPE_NUMBER) {
+                time_t t = xs_number_get(verified_time);
 
 
-                    xs *d = xs_dict_new();
+                if (t > 0)
+                    val_date = xs_str_utctime(t, ISO_DATE_SPEC);
+            }
 
 
-                    d = xs_dict_append(d, "name", k);
-                    d = xs_dict_append(d, "value", v);
-                    d = xs_dict_append(d, "verified_at",
-                        xs_type(val_date) == XSTYPE_STRING && *val_date ?
-                            val_date : xs_stock(XSTYPE_NULL));
+            xs *d = xs_dict_new();
 
 
-                    fields = xs_list_append(fields, d);
-                }
+            d = xs_dict_append(d, "name", k);
+            d = xs_dict_append(d, "value", v);
+            d = xs_dict_append(d, "verified_at",
+                               xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
 
 
-                acct = xs_dict_set(acct, "fields", fields);
-            }
+            fields = xs_list_append(fields, d);
+        }
 
 
-            acct = xs_dict_append(acct, "followers_count", xs_stock(0));
-            acct = xs_dict_append(acct, "following_count", xs_stock(0));
-            acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
+        acct = xs_dict_set(acct, "fields", fields);
+        /* some apps take the fields from the source object */
+        src = xs_dict_set(src, "fields", fields);
+    }
 
 
-            *body  = xs_json_dumps(acct, 4);
-            *ctype = "application/json";
-            status = 200;
+    acct = xs_dict_append(acct, "source", src);
+    acct = xs_dict_append(acct, "followers_count", xs_stock(0));
+    acct = xs_dict_append(acct, "following_count", xs_stock(0));
+    acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
+
+    *body = xs_json_dumps(acct, 4);
+    *ctype = "application/json";
+    *status = HTTP_STATUS_OK;
+}
+
+int mastoapi_get_handler(const xs_dict *req, const char *q_path,
+                         char **body, int *b_size, char **ctype)
+{
+    (void)b_size;
+
+    if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
+        return 0;
+
+    int status    = HTTP_STATUS_NOT_FOUND;
+    const xs_dict *args = xs_dict_get(req, "q_vars");
+    xs *cmd       = xs_replace_n(q_path, "/api", "", 1);
+
+    snac snac1 = {0};
+    int logged_in = process_auth_token(&snac1, req);
+
+    if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
+        if (logged_in) {
+            credentials_get(body, ctype, &status, snac1);
         }
         }
         else {
         else {
-            status = 422;   // "Unprocessable entity" (no login)
+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
         }
         }
     }
     }
     else
     else
@@ -1279,10 +1293,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(res, 4);
             *body  = xs_json_dumps(res, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 422;
+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
     if (strcmp(cmd, "/v1/accounts/lookup") == 0) { /** **/
@@ -1304,7 +1318,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
                     *body  = xs_json_dumps(macct, 4);
                     *body  = xs_json_dumps(macct, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
 
 
                     user_free(&user);
                     user_free(&user);
                 }
                 }
@@ -1429,6 +1443,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                         }
                         }
                     }
                     }
                 }
                 }
+                else
+                if (strcmp(opt, "featured_tags") == 0) {
+                    /* snac doesn't have features tags, yet? */
+                    /* implement empty response so apps like Tokodon don't show an error */
+                    *body  = xs_dup("[]");
+                    *ctype = "application/json";
+                    status = HTTP_STATUS_OK;
+                }
 
 
                 user_free(&snac2);
                 user_free(&snac2);
             }
             }
@@ -1441,16 +1463,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                     }
                     }
                     else
                     else
                     if (strcmp(opt, "statuses") == 0) {
                     if (strcmp(opt, "statuses") == 0) {
-                        /* we don't serve statuses of others; return the empty list */
+                        /* we don't serve statuses of others; return the empty list */ 
                         out = xs_list_new();
                         out = xs_list_new();
                     }
                     }
+                    else
+                    if (strcmp(opt, "featured_tags") == 0) {
+                        /* snac doesn't have features tags, yet? */
+                        /* implement empty response so apps like Tokodon don't show an error */
+                        *body  = xs_dup("[]");
+                        *ctype = "application/json";
+                        status = HTTP_STATUS_OK;
+                    }
                 }
                 }
             }
             }
 
 
             if (out != NULL) {
             if (out != NULL) {
                 *body  = xs_json_dumps(out, 4);
                 *body  = xs_json_dumps(out, 4);
                 *ctype = "application/json";
                 *ctype = "application/json";
-                status = 200;
+                status = HTTP_STATUS_OK;
             }
             }
         }
         }
     }
     }
@@ -1554,12 +1584,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(out, 4);
             *body  = xs_json_dumps(out, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
 
 
             srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
             srv_debug(2, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out)));
         }
         }
         else {
         else {
-            status = 401; // unauthorized
+            status = HTTP_STATUS_UNAUTHORIZED;
         }
         }
     }
     }
     else
     else
@@ -1612,7 +1642,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
         *body  = xs_json_dumps(out, 4);
         *body  = xs_json_dumps(out, 4);
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
     if (xs_startswith(cmd, "/v1/timelines/tag/")) { /** **/
@@ -1661,7 +1691,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
         *body  = xs_json_dumps(out, 4);
         *body  = xs_json_dumps(out, 4);
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
     if (xs_startswith(cmd, "/v1/timelines/list/")) { /** **/
@@ -1729,17 +1759,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(out, 4);
             *body  = xs_json_dumps(out, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 421;
+            status = HTTP_STATUS_MISDIRECTED_REQUEST;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
     if (strcmp(cmd, "/v1/conversations") == 0) { /** **/
         /* TBD */
         /* TBD */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
     if (strcmp(cmd, "/v1/notifications") == 0) { /** **/
@@ -1817,17 +1847,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(out, 4);
             *body  = xs_json_dumps(out, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/filters") == 0) { /** **/
     if (strcmp(cmd, "/v1/filters") == 0) { /** **/
         /* snac will never have filters */
         /* snac will never have filters */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v2/filters") == 0) { /** **/
     if (strcmp(cmd, "/v2/filters") == 0) { /** **/
@@ -1836,21 +1866,21 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
          * in some apps */
          * in some apps */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
     if (strcmp(cmd, "/v1/favourites") == 0) { /** **/
         /* snac will never support a list of favourites */
         /* snac will never support a list of favourites */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
     if (strcmp(cmd, "/v1/bookmarks") == 0) { /** **/
         /* snac does not support bookmarks */
         /* snac does not support bookmarks */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
     if (strcmp(cmd, "/v1/lists") == 0) { /** list of lists **/
@@ -1873,7 +1903,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(l, 4);
             *body  = xs_json_dumps(l, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
     }
     }
     else
     else
@@ -1903,7 +1933,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
                         *body  = xs_json_dumps(out, 4);
                         *body  = xs_json_dumps(out, 4);
                         *ctype = "application/json";
                         *ctype = "application/json";
-                        status = 200;
+                        status = HTTP_STATUS_OK;
                     }
                     }
                 }
                 }
                 else
                 else
@@ -1931,7 +1961,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
                     *body  = xs_json_dumps(out, 4);
                     *body  = xs_json_dumps(out, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
@@ -1941,28 +1971,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
         /* snac does not schedule notes */
         /* snac does not schedule notes */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
     if (strcmp(cmd, "/v1/follow_requests") == 0) { /** **/
         /* snac does not support optional follow confirmations */
         /* snac does not support optional follow confirmations */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
     if (strcmp(cmd, "/v1/announcements") == 0) { /** **/
         /* snac has no announcements (yet?) */
         /* snac has no announcements (yet?) */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
     if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/
         /* are you kidding me? */
         /* are you kidding me? */
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/instance") == 0) { /** **/
     if (strcmp(cmd, "/v1/instance") == 0) { /** **/
@@ -2017,7 +2047,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 "\"max_characters\":100000,\"max_media_attachments\":8}");
                 "\"max_characters\":100000,\"max_media_attachments\":8}");
             cfg = xs_dict_append(cfg, "statuses", d11);
             cfg = xs_dict_append(cfg, "statuses", d11);
 
 
-            xs *d12 = xs_json_loads("{\"max_featured_tags\":10}");
+            xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
             cfg = xs_dict_append(cfg, "accounts", d12);
             cfg = xs_dict_append(cfg, "accounts", d12);
 
 
             xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
             xs *d13 = xs_json_loads("{\"image_matrix_limit\":33177600,"
@@ -2075,7 +2105,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
         *body  = xs_json_dumps(ins, 4);
         *body  = xs_json_dumps(ins, 4);
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
     if (xs_startswith(cmd, "/v1/statuses/")) { /** **/
@@ -2188,30 +2218,30 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 if (out != NULL) {
                 if (out != NULL) {
                     *body  = xs_json_dumps(out, 4);
                     *body  = xs_json_dumps(out, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
     if (strcmp(cmd, "/v1/preferences") == 0) { /** **/
         *body  = xs_dup("{}");
         *body  = xs_dup("{}");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/markers") == 0) { /** **/
     if (strcmp(cmd, "/v1/markers") == 0) { /** **/
         *body  = xs_dup("{}");
         *body  = xs_dup("{}");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
     if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
         *body  = xs_dup("[]");
         *body  = xs_dup("[]");
         *ctype = "application/json";
         *ctype = "application/json";
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (strcmp(cmd, "/v2/search") == 0) { /** **/
     if (strcmp(cmd, "/v2/search") == 0) { /** **/
@@ -2290,10 +2320,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(res, 4);
             *body  = xs_json_dumps(res, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
 
 
     /* user cleanup */
     /* user cleanup */
@@ -2316,7 +2346,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
-    int status    = 404;
+    int status    = HTTP_STATUS_NOT_FOUND;
     xs *args      = NULL;
     xs *args      = NULL;
     const char *i_ctype = xs_dict_get(req, "content-type");
     const char *i_ctype = xs_dict_get(req, "content-type");
 
 
@@ -2336,7 +2366,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
         args = xs_dup(xs_dict_get(req, "p_vars"));
         args = xs_dup(xs_dict_get(req, "p_vars"));
 
 
     if (args == NULL)
     if (args == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 
 
@@ -2378,7 +2408,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(app, 4);
             *body  = xs_json_dumps(app, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
 
 
             app = xs_dict_append(app, "code", "");
             app = xs_dict_append(app, "code", "");
 
 
@@ -2470,10 +2500,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(st, 4);
             *body  = xs_json_dumps(st, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/statuses")) { /** **/
     if (xs_startswith(cmd, "/v1/statuses")) { /** **/
@@ -2552,7 +2582,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                         if (pin(&snac, id))
                         if (pin(&snac, id))
                             out = mastoapi_status(&snac, msg);
                             out = mastoapi_status(&snac, msg);
                         else
                         else
-                            status = 422;
+                            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
                     }
                     }
                     else
                     else
                     if (strcmp(op, "unpin") == 0) { /** **/
                     if (strcmp(op, "unpin") == 0) { /** **/
@@ -2573,12 +2603,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                 if (out != NULL) {
                 if (out != NULL) {
                     *body  = xs_json_dumps(out, 4);
                     *body  = xs_json_dumps(out, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
     if (strcmp(cmd, "/v1/notifications/clear") == 0) { /** **/
@@ -2588,10 +2618,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_dup("{}");
             *body  = xs_dup("{}");
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
     if (strcmp(cmd, "/v1/push/subscription") == 0) { /** **/
@@ -2616,10 +2646,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
             *body  = xs_json_dumps(wpush, 4);
             *body  = xs_json_dumps(wpush, 4);
             *ctype = "application/json";
             *ctype = "application/json";
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
     if (strcmp(cmd, "/v1/media") == 0 || strcmp(cmd, "/v2/media") == 0) { /** **/
@@ -2630,7 +2660,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
             if (xs_is_null(desc))
             if (xs_is_null(desc))
                 desc = "";
                 desc = "";
 
 
-            status = 400;
+            status = HTTP_STATUS_BAD_REQUEST;
 
 
             if (xs_type(file) == XSTYPE_LIST) {
             if (xs_type(file) == XSTYPE_LIST) {
                 const char *fn = xs_list_get(file, 0);
                 const char *fn = xs_list_get(file, 0);
@@ -2659,12 +2689,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
 
                     *body  = xs_json_dumps(rsp, 4);
                     *body  = xs_json_dumps(rsp, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/accounts")) { /** **/
     if (xs_startswith(cmd, "/v1/accounts")) { /** **/
@@ -2744,11 +2774,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
             if (rsp != NULL) {
             if (rsp != NULL) {
                 *body  = xs_json_dumps(rsp, 4);
                 *body  = xs_json_dumps(rsp, 4);
                 *ctype = "application/json";
                 *ctype = "application/json";
-                status = 200;
+                status = HTTP_STATUS_OK;
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/polls")) { /** **/
     if (xs_startswith(cmd, "/v1/polls")) { /** **/
@@ -2810,12 +2840,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                 if (out != NULL) {
                 if (out != NULL) {
                     *body  = xs_json_dumps(out, 4);
                     *body  = xs_json_dumps(out, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (strcmp(cmd, "/v1/lists") == 0) {
     if (strcmp(cmd, "/v1/lists") == 0) {
@@ -2831,18 +2861,18 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                     out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
                     out = xs_dict_append(out, "replies_policy", xs_dict_get_def(args, "replies_policy", "list"));
                     out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
                     out = xs_dict_append(out, "exclusive", xs_stock(XSTYPE_FALSE));
 
 
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
                 else {
                 else {
                     out = xs_dict_append(out, "error", "cannot create list");
                     out = xs_dict_append(out, "error", "cannot create list");
-                    status = 422;
+                    status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
                 }
                 }
 
 
                 *body  = xs_json_dumps(out, 4);
                 *body  = xs_json_dumps(out, 4);
                 *ctype = "application/json";
                 *ctype = "application/json";
             }
             }
             else
             else
-                status = 422;
+                status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
         }
         }
     }
     }
     if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
     if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
@@ -2861,12 +2891,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                         list_content(&snac, id, v, 1);
                         list_content(&snac, id, v, 1);
                     }
                     }
 
 
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 422;
+            status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
     }
     }
 
 
     /* user cleanup */
     /* user cleanup */
@@ -2891,7 +2921,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
-    int status    = 404;
+    int status    = HTTP_STATUS_NOT_FOUND;
     xs *args      = NULL;
     xs *args      = NULL;
     const char *i_ctype = xs_dict_get(req, "content-type");
     const char *i_ctype = xs_dict_get(req, "content-type");
 
 
@@ -2911,7 +2941,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
         args = xs_dup(xs_dict_get(req, "p_vars"));
         args = xs_dup(xs_dict_get(req, "p_vars"));
 
 
     if (args == NULL)
     if (args == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     snac snac = {0};
     snac snac = {0};
     int logged_in = process_auth_token(&snac, req);
     int logged_in = process_auth_token(&snac, req);
@@ -2920,7 +2950,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 
 
     if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
     if (xs_startswith(cmd, "/v1/push/subscription") || xs_startswith(cmd, "/v2/push/subscription")) { /** **/
         // pretend we deleted it, since it doesn't exist anyway
         // pretend we deleted it, since it doesn't exist anyway
-        status = 200;
+        status = HTTP_STATUS_OK;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/lists/")) {
     if (xs_startswith(cmd, "/v1/lists/")) {
@@ -2948,10 +2978,10 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
                 }
                 }
             }
             }
 
 
-            status = 200;
+            status = HTTP_STATUS_OK;
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
 
 
     /* user cleanup */
     /* user cleanup */
@@ -2974,7 +3004,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
     if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
         return 0;
         return 0;
 
 
-    int status    = 404;
+    int status    = HTTP_STATUS_NOT_FOUND;
     xs *args      = NULL;
     xs *args      = NULL;
     const char *i_ctype = xs_dict_get(req, "content-type");
     const char *i_ctype = xs_dict_get(req, "content-type");
 
 
@@ -2986,7 +3016,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
         args = xs_dup(xs_dict_get(req, "p_vars"));
         args = xs_dup(xs_dict_get(req, "p_vars"));
 
 
     if (args == NULL)
     if (args == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
     xs *cmd = xs_replace_n(q_path, "/api", "", 1);
 
 
@@ -3017,11 +3047,11 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 
 
                 *body  = xs_json_dumps(rsp, 4);
                 *body  = xs_json_dumps(rsp, 4);
                 *ctype = "application/json";
                 *ctype = "application/json";
-                status = 200;
+                status = HTTP_STATUS_OK;
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
     else
     else
     if (xs_startswith(cmd, "/v1/statuses")) {
     if (xs_startswith(cmd, "/v1/statuses")) {
@@ -3060,12 +3090,12 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
                 if (rsp != NULL) {
                 if (rsp != NULL) {
                     *body  = xs_json_dumps(rsp, 4);
                     *body  = xs_json_dumps(rsp, 4);
                     *ctype = "application/json";
                     *ctype = "application/json";
-                    status = 200;
+                    status = HTTP_STATUS_OK;
                 }
                 }
             }
             }
         }
         }
         else
         else
-            status = 401;
+            status = HTTP_STATUS_UNAUTHORIZED;
     }
     }
 
 
     /* user cleanup */
     /* user cleanup */
@@ -3077,6 +3107,158 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
     return status;
     return status;
 }
 }
 
 
+void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
+/* Store header or avatar */
+{
+    if (data != NULL) {
+        if (xs_type(data) == XSTYPE_LIST) {
+            const char *fn = xs_list_get(data, 0);
+
+            if (fn && *fn) {
+                const char *ext = strrchr(fn, '.');
+
+                /* Mona iOS sends always jpg as application/octet-stream with no filename */
+                if (ext == NULL || strcmp(fn, key) == 0) {
+                    ext = ".jpg";
+                }
+
+                /* Make sure we have a unique file name, otherwise updated images will not be
+                 * re-loaded by clients. */
+                xs *rnd         = random_str();
+                xs *hash        = xs_md5_hex(rnd, strlen(rnd));
+                xs *id          = xs_fmt("%s%s", hash, ext);
+                xs *url         = xs_fmt("%s/s/%s", snac->actor, id);
+                int fo          = xs_number_get(xs_list_get(data, 1));
+                int fs          = xs_number_get(xs_list_get(data, 2));
+
+                /* store */
+                static_put(snac, id, payload + fo, fs);
+
+                snac->config = xs_dict_set(snac->config, key, url);
+            }
+        }
+    }
+}
+
+int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
+                          const char *payload, int p_size,
+                          char **body, int *b_size, char **ctype)
+/* Handle profile updates */
+{
+    (void)p_size;
+    (void)b_size;
+
+    if (!xs_startswith(q_path, "/api/v1/"))
+        return 0;
+
+    int status    = HTTP_STATUS_NOT_FOUND;
+    xs *args      = NULL;
+    const char *i_ctype = xs_dict_get(req, "content-type");
+
+    if (i_ctype && xs_startswith(i_ctype, "application/json")) {
+        if (!xs_is_null(payload))
+            args = xs_json_loads(payload);
+    }
+    else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded"))
+    {
+        // Some apps send form data instead of json so we should cater for those
+        if (!xs_is_null(payload)) {
+            xs *upl = xs_url_dec(payload);
+            args    = xs_url_vars(upl);
+        }
+    }
+    else
+        args = xs_dup(xs_dict_get(req, "p_vars"));
+
+    if (args == NULL)
+        return HTTP_STATUS_BAD_REQUEST;
+
+    xs *cmd = xs_replace_n(q_path, "/api", "", 1);
+
+    snac snac = {0};
+    int logged_in = process_auth_token(&snac, req);
+
+    if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
+        /* Update user profile fields */
+        if (logged_in) {
+            int c = 0;
+            const xs_str *k;
+            const xs_val *v;
+            const xs_str *field_name = NULL;
+            xs_dict *new_fields = xs_dict_new();
+            while (xs_dict_next(args, &k, &v, &c)) {
+                if (strcmp(k, "display_name") == 0) {
+                    if (v != NULL)
+                        snac.config = xs_dict_set(snac.config, "name", v);
+                }
+                else
+                if (strcmp(k, "note") == 0) {
+                    if (v != NULL)
+                        snac.config = xs_dict_set(snac.config, "bio", v);
+                }
+                else
+                if (strcmp(k, "bot") == 0) {
+                    if (v != NULL)
+                        snac.config = xs_dict_set(snac.config, "bot",
+                            (strcmp(v, "true") == 0 ||
+                                strcmp(v, "1") == 0) ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
+                }
+                else
+                if (strcmp(k, "source[sensitive]") == 0) {
+                    if (v != NULL)
+                        snac.config = xs_dict_set(snac.config, "cw",
+                            strcmp(v, "true") == 0 ? "open" : "");
+                }
+                else
+                if (strcmp(k, "source[privacy]") == 0) {
+                    if (v != NULL)
+                        snac.config = xs_dict_set(snac.config, "private",
+                            strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
+                }
+                else
+                if (strcmp(k, "header") == 0) {
+                    persist_image("header", v, payload, &snac);
+                }
+                else
+                if (strcmp(k, "avatar") == 0) {
+                    persist_image("avatar", v, payload, &snac);
+                }
+                else
+                if (xs_starts_and_ends("fields_attributes", k, "[name]")) {
+                    field_name = strcmp(v, "") != 0 ? v : NULL;
+                }
+                else
+                if (xs_starts_and_ends("fields_attributes", k, "[value]")) {
+                    if (field_name != NULL) {
+                        new_fields = xs_dict_set(new_fields, field_name, v);
+                        snac.config = xs_dict_set(snac.config, "metadata", new_fields);
+                    }
+                }
+                /* we don't have support for the following options, yet
+                   - discoverable (0/1)
+                   - locked (0/1)
+                 */
+            }
+
+            /* Persist profile */
+            if (user_persist(&snac) == 0)
+                credentials_get(body, ctype, &status, snac);
+            else
+                status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
+        }
+        else
+            status = HTTP_STATUS_UNAUTHORIZED;
+    }
+
+    /* user cleanup */
+    if (logged_in)
+        user_free(&snac);
+
+    srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
+
+    return status;
+}
+
 
 
 void mastoapi_purge(void)
 void mastoapi_purge(void)
 {
 {

+ 12 - 0
snac.c

@@ -170,3 +170,15 @@ int check_password(const char *uid, const char *passwd, const char *hash)
 
 
     return ret;
     return ret;
 }
 }
+
+
+const char *http_status_text(int status)
+/* translate status codes to canonical status texts */
+{
+    switch (status) {
+#define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text;
+#include "http_codes.h"
+#undef HTTP_STATUS
+        default: return "Unknown";
+    }
+}

+ 14 - 0
snac.h

@@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
 void user_free(snac *snac);
 void user_free(snac *snac);
 xs_list *user_list(void);
 xs_list *user_list(void);
 int user_open_by_md5(snac *snac, const char *md5);
 int user_open_by_md5(snac *snac, const char *md5);
+int user_persist(snac *snac);
 
 
 int validate_uid(const char *uid);
 int validate_uid(const char *uid);
 
 
@@ -358,6 +359,19 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
 int mastoapi_put_handler(const xs_dict *req, const char *q_path,
 int mastoapi_put_handler(const xs_dict *req, const char *q_path,
                           const char *payload, int p_size,
                           const char *payload, int p_size,
                           char **body, int *b_size, char **ctype);
                           char **body, int *b_size, char **ctype);
+void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac);
+int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
+                          const char *payload, int p_size,
+                          char **body, int *b_size, char **ctype);
 void mastoapi_purge(void);
 void mastoapi_purge(void);
 
 
 void verify_links(snac *user);
 void verify_links(snac *user);
+
+
+typedef enum {
+#define HTTP_STATUS(code, name, text) HTTP_STATUS_ ## name = code,
+#include "http_codes.h"
+#undef HTTP_STATUS
+} http_status;
+
+const char *http_status_text(int status);

+ 4 - 4
webfinger.c

@@ -42,7 +42,7 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
     }
     }
 
 
     if (host == NULL || resource == NULL)
     if (host == NULL || resource == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     headers = xs_dict_append(headers, "accept",     "application/json");
     headers = xs_dict_append(headers, "accept",     "application/json");
     headers = xs_dict_append(headers, "user-agent", USER_AGENT);
     headers = xs_dict_append(headers, "user-agent", USER_AGENT);
@@ -139,7 +139,7 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
     const char *resource = xs_dict_get(q_vars, "resource");
     const char *resource = xs_dict_get(q_vars, "resource");
 
 
     if (resource == NULL)
     if (resource == NULL)
-        return 400;
+        return HTTP_STATUS_BAD_REQUEST;
 
 
     snac snac;
     snac snac;
     int found = 0;
     int found = 0;
@@ -220,12 +220,12 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
 
 
         user_free(&snac);
         user_free(&snac);
 
 
-        status = 200;
+        status = HTTP_STATUS_OK;
         *body  = j;
         *body  = j;
         *ctype = "application/jrd+json";
         *ctype = "application/jrd+json";
     }
     }
     else
     else
-        status = 404;
+        status = HTTP_STATUS_NOT_FOUND;
 
 
     srv_debug(1, xs_fmt("webfinger_get_handler resource=%s %d", resource, status));
     srv_debug(1, xs_fmt("webfinger_get_handler resource=%s %d", resource, status));
 
 

+ 1 - 1
xs.h

@@ -52,7 +52,7 @@ typedef char xs_data;
 
 
 void *xs_free(void *ptr);
 void *xs_free(void *ptr);
 void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
 void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
-#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__)
+#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __func__)
 int _xs_blk_size(int sz);
 int _xs_blk_size(int sz);
 void _xs_destroy(char **var);
 void _xs_destroy(char **var);
 #define xs_debug() raise(SIGTRAP)
 #define xs_debug() raise(SIGTRAP)

+ 3 - 3
xs_httpd.h

@@ -5,7 +5,7 @@
 #define _XS_HTTPD_H
 #define _XS_HTTPD_H
 
 
 xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size);
 xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size);
-void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size);
+void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size);
 
 
 
 
 #ifdef XS_IMPLEMENTATION
 #ifdef XS_IMPLEMENTATION
@@ -95,14 +95,14 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
 }
 }
 
 
 
 
-void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size)
+void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size)
 /* sends an httpd response */
 /* sends an httpd response */
 {
 {
     xs *proto;
     xs *proto;
     const xs_str *k;
     const xs_str *k;
     const xs_val *v;
     const xs_val *v;
 
 
-    proto = xs_fmt("HTTP/1.1 %d %s", status, status / 100 == 2 ? "OK" : "ERROR");
+    proto = xs_fmt("HTTP/1.1 %d %s", status, status_text);
     fprintf(f, "%s\r\n", proto);
     fprintf(f, "%s\r\n", proto);
 
 
     int c = 0;
     int c = 0;

+ 1 - 1
xs_set.h

@@ -111,4 +111,4 @@ int xs_set_add(xs_set *s, const xs_val *data)
 
 
 #endif /* XS_IMPLEMENTATION */
 #endif /* XS_IMPLEMENTATION */
 
 
-#endif /* XS_SET_H */
+#endif /* XS_SET_H */

+ 50 - 22
xs_url.h

@@ -8,7 +8,6 @@ xs_str *xs_url_dec(const char *str);
 xs_dict *xs_url_vars(const char *str);
 xs_dict *xs_url_vars(const char *str);
 xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
 xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
 
 
-
 #ifdef XS_IMPLEMENTATION
 #ifdef XS_IMPLEMENTATION
 
 
 xs_str *xs_url_dec(const char *str)
 xs_str *xs_url_dec(const char *str)
@@ -107,7 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
         if (xs_list_len(l1) != 2)
         if (xs_list_len(l1) != 2)
             return NULL;
             return NULL;
 
 
-        boundary = xs_fmt("--%s", xs_list_get(l1, 1));
+        boundary = xs_dup(xs_list_get(l1, 1));
+
+        /* Tokodon sends the boundary header with double quotes surrounded */
+        if (xs_starts_and_ends("\"", boundary, "\"") != 0)
+            boundary = xs_strip_chars_i(boundary, "\"");
+
+        boundary = xs_fmt("--%s", boundary);
     }
     }
 
 
     bsz = strlen(boundary);
     bsz = strlen(boundary);
@@ -120,6 +125,7 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
         xs *l1 = NULL;
         xs *l1 = NULL;
         const char *vn = NULL;
         const char *vn = NULL;
         const char *fn = NULL;
         const char *fn = NULL;
+        const char *ct = NULL;
         char *q;
         char *q;
         int po, ps;
         int po, ps;
 
 
@@ -132,32 +138,47 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
         /* skip the \r\n */
         /* skip the \r\n */
         p += 2;
         p += 2;
 
 
-        /* now on a Content-Disposition... line; get it */
-        q = strchr(p, '\r');
-        s1 = xs_realloc(NULL, q - p + 1);
-        memcpy(s1, p, q - p);
-        s1[q - p] = '\0';
-
-        /* move on (over a \r\n) */
-        p = q;
-
-        /* split by " like a primitive man */
-        l1 = xs_split(s1, "\"");
+        /* Tokodon sends also a Content-Type headers,
+           let's use it to determine the file type */
+        do {
+            if (p[0] == 13 && p[1] == 10)
+                break;
+            q = strchr(p, '\r');
+            s1 = xs_realloc(NULL, q - p + 1);
+            memcpy(s1, p, q - p);
+            s1[q - p] = '\0';
+
+            if (xs_startswith(s1, "Content-Disposition")) {
+                /* split by " like a primitive man */
+                l1 = xs_split(s1, "\"");
+
+                /* get the variable name */
+                vn = xs_list_get(l1, 1);
+
+                /* is it an attached file? */
+                if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
+                    /* get the file name */
+                    fn = xs_list_get(l1, 3);
+                }
+            }
+            else
+            if (xs_startswith(s1, "Content-Type")) {
+                l1 = xs_split(s1, ":");
 
 
-        /* get the variable name */
-        vn = xs_list_get(l1, 1);
+                if (xs_list_len(l1) >= 2) {
+                    ct = xs_lstrip_chars_i(xs_dup(xs_list_get(l1, 1)), " ");
+                }
+            }
 
 
-        /* is it an attached file? */
-        if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
-            /* get the file name */
-            fn = xs_list_get(l1, 3);
-        }
+            p += (q - p);
+            p += 2; // Skip /r/n
+        } while (1);
 
 
         /* find the start of the part content */
         /* find the start of the part content */
-        if ((p = xs_memmem(p, p_size - (p - payload), "\r\n\r\n", 4)) == NULL)
+        if ((p = xs_memmem(p, p_size - (p - payload), "\r\n", 2)) == NULL)
             break;
             break;
 
 
-        p += 4;
+        p += 2; // Skip empty line
 
 
         /* find the next boundary */
         /* find the next boundary */
         if ((q = xs_memmem(p, p_size - (p - payload), boundary, bsz)) == NULL)
         if ((q = xs_memmem(p, p_size - (p - payload), boundary, bsz)) == NULL)
@@ -169,6 +190,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
         /* is it a filename? */
         /* is it a filename? */
         if (fn != NULL) {
         if (fn != NULL) {
             /* p_var value is a list */
             /* p_var value is a list */
+            /* if filename has no extension and content-type is image, attach extension to the filename */
+            if (strchr(fn, '.') == NULL && xs_startswith(ct, "image/")) {
+                char *ext = strchr(ct, '/');
+                ext++;
+                fn = xs_str_cat(xs_str_new(""), fn, ".", ext);
+            }
+
             xs *l1 = xs_list_new();
             xs *l1 = xs_list_new();
             xs *vpo = xs_number_new(po);
             xs *vpo = xs_number_new(po);
             xs *vps = xs_number_new(ps);
             xs *vps = xs_number_new(ps);