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