From d158fed722ac53c170af0d0392129749bba774fe Mon Sep 17 00:00:00 2001 From: teknomunk Date: Fri, 14 Jul 2023 17:33:25 -0500 Subject: [PATCH] Implement featured/pinned posts --- src/controller/api/status.c | 30 +++++++++ src/controller/api/timeline.c | 12 ++++ src/controller/owner.c | 111 ++++++++++++++-------------------- src/model/account.c | 57 +++++++++++++++++ src/model/account.h | 4 ++ src/model/account/ap_data.c | 75 +++++++++++++++++++---- src/view/api/Status.c | 7 ++- 7 files changed, 216 insertions(+), 80 deletions(-) diff --git a/src/controller/api/status.c b/src/controller/api/status.c index d2ef25f..b7cd0ce 100644 --- a/src/controller/api/status.c +++ b/src/controller/api/status.c @@ -281,6 +281,32 @@ bool handle_favorite( struct http_request* req, struct status* s ) return true; } +// route: GET /api/v1/statuses/%d{id}/pin +bool handle_pin( struct http_request* req, struct status* s ) +{ + struct account* owner = account_from_id( owner_account_id ); + account_pin_status( owner, s ); + status_save(s); + show_status( req, s ); + + account_free(owner); + + return true; +} +// route: GET /api/v1/statuses/%d{id}/unpin +bool handle_unpin( struct http_request* req, struct status* s ) +{ + struct account* owner = account_from_id( owner_account_id ); + account_unpin_status( owner, s ); + status_save(s); + show_status( req, s ); + + account_free(owner); + + return true; +} + + bool http_request_route_id( struct http_request* req, int* id ) { char* id_str = http_request_route_get_dir_or_file(req); @@ -338,6 +364,10 @@ bool route_statuses( struct http_request* req ) if( handle_unbookmark( req, s ) ) { goto success; } } else if( http_request_route_term( req, "favourite" ) ) { if( handle_favorite( req, s ) ) { goto success; } + } else if( http_request_route_term( req, "pin" ) ) { + if( handle_pin( req, s ) ) { goto success; } + } else if( http_request_route_term( req, "unpin" ) ) { + if( handle_unpin( req, s ) ) { goto success; } } goto failed; diff --git a/src/controller/api/timeline.c b/src/controller/api/timeline.c index 991a3f3..8bde667 100644 --- a/src/controller/api/timeline.c +++ b/src/controller/api/timeline.c @@ -7,6 +7,7 @@ // Model #include "model/timeline.h" #include "model/status.h" +#include "model/account.h" // Controller #include "controller/api/status.h" @@ -15,6 +16,8 @@ static bool handle_timeline_internal( struct http_request* req, struct timeline* tl ) { + struct timeline* pinned_timeline = NULL; + if( !tl ) { http_request_send_headers( req, 200, "application/json", true ); FILE* f = http_request_get_response_body( req ); @@ -66,6 +69,14 @@ static bool handle_timeline_internal( struct http_request* req, struct timeline* .count = 0 }; + if( params.pinned ) { + char pinned_timeline_path[512]; + snprintf( pinned_timeline_path,512, "%s/pinned", tl->path ); + tl = pinned_timeline = timeline_from_path( pinned_timeline_path ); + } + + printf( "timeline path = %s\n", tl->path ); + int offset = 0; int count = 0; int max_loops = 5; @@ -103,6 +114,7 @@ load_statuses: done: show_statuses( req, show.items, show.count ); + timeline_free( pinned_timeline ); for( int i = 0; i < show.count; ++i ) { status_free( show.items[i] ); diff --git a/src/controller/owner.c b/src/controller/owner.c index f7a4bc5..deb3c22 100644 --- a/src/controller/owner.c +++ b/src/controller/owner.c @@ -13,26 +13,17 @@ #include #include -static bool handle_featured( struct http_request* req ) +struct pager { - struct account* owner_account = account_from_id(0); - - http_request_add_header( req, "Access-Control-Allow-Origin", "*" ); - http_request_send_headers( req, 200, "application/activity+json", true ); - FILE* f = http_request_get_response_body(req); - #include "src/view/owner/featured.json.inc" + void* data; + struct ap_object* (*get_root)( void* ); + struct ap_object* (*get_page)( void*, int ); +}; - account_free( owner_account ); - - return true; -} -static bool handle_followers( struct http_request* req ) +static bool handle_paged_collection( struct http_request* req, struct pager* pg ) { - bool result = false; - struct account* owner_account = account_from_id(0); - if( http_request_route_term(req,"") ) { - struct ap_object* obj = account_ap_followers(owner_account); + struct ap_object* obj = pg->get_root( pg->data ); http_request_add_header( req, "Access-Control-Allow-Origin", "*" ); http_request_send_headers( req, 200, "application/activity+json", true ); @@ -43,12 +34,12 @@ static bool handle_followers( struct http_request* req ) printf( "/page-\n" ); char* page_str = http_request_route_get_dir_or_file(req); int page = -1; - if( !page_str ) { goto failed; } - if( 1 != sscanf(page_str,"%d",&page) ) { goto failed; } - if( page < 0 ) { goto failed; } + if( !page_str ) { return false; } + if( 1 != sscanf(page_str,"%d",&page) ) { return false; } + if( page < 0 ) { return false; } - struct ap_object* obj = account_ap_followers_page(owner_account,page); - if( !obj ) { goto failed; } + struct ap_object* obj = pg->get_page( pg->data, page ); + if( !obj ) { return false; } http_request_add_header( req, "Access-Control-Allow-Origin", "*" ); http_request_send_headers( req, 200, "application/activity+json", true ); @@ -56,63 +47,49 @@ static bool handle_followers( struct http_request* req ) ap_object_write_to_FILE( obj, f ); ap_object_free(obj); } - goto success; + return true; +} -cleanup: - account_free( owner_account ); +static bool handle_featured( struct http_request* req ) +{ + struct account* owner_account = account_from_id(0); + + struct pager pg = { + .data = owner_account, + .get_root = (void*)account_ap_featured, + .get_page = (void*)account_ap_featured_page, + }; + bool result = handle_paged_collection(req,&pg); + account_free( owner_account ); return result; -success: - result = true; - goto cleanup; -failed: - printf( "! failed\n" ); - result = false; - goto cleanup; } -static bool handle_following( struct http_request* req ) +static bool handle_followers( struct http_request* req ) { - printf( "handle_following\n" ); - bool result = false; struct account* owner_account = account_from_id(0); - if( http_request_route_term(req,"") ) { - struct ap_object* obj = account_ap_following(owner_account); + struct pager pg = { + .data = owner_account, + .get_root = (void*)account_ap_followers, + .get_page = (void*)account_ap_followers_page, + }; - http_request_add_header( req, "Access-Control-Allow-Origin", "*" ); - http_request_send_headers( req, 200, "application/activity+json", true ); - FILE* f = http_request_get_response_body(req); - ap_object_write_to_FILE( obj, f ); - ap_object_free(obj); - } else if( http_request_route( req, "/page-" ) ) { - printf( "/page-\n" ); - char* page_str = http_request_route_get_dir_or_file(req); - int page = -1; - if( !page_str ) { goto failed; } - if( 1 != sscanf(page_str,"%d",&page) ) { goto failed; } - if( page < 0 ) { goto failed; } - - struct ap_object* obj = account_ap_following_page(owner_account,page); - if( !obj ) { goto failed; } + bool result = handle_paged_collection(req,&pg); + account_free( owner_account ); + return result; +} +static bool handle_following( struct http_request* req ) +{ + struct account* owner_account = account_from_id(0); - http_request_add_header( req, "Access-Control-Allow-Origin", "*" ); - http_request_send_headers( req, 200, "application/activity+json", true ); - FILE* f = http_request_get_response_body(req); - ap_object_write_to_FILE( obj, f ); - ap_object_free(obj); - } + struct pager pg = { + .data = owner_account, + .get_root = (void*)account_ap_following, + .get_page = (void*)account_ap_following_page, + }; - goto success; -success: - result = true; - goto cleanup; -failed: - printf( "! failed\n" ); - result = false; - goto cleanup; -cleanup: + bool result = handle_paged_collection(req,&pg); account_free( owner_account ); - return result; } diff --git a/src/model/account.c b/src/model/account.c index c57b051..879132c 100644 --- a/src/model/account.c +++ b/src/model/account.c @@ -18,6 +18,8 @@ #include "model/notification.h" #include "model/fetch.h" #include "model/webfinger.h" +#include "model/timeline.h" +#include "model/activity.h" // View #include "view/api/Relationship.h" @@ -321,6 +323,7 @@ static void create_account_skeleton( int account_id ) // Make sure the account directory exists mkdir( format( b, 512, "data/accounts/%d", account_id ), 0755 ); mkdir( format( b, 512, "data/accounts/%d/timeline", account_id ), 0755 ); + mkdir( format( b, 512, "data/accounts/%d/timeline/pinned", account_id ), 0755 ); fs_list_set( format( b, 512, "data/accounts/%d/timeline/HEAD", account_id ), 0 ); } @@ -607,4 +610,58 @@ bool account_does_follow( struct account* a, int account_id ) return result; } +void account_pin_status( struct account* a, struct status* s ) +{ + char buffer[512]; + snprintf( buffer,sizeof(buffer), "data/accounts/%d/timeline/pinned", a->id ); + s->pinned = true; + + struct timeline* pinned = timeline_from_path( buffer ); + timeline_add_post( pinned, s ); + timeline_free(pinned); + + // TODO: federate an Add(Note) activity + struct ap_object* act = ap_object_new(); + activity_allocate_local_id(act); + act->type = ap_Add; + act->object.tag = apaot_ref; + act->object.ref = strdup( s->url ); + act->actor = strdup( a->account_url ); + act->target = aformat( "https://%s/owner/collections/featured", g_server->domain ); + + char* str; + str = aformat("https://%s/owner/followers", g_server->domain ); + array_append( &act->to, sizeof(str), &str ); + + ap_object_write_to_FILE( act, stdout ); + activity_deliver( act ); + ap_object_free(act); +} +void account_unpin_status( struct account* a, struct status* s ) +{ + char buffer[512]; + snprintf( buffer,sizeof(buffer), "data/accounts/%d/timeline/pinned", a->id ); + s->pinned = false; + + struct timeline* pinned = timeline_from_path( buffer ); + timeline_remove_post( pinned, s ); + timeline_free(pinned); + + // TODO: federate an Add(Note) activity + struct ap_object* act = ap_object_new(); + activity_allocate_local_id(act); + act->type = ap_Remove; + act->object.tag = apaot_ref; + act->object.ref = strdup( s->url ); + act->actor = strdup( a->account_url ); + act->target = aformat( "https://%s/owner/collections/featured", g_server->domain ); + + char* str; + str = aformat("https://%s/owner/followers", g_server->domain ); + array_append( &act->to, sizeof(str), &str ); + + ap_object_write_to_FILE( act, stdout ); + activity_deliver( act ); + ap_object_free(act); +} diff --git a/src/model/account.h b/src/model/account.h index 4e9e62e..af2683e 100644 --- a/src/model/account.h +++ b/src/model/account.h @@ -126,6 +126,8 @@ struct ap_object* account_ap_followers( struct account* a ); struct ap_object* account_ap_followers_page( struct account* a, int page ); struct ap_object* account_ap_following( struct account* a ); struct ap_object* account_ap_following_page( struct account* a, int page ); +struct ap_object* account_ap_featured( struct account* a ); +struct ap_object* account_ap_featured_page( struct account* a, int page ); // Local actions void account_add_follower( struct account* a, struct account* follower ); @@ -141,4 +143,6 @@ struct status* account_announce( struct account* a, struct status* original_post void account_follow( struct account* a, struct account* to_follow ); void account_unfollow( struct account* a, struct account* to_unfollow ); void account_update( struct account* a ); +void account_pin_status( struct account* a, struct status* s ); +void account_unpin_status( struct account* a, struct status* s ); diff --git a/src/model/account/ap_data.c b/src/model/account/ap_data.c index f3c3619..e900fc5 100644 --- a/src/model/account/ap_data.c +++ b/src/model/account/ap_data.c @@ -10,6 +10,7 @@ #include "model/server.h" #include "model/status.h" #include "model/activity.h" +#include "model/timeline.h" // Standard Library #include @@ -192,6 +193,17 @@ static struct ap_object* account_list_page( int page, char* part_of, const char* if( page >= page_count ) { return NULL; } struct ap_object* o = activity_new_local_activity(); + o->type = ap_OrderedCollectionPage; + o->published = time(NULL); + o->part_of = part_of; + o->id = aformat( page_format, page ); + if( page > 0 ) { + o->prev = aformat( page_format, page - 1 ); + } + if( page < page_count - 1 ) { + o->next.tag = apaot_ref; + o->next.ref = aformat( page_format, page + 1 ); + } struct { char** items; @@ -216,18 +228,6 @@ static struct ap_object* account_list_page( int page, char* part_of, const char* } free( values.items ); - o->type = ap_OrderedCollectionPage; - o->published = time(NULL); - o->part_of = part_of; - o->id = aformat( page_format, page ); - if( page > 0 ) { - o->prev = aformat( page_format, page - 1 ); - } - if( page < page_count - 1 ) { - o->next.tag = apaot_ref; - o->next.ref = aformat( page_format, page + 1 ); - } - return o; } @@ -293,3 +293,54 @@ struct ap_object* account_ap_following_page( struct account* a, int page ) return account_list_page( page, part_of, page_format, trie_filename ); } +struct ap_object* account_ap_featured( struct account* a ) +{ + struct ap_object* o = activity_new_local_activity(); + o->type = ap_OrderedCollection; + o->published = time(NULL); + o->part_of = aformat( "https://%s/owner/collections/featured", g_server->domain ); + o->id = strdup(o->part_of); + o->first.tag = apaot_object; + o->first.ptr = account_ap_featured_page( a, 0 ); + char buffer[512]; + o->total_items = ffdb_trie_count( format( buffer,512, "data/accounts/%d/timeline/pinned", a->id ) ); + return o; +} +struct ap_object* account_ap_featured_page( struct account* a, int page ) +{ + char buffer[512]; + snprintf( buffer,512, "data/accounts/%d/timeline/pinned", a->id ); + struct timeline* tl = timeline_from_path( buffer ); + if( !tl ) { return NULL; } + + enum { items_per_page = 20 }; + + struct status* statuses[items_per_page]; + int count = timeline_load_statuses( tl, page * items_per_page, items_per_page, statuses ); + if( count == 0 ) { + timeline_free(tl); + return NULL; + } + + struct ap_object* o = activity_new_local_activity(); + o->type = ap_OrderedCollectionPage; + o->part_of = aformat( "https://%s/owner/collections/featured", g_server->domain ); + o->id = aformat( "%s/page-%d", o->part_of, page); + o->next.tag = apaot_ref; + o->next.ref = aformat( "%s/page-%d", o->part_of, page+1 ); + if( page > 0 ) { + o->prev = aformat( "%s/page-%d", o->part_of, page-11 ); + } + + for( int i = 0; i < count; ++i ) { + struct ap_object_ptr_or_ref r; + r.tag = apaot_ref; + r.ref = strdup(statuses[i]->url); + array_append( &o->collection_items, sizeof(r), &r ); + status_free(statuses[i]); + } + + timeline_free(tl); + return o; +} + diff --git a/src/view/api/Status.c b/src/view/api/Status.c index 1b59a20..9e235df 100644 --- a/src/view/api/Status.c +++ b/src/view/api/Status.c @@ -506,7 +506,12 @@ struct json_object_field api_Status_layout[] = { .array_item_type = &Mention_type, }, JSON_FIELD_FIXED_BOOL( muted, false ), - JSON_FIELD_FIXED_BOOL( pinned, false ), + { + .key = "pinned", + .offset = offsetof( OBJ_TYPE, pinned ), + .type = &json_field_bool, + .allow_drop_empty = true, + }, { .key = "pleroma", .offset = 0,