summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Louis Brauer2024-05-27 12:24:17 +0200
committerGravatar Louis Brauer2024-05-27 12:24:17 +0200
commit81cf309e4d0ba6c2debccc21ea4f85e1e6245dc5 (patch)
tree10528a18c859964fa11eabe15955c85dce7ecf25
parentUse enum instead of numeric status codes for HTTP statuses (diff)
downloadpenes-snac2-81cf309e4d0ba6c2debccc21ea4f85e1e6245dc5.tar.gz
penes-snac2-81cf309e4d0ba6c2debccc21ea4f85e1e6245dc5.tar.xz
penes-snac2-81cf309e4d0ba6c2debccc21ea4f85e1e6245dc5.zip
Implement Mastodon PATCH endpoint for account profile updates
-rw-r--r--data.c27
-rw-r--r--html.c22
-rw-r--r--httpd.c10
-rw-r--r--mastoapi.c316
-rw-r--r--snac.h5
5 files changed, 279 insertions, 101 deletions
diff --git a/data.c b/data.c
index 8fdd292..e24bf16 100644
--- a/data.c
+++ b/data.c
@@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
303 return 0; 303 return 0;
304} 304}
305 305
306int user_persist(snac *snac)
307/* store user */
308{
309 xs *fn = xs_fmt("%s/user.json", snac->basedir);
310 xs *bfn = xs_fmt("%s.bak", fn);
311 FILE *f;
312
313 rename(fn, bfn);
314
315 if ((f = fopen(fn, "w")) != NULL) {
316 xs_json_dump(snac->config, 4, f);
317 fclose(f);
318 }
319 else
320 rename(bfn, fn);
321
322 history_del(snac, "timeline.html_");
323
324 xs *a_msg = msg_actor(snac);
325 xs *u_msg = msg_update(snac, a_msg);
326
327 enqueue_message(snac, u_msg);
328 enqueue_verify_links(snac);
329
330 return 0;
331}
332
306 333
307double mtime_nl(const char *fn, int *n_link) 334double mtime_nl(const char *fn, int *n_link)
308/* returns the mtime and number of links of a file or directory, or 0.0 */ 335/* returns the mtime and number of links of a file or directory, or 0.0 */
diff --git a/html.c b/html.c
index 1840799..6267adf 100644
--- a/html.c
+++ b/html.c
@@ -3334,27 +3334,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
3334 snac.config = xs_dict_set(snac.config, "passwd", pw); 3334 snac.config = xs_dict_set(snac.config, "passwd", pw);
3335 } 3335 }
3336 3336
3337 xs *fn = xs_fmt("%s/user.json", snac.basedir); 3337 user_persist(&snac);
3338 xs *bfn = xs_fmt("%s.bak", fn);
3339 FILE *f;
3340
3341 rename(fn, bfn);
3342
3343 if ((f = fopen(fn, "w")) != NULL) {
3344 xs_json_dump(snac.config, 4, f);
3345 fclose(f);
3346 }
3347 else
3348 rename(bfn, fn);
3349
3350 history_del(&snac, "timeline.html_");
3351
3352 xs *a_msg = msg_actor(&snac);
3353 xs *u_msg = msg_update(&snac, a_msg);
3354
3355 enqueue_message(&snac, u_msg);
3356
3357 enqueue_verify_links(&snac);
3358 3338
3359 status = HTTP_STATUS_SEE_OTHER; 3339 status = HTTP_STATUS_SEE_OTHER;
3360 } 3340 }
diff --git a/httpd.c b/httpd.c
index 60afe28..1c60a56 100644
--- a/httpd.c
+++ b/httpd.c
@@ -362,6 +362,16 @@ void httpd_connection(FILE *f)
362 362
363 } 363 }
364 else 364 else
365 if (strcmp(method, "PATCH") == 0) {
366
367#ifndef NO_MASTODON_API
368 if (status == 0)
369 status = mastoapi_patch_handler(req, q_path,
370 payload, p_size, &body, &b_size, &ctype);
371#endif
372
373 }
374 else
365 if (strcmp(method, "OPTIONS") == 0) { 375 if (strcmp(method, "OPTIONS") == 0) {
366 status = HTTP_STATUS_OK; 376 status = HTTP_STATUS_OK;
367 } 377 }
diff --git a/mastoapi.c b/mastoapi.c
index 8a54230..120c1aa 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -1150,106 +1150,120 @@ int process_auth_token(snac *snac, const xs_dict *req)
1150 return logged_in; 1150 return logged_in;
1151} 1151}
1152 1152
1153 1153void credentials_get(char **body, char **ctype, int *status, snac snac)
1154int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1155 char **body, int *b_size, char **ctype)
1156{ 1154{
1157 (void)b_size; 1155 xs *acct = xs_dict_new();
1156
1157 acct = xs_dict_append(acct, "id", snac.md5);
1158 acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
1159 acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
1160 acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name"));
1161 acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published"));
1162 acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac.config, "published"));
1163 acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
1164 acct = xs_dict_append(acct, "url", snac.actor);
1165 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
1166 acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
1158 1167
1159 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) 1168 xs *src = xs_json_loads("{\"privacy\":\"public\","
1160 return 0; 1169 "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
1170 /* some apps take the note from the source object */
1171 src = xs_dict_set(src, "note", xs_dict_get(snac.config, "bio"));
1172 src = xs_dict_set(src, "privacy", xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE ? "private" : "public");
1161 1173
1162 int status = HTTP_STATUS_NOT_FOUND; 1174 const xs_str *cw = xs_dict_get(snac.config, "cw");
1163 const xs_dict *args = xs_dict_get(req, "q_vars"); 1175 src = xs_dict_set(src, "sensitive",
1164 xs *cmd = xs_replace_n(q_path, "/api", "", 1); 1176 strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1165 1177
1166 snac snac1 = {0}; 1178 src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
1167 int logged_in = process_auth_token(&snac1, req);
1168 1179
1169 if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/ 1180 xs *avatar = NULL;
1170 if (logged_in) { 1181 const char *av = xs_dict_get(snac.config, "avatar");
1171 xs *acct = xs_dict_new();
1172
1173 acct = xs_dict_append(acct, "id", snac1.md5);
1174 acct = xs_dict_append(acct, "username", xs_dict_get(snac1.config, "uid"));
1175 acct = xs_dict_append(acct, "acct", xs_dict_get(snac1.config, "uid"));
1176 acct = xs_dict_append(acct, "display_name", xs_dict_get(snac1.config, "name"));
1177 acct = xs_dict_append(acct, "created_at", xs_dict_get(snac1.config, "published"));
1178 acct = xs_dict_append(acct, "last_status_at", xs_dict_get(snac1.config, "published"));
1179 acct = xs_dict_append(acct, "note", xs_dict_get(snac1.config, "bio"));
1180 acct = xs_dict_append(acct, "url", snac1.actor);
1181 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
1182 acct = xs_dict_append(acct, "bot", xs_dict_get(snac1.config, "bot"));
1183
1184 xs *src = xs_json_loads("{\"privacy\":\"public\","
1185 "\"sensitive\":false,\"fields\":[],\"note\":\"\"}");
1186 acct = xs_dict_append(acct, "source", src);
1187
1188 xs *avatar = NULL;
1189 const char *av = xs_dict_get(snac1.config, "avatar");
1190
1191 if (xs_is_null(av) || *av == '\0')
1192 avatar = xs_fmt("%s/susie.png", srv_baseurl);
1193 else
1194 avatar = xs_dup(av);
1195 1182
1196 acct = xs_dict_append(acct, "avatar", avatar); 1183 if (xs_is_null(av) || *av == '\0')
1197 acct = xs_dict_append(acct, "avatar_static", avatar); 1184 avatar = xs_fmt("%s/susie.png", srv_baseurl);
1185 else
1186 avatar = xs_dup(av);
1198 1187
1199 xs *header = NULL; 1188 acct = xs_dict_append(acct, "avatar", avatar);
1200 const char *hd = xs_dict_get(snac1.config, "header"); 1189 acct = xs_dict_append(acct, "avatar_static", avatar);
1201 1190
1202 if (!xs_is_null(hd)) 1191 xs *header = NULL;
1203 header = xs_dup(hd); 1192 const char *hd = xs_dict_get(snac.config, "header");
1204 else
1205 header = xs_fmt("%s/header.png", srv_baseurl);
1206 1193
1207 acct = xs_dict_append(acct, "header", header); 1194 if (!xs_is_null(hd))
1208 acct = xs_dict_append(acct, "header_static", header); 1195 header = xs_dup(hd);
1196 else
1197 header = xs_fmt("%s/header.png", srv_baseurl);
1209 1198
1210 const xs_dict *metadata = xs_dict_get(snac1.config, "metadata"); 1199 acct = xs_dict_append(acct, "header", header);
1211 if (xs_type(metadata) == XSTYPE_DICT) { 1200 acct = xs_dict_append(acct, "header_static", header);
1212 xs *fields = xs_list_new();
1213 const xs_str *k;
1214 const xs_str *v;
1215 1201
1216 xs_dict *val_links = snac1.links; 1202 const xs_dict *metadata = xs_dict_get(snac.config, "metadata");
1217 if (xs_is_null(val_links)) 1203 if (xs_type(metadata) == XSTYPE_DICT) {
1218 val_links = xs_stock(XSTYPE_DICT); 1204 xs *fields = xs_list_new();
1205 const xs_str *k;
1206 const xs_str *v;
1219 1207
1220 int c = 0; 1208 xs_dict *val_links = snac.links;
1221 while (xs_dict_next(metadata, &k, &v, &c)) { 1209 if (xs_is_null(val_links))
1222 xs *val_date = NULL; 1210 val_links = xs_stock(XSTYPE_DICT);
1223 1211
1224 const xs_number *verified_time = xs_dict_get(val_links, v); 1212 int c = 0;
1225 if (xs_type(verified_time) == XSTYPE_NUMBER) { 1213 while (xs_dict_next(metadata, &k, &v, &c)) {
1226 time_t t = xs_number_get(verified_time); 1214 xs *val_date = NULL;
1227 1215
1228 if (t > 0) 1216 const xs_number *verified_time = xs_dict_get(val_links, v);
1229 val_date = xs_str_utctime(t, ISO_DATE_SPEC); 1217 if (xs_type(verified_time) == XSTYPE_NUMBER) {
1230 } 1218 time_t t = xs_number_get(verified_time);
1231 1219
1232 xs *d = xs_dict_new(); 1220 if (t > 0)
1221 val_date = xs_str_utctime(t, ISO_DATE_SPEC);
1222 }
1233 1223
1234 d = xs_dict_append(d, "name", k); 1224 xs *d = xs_dict_new();
1235 d = xs_dict_append(d, "value", v);
1236 d = xs_dict_append(d, "verified_at",
1237 xs_type(val_date) == XSTYPE_STRING && *val_date ?
1238 val_date : xs_stock(XSTYPE_NULL));
1239 1225
1240 fields = xs_list_append(fields, d); 1226 d = xs_dict_append(d, "name", k);
1241 } 1227 d = xs_dict_append(d, "value", v);
1228 d = xs_dict_append(d, "verified_at",
1229 xs_type(val_date) == XSTYPE_STRING && *val_date ? val_date : xs_stock(XSTYPE_NULL));
1242 1230
1243 acct = xs_dict_set(acct, "fields", fields); 1231 fields = xs_list_append(fields, d);
1244 } 1232 }
1245 1233
1246 acct = xs_dict_append(acct, "followers_count", xs_stock(0)); 1234 acct = xs_dict_set(acct, "fields", fields);
1247 acct = xs_dict_append(acct, "following_count", xs_stock(0)); 1235 /* some apps take the fields from the source object */
1248 acct = xs_dict_append(acct, "statuses_count", xs_stock(0)); 1236 src = xs_dict_set(src, "fields", fields);
1237 }
1249 1238
1250 *body = xs_json_dumps(acct, 4); 1239 acct = xs_dict_append(acct, "source", src);
1251 *ctype = "application/json"; 1240 acct = xs_dict_append(acct, "followers_count", xs_stock(0));
1252 status = HTTP_STATUS_OK; 1241 acct = xs_dict_append(acct, "following_count", xs_stock(0));
1242 acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1243
1244 *body = xs_json_dumps(acct, 4);
1245 *ctype = "application/json";
1246 *status = HTTP_STATUS_OK;
1247}
1248
1249int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1250 char **body, int *b_size, char **ctype)
1251{
1252 (void)b_size;
1253
1254 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
1255 return 0;
1256
1257 int status = HTTP_STATUS_NOT_FOUND;
1258 const xs_dict *args = xs_dict_get(req, "q_vars");
1259 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
1260
1261 snac snac1 = {0};
1262 int logged_in = process_auth_token(&snac1, req);
1263
1264 if (strcmp(cmd, "/v1/accounts/verify_credentials") == 0) { /** **/
1265 if (logged_in) {
1266 credentials_get(body, ctype, &status, snac1);
1253 } 1267 }
1254 else { 1268 else {
1255 status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login) 1269 status = HTTP_STATUS_UNPROCESSABLE_CONTENT; // (no login)
@@ -3077,6 +3091,148 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
3077 return status; 3091 return status;
3078} 3092}
3079 3093
3094void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac)
3095/* Store header or avatar */
3096{
3097 if (data != NULL) {
3098 if (xs_type(data) == XSTYPE_LIST) {
3099 const char *fn = xs_list_get(data, 0);
3100
3101 if (fn && *fn) {
3102 const char *ext = strrchr(fn, '.');
3103 /* Mona iOS sends JPG file as application/octet-stream with filename "header"
3104 * Make sure we have a unique file name, otherwise updated images will not be
3105 * loaded by clients.
3106 */
3107 if (ext == NULL || strcmp(fn, key) == 0) {
3108 fn = random_str();
3109 ext = ".jpg";
3110 }
3111 xs *hash = xs_md5_hex(fn, strlen(fn));
3112 xs *id = xs_fmt("%s%s", hash, ext);
3113 xs *url = xs_fmt("%s/s/%s", snac->actor, id);
3114 int fo = xs_number_get(xs_list_get(data, 1));
3115 int fs = xs_number_get(xs_list_get(data, 2));
3116
3117 /* store */
3118 static_put(snac, id, payload + fo, fs);
3119
3120 snac->config = xs_dict_set(snac->config, key, url);
3121 }
3122 }
3123 }
3124}
3125
3126int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
3127 const char *payload, int p_size,
3128 char **body, int *b_size, char **ctype)
3129/* Handle profile updates */
3130{
3131 (void)p_size;
3132 (void)b_size;
3133
3134 if (!xs_startswith(q_path, "/api/v1/"))
3135 return 0;
3136
3137 int status = HTTP_STATUS_NOT_FOUND;
3138 xs *args = NULL;
3139 const char *i_ctype = xs_dict_get(req, "content-type");
3140
3141 if (i_ctype && xs_startswith(i_ctype, "application/json")) {
3142 if (!xs_is_null(payload))
3143 args = xs_json_loads(payload);
3144 }
3145 else
3146 args = xs_dup(xs_dict_get(req, "p_vars"));
3147
3148 if (args == NULL)
3149 return HTTP_STATUS_BAD_REQUEST;
3150
3151 xs *cmd = xs_replace_n(q_path, "/api", "", 1);
3152
3153 snac snac = {0};
3154 int logged_in = process_auth_token(&snac, req);
3155
3156 if (xs_startswith(cmd, "/v1/accounts/update_credentials")) {
3157 /* Update user profile fields */
3158 if (logged_in) {
3159 /*
3160 xs_str *dump = xs_json_dumps(args, 4);
3161 printf("%s\n\n", dump);
3162 */
3163 int c = 0;
3164 const xs_str *k;
3165 const xs_val *v;
3166 const xs_str *field_name = NULL;
3167 xs_dict *new_fields = xs_dict_new();
3168 while (xs_dict_next(args, &k, &v, &c)) {
3169 if (strcmp(k, "display_name") == 0) {
3170 if (v != NULL)
3171 snac.config = xs_dict_set(snac.config, "name", v);
3172 }
3173 else
3174 if (strcmp(k, "note") == 0) {
3175 if (v != NULL)
3176 snac.config = xs_dict_set(snac.config, "bio", v);
3177 }
3178 else
3179 if (strcmp(k, "bot") == 0) {
3180 if (v != NULL)
3181 snac.config = xs_dict_set(snac.config, "bot",
3182 strcmp(v, "true") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
3183 }
3184 else
3185 if (strcmp(k, "source[sensitive]") == 0) {
3186 if (v != NULL)
3187 snac.config = xs_dict_set(snac.config, "cw",
3188 strcmp(v, "true") == 0 ? "open" : "");
3189 }
3190 else
3191 if (strcmp(k, "source[privacy]") == 0) {
3192 if (v != NULL)
3193 snac.config = xs_dict_set(snac.config, "private",
3194 strcmp(v, "private") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
3195 }
3196 else
3197 if (strcmp(k, "header") == 0) {
3198 persist_image("header", v, payload, &snac);
3199 }
3200 else
3201 if (strcmp(k, "avatar") == 0) {
3202 persist_image("avatar", v, payload, &snac);
3203 }
3204 else
3205 if (xs_starts_and_ends("fields_attributes", k, "[name]")) {
3206 field_name = strcmp(v, "") != 0 ? v : NULL;
3207 }
3208 else
3209 if (xs_starts_and_ends("fields_attributes", k, "[value]")) {
3210 if (field_name != NULL) {
3211 new_fields = xs_dict_set(new_fields, field_name, v);
3212 snac.config = xs_dict_set(snac.config, "metadata", new_fields);
3213 }
3214 }
3215 }
3216
3217 /* Persist profile */
3218 if (user_persist(&snac) == 0)
3219 credentials_get(body, ctype, &status, snac);
3220 else
3221 status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
3222 }
3223 else
3224 status = HTTP_STATUS_UNAUTHORIZED;
3225 }
3226
3227 /* user cleanup */
3228 if (logged_in)
3229 user_free(&snac);
3230
3231 srv_debug(1, xs_fmt("mastoapi_patch_handler %s %d", q_path, status));
3232
3233 return status;
3234}
3235
3080 3236
3081void mastoapi_purge(void) 3237void mastoapi_purge(void)
3082{ 3238{
diff --git a/snac.h b/snac.h
index d79f3ea..5c2f731 100644
--- a/snac.h
+++ b/snac.h
@@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
76void user_free(snac *snac); 76void user_free(snac *snac);
77xs_list *user_list(void); 77xs_list *user_list(void);
78int user_open_by_md5(snac *snac, const char *md5); 78int user_open_by_md5(snac *snac, const char *md5);
79int user_persist(snac *snac);
79 80
80int validate_uid(const char *uid); 81int validate_uid(const char *uid);
81 82
@@ -358,6 +359,10 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
358int mastoapi_put_handler(const xs_dict *req, const char *q_path, 359int mastoapi_put_handler(const xs_dict *req, const char *q_path,
359 const char *payload, int p_size, 360 const char *payload, int p_size,
360 char **body, int *b_size, char **ctype); 361 char **body, int *b_size, char **ctype);
362void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac);
363int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
364 const char *payload, int p_size,
365 char **body, int *b_size, char **ctype);
361void mastoapi_purge(void); 366void mastoapi_purge(void);
362 367
363void verify_links(snac *user); 368void verify_links(snac *user);