diff options
| author | 2025-10-18 21:33:31 +0200 | |
|---|---|---|
| committer | 2025-10-18 21:33:31 +0200 | |
| commit | 2d433afc27afe46c9641cd0b0bd076566cfb67e2 (patch) | |
| tree | 826378411a16df79b16b5b351b7c56d20db3219f /mastoapi.c | |
| parent | FEDERATION.md: also document outgoing Webmention. (diff) | |
| download | snac2-2d433afc27afe46c9641cd0b0bd076566cfb67e2.tar.gz snac2-2d433afc27afe46c9641cd0b0bd076566cfb67e2.tar.xz snac2-2d433afc27afe46c9641cd0b0bd076566cfb67e2.zip | |
Enhances Mastodon API compatibility by adding support for displaying remote users' follower/following/post counts and their posts when viewing profiles in Mastodon-compatible apps (Fedilab, Tusky, etc.).
Fixes for Moshidon and improvements for better compatibility with HAProxy
Diffstat (limited to 'mastoapi.c')
| -rw-r--r-- | mastoapi.c | 339 |
1 files changed, 325 insertions, 14 deletions
| @@ -533,6 +533,117 @@ xs_str *mastoapi_id(const xs_dict *msg) | |||
| 533 | #define MID_TO_MD5(id) (id + 10) | 533 | #define MID_TO_MD5(id) (id + 10) |
| 534 | 534 | ||
| 535 | 535 | ||
| 536 | static xs_val *get_count_from_actor(const xs_dict *actor, const char *field_names[], const char *collection_url_field) | ||
| 537 | /* helper to extract count from actor dict using various field name variations or from cached collection */ | ||
| 538 | { | ||
| 539 | xs_val *count = NULL; | ||
| 540 | |||
| 541 | /* try direct field name variations first */ | ||
| 542 | for (int i = 0; field_names[i] && xs_type(count) != XSTYPE_NUMBER; i++) { | ||
| 543 | const xs_number *val = xs_dict_get(actor, field_names[i]); | ||
| 544 | if (xs_type(val) == XSTYPE_NUMBER) | ||
| 545 | count = xs_dup(val); | ||
| 546 | } | ||
| 547 | |||
| 548 | /* if not found directly, try to get from cached collection object */ | ||
| 549 | if (xs_type(count) != XSTYPE_NUMBER) { | ||
| 550 | const char *url = xs_dict_get(actor, collection_url_field); | ||
| 551 | if (!xs_is_null(url)) { | ||
| 552 | xs *coll = NULL; | ||
| 553 | if (valid_status(object_get(url, &coll))) { | ||
| 554 | const xs_number *total = xs_dict_get(coll, "totalItems"); | ||
| 555 | if (xs_type(total) == XSTYPE_NUMBER) | ||
| 556 | count = xs_dup(total); | ||
| 557 | } | ||
| 558 | } | ||
| 559 | } | ||
| 560 | |||
| 561 | return count; | ||
| 562 | } | ||
| 563 | |||
| 564 | |||
| 565 | static const xs_list *get_collection_items(snac *snac, const char *collection_url, | ||
| 566 | xs_dict **out_collection, xs_dict **out_page) | ||
| 567 | /* fetches items from an ActivityPub collection (outbox, etc) with minimal HTTP requests */ | ||
| 568 | { | ||
| 569 | const xs_list *items = NULL; | ||
| 570 | xs_dict *collection = NULL; | ||
| 571 | |||
| 572 | if (activitypub_request(snac, collection_url, &collection) == 200) { | ||
| 573 | /* check if items are directly embedded */ | ||
| 574 | items = xs_dict_get(collection, "orderedItems"); | ||
| 575 | if (xs_is_null(items)) | ||
| 576 | items = xs_dict_get(collection, "items"); | ||
| 577 | |||
| 578 | if (!xs_is_null(items)) { | ||
| 579 | /* items found in main collection - transfer ownership to keep items valid */ | ||
| 580 | if (out_collection) | ||
| 581 | *out_collection = collection; | ||
| 582 | return items; | ||
| 583 | } | ||
| 584 | |||
| 585 | /* if no items, try fetching first page (only 1 extra request) */ | ||
| 586 | const char *first_url = xs_dict_get(collection, "first"); | ||
| 587 | if (!xs_is_null(first_url)) { | ||
| 588 | xs_dict *first_page = NULL; | ||
| 589 | if (activitypub_request(snac, first_url, &first_page) == 200) { | ||
| 590 | items = xs_dict_get(first_page, "orderedItems"); | ||
| 591 | if (xs_is_null(items)) | ||
| 592 | items = xs_dict_get(first_page, "items"); | ||
| 593 | |||
| 594 | if (!xs_is_null(items)) { | ||
| 595 | /* items found in first page - transfer ownership to keep items valid */ | ||
| 596 | if (out_page) | ||
| 597 | *out_page = first_page; | ||
| 598 | xs_free(collection); | ||
| 599 | return items; | ||
| 600 | } | ||
| 601 | xs_free(first_page); | ||
| 602 | } | ||
| 603 | } | ||
| 604 | xs_free(collection); | ||
| 605 | } | ||
| 606 | |||
| 607 | return NULL; | ||
| 608 | } | ||
| 609 | |||
| 610 | |||
| 611 | static const xs_dict *extract_post_from_item(const xs_val *item) | ||
| 612 | /* extracts the post object from an outbox item, handling Create/Announce wrappers */ | ||
| 613 | { | ||
| 614 | if (xs_type(item) != XSTYPE_DICT) | ||
| 615 | return NULL; | ||
| 616 | |||
| 617 | const char *item_type = xs_dict_get(item, "type"); | ||
| 618 | const xs_dict *post = item; | ||
| 619 | |||
| 620 | /* if it's an activity, try to get embedded object */ | ||
| 621 | if (!xs_is_null(item_type) && | ||
| 622 | (strcmp(item_type, "Create") == 0 || strcmp(item_type, "Announce") == 0)) { | ||
| 623 | const xs_val *obj = xs_dict_get(item, "object"); | ||
| 624 | |||
| 625 | /* only use embedded objects, skip URL references (would need HTTP fetch) */ | ||
| 626 | if (!xs_is_null(obj) && xs_type(obj) == XSTYPE_DICT) | ||
| 627 | post = obj; | ||
| 628 | else | ||
| 629 | return NULL; | ||
| 630 | } | ||
| 631 | |||
| 632 | return post; | ||
| 633 | } | ||
| 634 | |||
| 635 | |||
| 636 | static int is_valid_post_type(const char *post_type) | ||
| 637 | /* checks if a type is a valid post type for timeline display */ | ||
| 638 | { | ||
| 639 | return !xs_is_null(post_type) && | ||
| 640 | (strcmp(post_type, "Note") == 0 || | ||
| 641 | strcmp(post_type, "Article") == 0 || | ||
| 642 | strcmp(post_type, "Question") == 0 || | ||
| 643 | strcmp(post_type, "Page") == 0); | ||
| 644 | } | ||
| 645 | |||
| 646 | |||
| 536 | xs_dict *mastoapi_account(snac *logged, const xs_dict *actor) | 647 | xs_dict *mastoapi_account(snac *logged, const xs_dict *actor) |
| 537 | /* converts an ActivityPub actor to a Mastodon account */ | 648 | /* converts an ActivityPub actor to a Mastodon account */ |
| 538 | { | 649 | { |
| @@ -661,9 +772,22 @@ xs_dict *mastoapi_account(snac *logged, const xs_dict *actor) | |||
| 661 | } | 772 | } |
| 662 | 773 | ||
| 663 | acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE)); | 774 | acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE)); |
| 664 | acct = xs_dict_append(acct, "followers_count", xs_stock(0)); | 775 | |
| 665 | acct = xs_dict_append(acct, "following_count", xs_stock(0)); | 776 | /* try to get counts from actor object if available (some servers include these) */ |
| 666 | acct = xs_dict_append(acct, "statuses_count", xs_stock(0)); | 777 | const char *fcount_fields[] = { "followersCount", "followers_count", NULL }; |
| 778 | const char *gcount_fields[] = { "followingCount", "following_count", NULL }; | ||
| 779 | const char *scount_fields[] = { "statusesCount", "statuses_count", "totalItems", NULL }; | ||
| 780 | |||
| 781 | xs *followers_count = get_count_from_actor(actor, fcount_fields, "followers"); | ||
| 782 | xs *following_count = get_count_from_actor(actor, gcount_fields, "following"); | ||
| 783 | xs *statuses_count = get_count_from_actor(actor, scount_fields, "outbox"); | ||
| 784 | |||
| 785 | acct = xs_dict_append(acct, "followers_count", | ||
| 786 | xs_type(followers_count) == XSTYPE_NUMBER ? followers_count : xs_stock(0)); | ||
| 787 | acct = xs_dict_append(acct, "following_count", | ||
| 788 | xs_type(following_count) == XSTYPE_NUMBER ? following_count : xs_stock(0)); | ||
| 789 | acct = xs_dict_append(acct, "statuses_count", | ||
| 790 | xs_type(statuses_count) == XSTYPE_NUMBER ? statuses_count : xs_stock(0)); | ||
| 667 | 791 | ||
| 668 | xs *fields = xs_list_new(); | 792 | xs *fields = xs_list_new(); |
| 669 | p = xs_dict_get(actor, "attachment"); | 793 | p = xs_dict_get(actor, "attachment"); |
| @@ -1679,6 +1803,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1679 | xs *out = NULL; | 1803 | xs *out = NULL; |
| 1680 | xs *actor = NULL; | 1804 | xs *actor = NULL; |
| 1681 | 1805 | ||
| 1806 | if (logged_in && strcmp(uid, "familiar_followers") == 0) { /** **/ | ||
| 1807 | /* familiar followers endpoint - return empty array */ | ||
| 1808 | out = xs_list_new(); | ||
| 1809 | } | ||
| 1810 | else | ||
| 1682 | if (logged_in && strcmp(uid, "search") == 0) { /** **/ | 1811 | if (logged_in && strcmp(uid, "search") == 0) { /** **/ |
| 1683 | /* search for accounts starting with q */ | 1812 | /* search for accounts starting with q */ |
| 1684 | const char *aq = xs_dict_get(args, "q"); | 1813 | const char *aq = xs_dict_get(args, "q"); |
| @@ -1764,26 +1893,59 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1764 | else | 1893 | else |
| 1765 | if (strcmp(opt, "statuses") == 0) { /** **/ | 1894 | if (strcmp(opt, "statuses") == 0) { /** **/ |
| 1766 | /* the public list of posts of a user */ | 1895 | /* the public list of posts of a user */ |
| 1896 | const char *limit_s = xs_dict_get(args, "limit"); | ||
| 1897 | const char *o_max_id = xs_dict_get(args, "max_id"); | ||
| 1898 | int limit = limit_s ? atoi(limit_s) : 20; | ||
| 1899 | xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL; | ||
| 1900 | |||
| 1901 | srv_debug(1, xs_fmt("account statuses: max_id=%s limit=%d", max_id ? max_id : "(null)", limit)); | ||
| 1902 | |||
| 1767 | xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL); | 1903 | xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL); |
| 1768 | xs_list *p = timeline; | 1904 | xs_list *p = timeline; |
| 1769 | const xs_str *v; | 1905 | const xs_str *v; |
| 1906 | xs_set seen; | ||
| 1907 | int cnt = 0; | ||
| 1908 | int skip_until_max = max_id != NULL; | ||
| 1770 | 1909 | ||
| 1771 | out = xs_list_new(); | 1910 | out = xs_list_new(); |
| 1911 | xs_set_init(&seen); | ||
| 1772 | 1912 | ||
| 1773 | while (xs_list_iter(&p, &v)) { | 1913 | while (xs_list_iter(&p, &v) && cnt < limit) { |
| 1774 | xs *msg = NULL; | 1914 | xs *msg = NULL; |
| 1775 | 1915 | ||
| 1776 | if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) { | 1916 | if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) { |
| 1917 | const char *msg_id = xs_dict_get(msg, "id"); | ||
| 1918 | |||
| 1777 | /* add only posts by the author */ | 1919 | /* add only posts by the author */ |
| 1778 | if (strcmp(xs_dict_get(msg, "type"), "Note") == 0 && | 1920 | if (!xs_is_null(msg_id) && |
| 1921 | strcmp(xs_dict_get(msg, "type"), "Note") == 0 && | ||
| 1779 | xs_startswith(xs_dict_get(msg, "id"), snac2.actor) && is_msg_public(msg)) { | 1922 | xs_startswith(xs_dict_get(msg, "id"), snac2.actor) && is_msg_public(msg)) { |
| 1780 | xs *st = mastoapi_status(&snac2, msg); | ||
| 1781 | 1923 | ||
| 1782 | if (st) | 1924 | /* if max_id is set, skip entries until we find it */ |
| 1783 | out = xs_list_append(out, st); | 1925 | if (skip_until_max) { |
| 1926 | xs *mid = mastoapi_id(msg); | ||
| 1927 | if (strcmp(mid, max_id) == 0) { | ||
| 1928 | skip_until_max = 0; | ||
| 1929 | srv_debug(2, xs_fmt("account statuses: found max_id, starting from next post")); | ||
| 1930 | } | ||
| 1931 | continue; | ||
| 1932 | } | ||
| 1933 | |||
| 1934 | /* deduplicate by message id */ | ||
| 1935 | if (xs_set_add(&seen, msg_id) == 1) { | ||
| 1936 | xs *st = mastoapi_status(&snac2, msg); | ||
| 1937 | |||
| 1938 | if (st) { | ||
| 1939 | out = xs_list_append(out, st); | ||
| 1940 | cnt++; | ||
| 1941 | } | ||
| 1942 | } | ||
| 1784 | } | 1943 | } |
| 1785 | } | 1944 | } |
| 1786 | } | 1945 | } |
| 1946 | |||
| 1947 | srv_debug(1, xs_fmt("account statuses: returning %d posts (requested %d)", cnt, limit)); | ||
| 1948 | xs_set_free(&seen); | ||
| 1787 | } | 1949 | } |
| 1788 | else | 1950 | else |
| 1789 | if (strcmp(opt, "featured_tags") == 0) { | 1951 | if (strcmp(opt, "featured_tags") == 0) { |
| @@ -1815,6 +1977,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1815 | if (strcmp(opt, "lists") == 0) { | 1977 | if (strcmp(opt, "lists") == 0) { |
| 1816 | out = mastoapi_account_lists(&snac1, uid); | 1978 | out = mastoapi_account_lists(&snac1, uid); |
| 1817 | } | 1979 | } |
| 1980 | else | ||
| 1981 | if (strcmp(opt, "familiar_followers") == 0) { | ||
| 1982 | /* familiar followers - not implemented, return empty array */ | ||
| 1983 | out = xs_list_new(); | ||
| 1984 | } | ||
| 1818 | 1985 | ||
| 1819 | user_free(&snac2); | 1986 | user_free(&snac2); |
| 1820 | } | 1987 | } |
| @@ -1827,8 +1994,95 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1827 | } | 1994 | } |
| 1828 | else | 1995 | else |
| 1829 | if (strcmp(opt, "statuses") == 0) { | 1996 | if (strcmp(opt, "statuses") == 0) { |
| 1830 | /* we don't serve statuses of others; return the empty list */ | 1997 | /* fetch statuses from remote outbox */ |
| 1831 | out = xs_list_new(); | 1998 | out = xs_list_new(); |
| 1999 | const char *outbox_url = xs_dict_get(actor, "outbox"); | ||
| 2000 | |||
| 2001 | if (!xs_is_null(outbox_url)) { | ||
| 2002 | /* extract query parameters */ | ||
| 2003 | const char *limit_s = xs_dict_get(args, "limit"); | ||
| 2004 | const char *exclude_replies_s = xs_dict_get(args, "exclude_replies"); | ||
| 2005 | const char *o_max_id = xs_dict_get(args, "max_id"); | ||
| 2006 | |||
| 2007 | int limit = 20; | ||
| 2008 | if (!xs_is_null(limit_s)) | ||
| 2009 | limit = atoi(limit_s); | ||
| 2010 | if (limit == 0 || limit > 40) | ||
| 2011 | limit = 20; | ||
| 2012 | |||
| 2013 | int exclude_replies = !xs_is_null(exclude_replies_s) && | ||
| 2014 | strcmp(exclude_replies_s, "true") == 0; | ||
| 2015 | |||
| 2016 | xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL; | ||
| 2017 | int skip_until_max = max_id != NULL; | ||
| 2018 | |||
| 2019 | srv_debug(1, xs_fmt("remote account statuses: fetching from %s (max_id=%s limit=%d)", | ||
| 2020 | outbox_url, max_id ? max_id : "(null)", limit)); | ||
| 2021 | |||
| 2022 | /* fetch first page only - safer for memory on large instances */ | ||
| 2023 | xs *outbox_collection = NULL; | ||
| 2024 | xs *first_page = NULL; | ||
| 2025 | const xs_list *items = get_collection_items(&snac1, outbox_url, | ||
| 2026 | &outbox_collection, &first_page); | ||
| 2027 | |||
| 2028 | int count = 0; | ||
| 2029 | int processed = 0; | ||
| 2030 | |||
| 2031 | if (!xs_is_null(items) && xs_type(items) == XSTYPE_LIST) { | ||
| 2032 | int total_items = xs_list_len(items); | ||
| 2033 | srv_debug(1, xs_fmt("remote account statuses: got %d items from outbox", total_items)); | ||
| 2034 | |||
| 2035 | const xs_val *item; | ||
| 2036 | |||
| 2037 | xs_list_foreach(items, item) { | ||
| 2038 | processed++; | ||
| 2039 | |||
| 2040 | if (count >= limit) | ||
| 2041 | break; | ||
| 2042 | |||
| 2043 | const xs_dict *post = extract_post_from_item(item); | ||
| 2044 | if (!post) | ||
| 2045 | continue; | ||
| 2046 | |||
| 2047 | const char *post_type = xs_dict_get(post, "type"); | ||
| 2048 | const char *in_reply_to = xs_dict_get(post, "inReplyTo"); | ||
| 2049 | |||
| 2050 | /* apply filters */ | ||
| 2051 | if (exclude_replies && !xs_is_null(in_reply_to)) | ||
| 2052 | continue; | ||
| 2053 | |||
| 2054 | if (is_valid_post_type(post_type)) { | ||
| 2055 | /* store object locally so mastoapi_id() can generate valid IDs */ | ||
| 2056 | const char *post_id = xs_dict_get(post, "id"); | ||
| 2057 | if (!xs_is_null(post_id)) | ||
| 2058 | object_add(post_id, post); | ||
| 2059 | |||
| 2060 | /* handle pagination with max_id */ | ||
| 2061 | if (skip_until_max) { | ||
| 2062 | xs *mid = mastoapi_id(post); | ||
| 2063 | if (!xs_is_null(mid) && strcmp(mid, max_id) == 0) { | ||
| 2064 | skip_until_max = 0; | ||
| 2065 | srv_debug(2, xs_fmt("remote account statuses: found max_id at position %d", processed)); | ||
| 2066 | } | ||
| 2067 | continue; | ||
| 2068 | } | ||
| 2069 | |||
| 2070 | /* pass logged-in user context to enable media proxying if configured */ | ||
| 2071 | xs *st = mastoapi_status(&snac1, post); | ||
| 2072 | if (st) { | ||
| 2073 | out = xs_list_append(out, st); | ||
| 2074 | count++; | ||
| 2075 | } | ||
| 2076 | } | ||
| 2077 | } | ||
| 2078 | } | ||
| 2079 | else { | ||
| 2080 | srv_debug(1, xs_fmt("remote account statuses: no items found in outbox")); | ||
| 2081 | } | ||
| 2082 | |||
| 2083 | srv_debug(1, xs_fmt("remote account statuses: processed %d items, returning %d posts (requested %d)", | ||
| 2084 | processed, count, limit)); | ||
| 2085 | } | ||
| 1832 | } | 2086 | } |
| 1833 | else | 2087 | else |
| 1834 | if (strcmp(opt, "featured_tags") == 0) { | 2088 | if (strcmp(opt, "featured_tags") == 0) { |
| @@ -1840,6 +2094,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1840 | if (strcmp(opt, "lists") == 0) { | 2094 | if (strcmp(opt, "lists") == 0) { |
| 1841 | out = mastoapi_account_lists(&snac1, uid); | 2095 | out = mastoapi_account_lists(&snac1, uid); |
| 1842 | } | 2096 | } |
| 2097 | else | ||
| 2098 | if (strcmp(opt, "familiar_followers") == 0) { | ||
| 2099 | /* familiar followers - not implemented, return empty array */ | ||
| 2100 | out = xs_list_new(); | ||
| 2101 | } | ||
| 1843 | } | 2102 | } |
| 1844 | } | 2103 | } |
| 1845 | 2104 | ||
| @@ -2730,8 +2989,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2730 | const char *i_ctype = xs_dict_get(req, "content-type"); | 2989 | const char *i_ctype = xs_dict_get(req, "content-type"); |
| 2731 | 2990 | ||
| 2732 | if (i_ctype && xs_startswith(i_ctype, "application/json")) { | 2991 | if (i_ctype && xs_startswith(i_ctype, "application/json")) { |
| 2733 | if (!xs_is_null(payload)) | 2992 | if (!xs_is_null(payload)) { |
| 2734 | args = xs_json_loads(payload); | 2993 | args = xs_json_loads(payload); |
| 2994 | } | ||
| 2735 | } | 2995 | } |
| 2736 | else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded")) | 2996 | else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded")) |
| 2737 | { | 2997 | { |
| @@ -2740,11 +3000,24 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2740 | args = xs_url_vars(payload); | 3000 | args = xs_url_vars(payload); |
| 2741 | } | 3001 | } |
| 2742 | } | 3002 | } |
| 2743 | else | 3003 | else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data")) |
| 3004 | { | ||
| 3005 | // Handle multipart/form-data by using p_vars (already parsed by httpd) | ||
| 3006 | args = xs_dup(xs_dict_get(req, "p_vars")); | ||
| 3007 | } | ||
| 3008 | |||
| 3009 | /* if args still NULL, try falling back to p_vars or q_vars */ | ||
| 3010 | if (args == NULL) | ||
| 2744 | args = xs_dup(xs_dict_get(req, "p_vars")); | 3011 | args = xs_dup(xs_dict_get(req, "p_vars")); |
| 2745 | 3012 | ||
| 2746 | if (args == NULL) | 3013 | if (args == NULL) |
| 3014 | args = xs_dup(xs_dict_get(req, "q_vars")); | ||
| 3015 | |||
| 3016 | if (args == NULL) { | ||
| 3017 | srv_debug(1, xs_fmt("mastoapi_post_handler: failed to parse args for %s, content-type: %s", | ||
| 3018 | q_path, i_ctype ? i_ctype : "(null)")); | ||
| 2747 | return HTTP_STATUS_BAD_REQUEST; | 3019 | return HTTP_STATUS_BAD_REQUEST; |
| 3020 | } | ||
| 2748 | 3021 | ||
| 2749 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); | 3022 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 2750 | 3023 | ||
| @@ -2926,7 +3199,12 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2926 | /* skip the 'fake' part of the id */ | 3199 | /* skip the 'fake' part of the id */ |
| 2927 | mid = MID_TO_MD5(mid); | 3200 | mid = MID_TO_MD5(mid); |
| 2928 | 3201 | ||
| 2929 | if (valid_status(timeline_get_by_md5(&snac, mid, &msg))) { | 3202 | /* try timeline first, then global object store for remote posts */ |
| 3203 | int found = valid_status(timeline_get_by_md5(&snac, mid, &msg)); | ||
| 3204 | if (!found) | ||
| 3205 | found = valid_status(object_get_by_md5(mid, &msg)); | ||
| 3206 | |||
| 3207 | if (found) { | ||
| 2930 | const char *id = xs_dict_get(msg, "id"); | 3208 | const char *id = xs_dict_get(msg, "id"); |
| 2931 | 3209 | ||
| 2932 | if (op == NULL) { | 3210 | if (op == NULL) { |
| @@ -3599,11 +3877,31 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path, | |||
| 3599 | if (!xs_is_null(payload)) | 3877 | if (!xs_is_null(payload)) |
| 3600 | args = xs_json_loads(payload); | 3878 | args = xs_json_loads(payload); |
| 3601 | } | 3879 | } |
| 3602 | else | 3880 | else if (i_ctype && xs_startswith(i_ctype, "application/x-www-form-urlencoded")) |
| 3881 | { | ||
| 3882 | // Some apps send form data instead of json so we should cater for those | ||
| 3883 | if (!xs_is_null(payload)) { | ||
| 3884 | args = xs_url_vars(payload); | ||
| 3885 | } | ||
| 3886 | } | ||
| 3887 | else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data")) | ||
| 3888 | { | ||
| 3889 | // Handle multipart/form-data by using p_vars (already parsed by httpd) | ||
| 3603 | args = xs_dup(xs_dict_get(req, "p_vars")); | 3890 | args = xs_dup(xs_dict_get(req, "p_vars")); |
| 3891 | } | ||
| 3604 | 3892 | ||
| 3893 | /* if args still NULL, try falling back to p_vars or q_vars */ | ||
| 3605 | if (args == NULL) | 3894 | if (args == NULL) |
| 3895 | args = xs_dup(xs_dict_get(req, "p_vars")); | ||
| 3896 | |||
| 3897 | if (args == NULL) | ||
| 3898 | args = xs_dup(xs_dict_get(req, "q_vars")); | ||
| 3899 | |||
| 3900 | if (args == NULL) { | ||
| 3901 | srv_debug(1, xs_fmt("mastoapi_put_handler: failed to parse args for %s, content-type: %s", | ||
| 3902 | q_path, i_ctype ? i_ctype : "(null)")); | ||
| 3606 | return HTTP_STATUS_BAD_REQUEST; | 3903 | return HTTP_STATUS_BAD_REQUEST; |
| 3904 | } | ||
| 3607 | 3905 | ||
| 3608 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); | 3906 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 3609 | 3907 | ||
| @@ -3753,11 +4051,24 @@ int mastoapi_patch_handler(const xs_dict *req, const char *q_path, | |||
| 3753 | args = xs_url_vars(payload); | 4051 | args = xs_url_vars(payload); |
| 3754 | } | 4052 | } |
| 3755 | } | 4053 | } |
| 3756 | else | 4054 | else if (i_ctype && xs_startswith(i_ctype, "multipart/form-data")) |
| 4055 | { | ||
| 4056 | // Handle multipart/form-data by using p_vars (already parsed by httpd) | ||
| 4057 | args = xs_dup(xs_dict_get(req, "p_vars")); | ||
| 4058 | } | ||
| 4059 | |||
| 4060 | /* if args still NULL, try falling back to p_vars or q_vars */ | ||
| 4061 | if (args == NULL) | ||
| 3757 | args = xs_dup(xs_dict_get(req, "p_vars")); | 4062 | args = xs_dup(xs_dict_get(req, "p_vars")); |
| 3758 | 4063 | ||
| 3759 | if (args == NULL) | 4064 | if (args == NULL) |
| 4065 | args = xs_dup(xs_dict_get(req, "q_vars")); | ||
| 4066 | |||
| 4067 | if (args == NULL) { | ||
| 4068 | srv_debug(1, xs_fmt("mastoapi_patch_handler: failed to parse args for %s, content-type: %s", | ||
| 4069 | q_path, i_ctype ? i_ctype : "(null)")); | ||
| 3760 | return HTTP_STATUS_BAD_REQUEST; | 4070 | return HTTP_STATUS_BAD_REQUEST; |
| 4071 | } | ||
| 3761 | 4072 | ||
| 3762 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); | 4073 | xs *cmd = xs_replace_n(q_path, "/api", "", 1); |
| 3763 | 4074 | ||