HOSTAS_CONTEXT, 'id' => $actor['id'], 'type' => 'Person', 'preferredUsername' => $actor['preferredUsername'], 'name' => $actor['name'], 'summary' => $actor['summary'], 'url' => $actor['url'], 'icon' => array( 'type' => 'Image', 'url' => $actor['icon'], ), 'publicKey' => array( 'id' => "{$actor['id']}#main-key", 'owner' => $actor['id'], 'publicKeyPem' => $public_key, ), 'inbox' => "https://" . HOSTAS_DOMAIN . "/api/v1/actor/$preferred_username/inbox", 'outbox' => "https://" . HOSTAS_DOMAIN . "/api/v1/actor/$preferred_username/outbox", ); header('Content-Type: application/activity+json'); echo json_encode($activity_representation); $conn->close(); die(); } /** /api/v1/actor/:preferred_username/outbox?before=:before_date_iso&limit=:limit * Returns an OrderedCollectionPage of at most `limit` pages published before * `before_date_iso`. * * Parameters: * before ISO timestamp string. Only posts published before this time are included * limit Integer. Number of posts to include in the result. Default: 20. Max: HOSTAS_MAX_POSTS_PER_PAGE */ function get_actor_outbox_paged(string $preferred_username) { header('Content-Type: application/activity+json'); $before = array_key_exists('before', $_GET) ? $_GET['before'] : date(DATE_ISO8601, time()); $after = array_key_exists('after', $_GET) ? $_GET['after'] : date(DATE_ISO8601, 0); $limit = array_key_exists('limit', $_GET) ? $_GET['limit'] : 20; // Parameters in $_GET with a + character will get replaced with whitespace, // so we've got to change it back $before = str_replace(" ", "+", $before); $after = str_replace(" ", "+", $after); // Cap the limit at the configured maximum if ($limit > HOSTAS_MAX_POSTS_PER_PAGE) { $limit = HOSTAS_MAX_POSTS_PER_PAGE; } $conn = new SQLite3(HOSTAS_DATABASE_PATH); $actor = get_actor_or_exit($conn, $preferred_username); $sql_query = <<<'EOT' select object.id as object_id, object.type as object_type, activity.actor as activity_actor, post.id as post_id, post.published as post_published, post.type as post_type, post.content as post_content, post.published as post_published from object join activity on activity.objectId = object.id join object as post on activity.object = post.id where activity_actor = :actor_id and post_published < :before and post_published > :after order by object.published desc limit :limit EOT; $create_activities_result = prepare_and_execute($conn, $sql_query, array( ':actor_id' => $actor['id'], ':before' => strtotime($before), ':after' => strtotime($after), ':limit' => $limit ) ); $total_items = 0; $ordered_items = array(); while ($entry = $create_activities_result->fetchArray()) { $total_items += 1; array_push($ordered_items, array( 'id' => $entry['object_id'], 'type' => $entry['object_type'], 'actor' => $entry['activity_actor'], 'published' => date(DATE_ISO8601, $entry['post_published']), 'cc' => array('https://www.w3.org/ns/activitystreams#Public'), 'object' => array( 'id' => $entry['post_id'], 'type'=> $entry['post_type'], 'published' => date(DATE_ISO8601, $entry['post_published']), 'url' => $entry['post_id'], 'attributedTo' => $actor['id'], 'cc' => array('https://www.w3.org/ns/activitystreams#Public'), 'content' => $entry['post_content'], ), )); } $clean_uri = 'https://' . HOSTAS_DOMAIN . strtok($_SERVER['REQUEST_URI'], '?'); $response = array( '@context' => HOSTAS_CONTEXT, 'id' => 'https://' . HOSTAS_DOMAIN . $_SERVER['REQUEST_URI'], 'type' => 'OrderedCollectionPage', 'partOf' => 'https://' . HOSTAS_DOMAIN . $clean_uri, 'orderedItems' => $ordered_items, ); if ($total_items === 0) { // There may be previous items, so we can reuse the before param. We // need to do some pre-processing to avoid characters getting escaped // from the URL $latest_time = str_replace(" ", "+", $before); $response['prev'] = $clean_uri . "?after=$latest_time"; } else { $earliest_time = $ordered_items[array_key_last($ordered_items)]['published']; $latest_time = $ordered_items[0]['published']; $response['prev'] = $clean_uri . "?after=$latest_time"; $response['next'] = $clean_uri . "?before=$earliest_time"; } echo json_encode($response); die(); } function get_actor_outbox(string $preferred_username) { header('Content-Type: application/activity+json'); $conn = new SQLite3(HOSTAS_DATABASE_PATH); $actor = get_actor_or_exit($conn, $preferred_username); $sql_query = <<<'EOT' select count(post.id) as post_count, min(post.published) from object join activity on activity.objectId = object.id join object as post on activity.object = post.id where activity.actor = :actor_id EOT; $items_count_result = prepare_and_execute($conn, $sql_query, array(':actor_id' => $actor['id']) ); $items_count_row = $items_count_result->fetchArray(SQLITE3_ASSOC); $total_items = $items_count_row['post_count']; $collection_id = "https://" . HOSTAS_DOMAIN . "/api/v1/actor/$preferred_username/outbox"; echo json_encode(array( '@context' => HOSTAS_CONTEXT, 'id' => $collection_id, 'type' => 'OrderedCollection', 'totalItems' => $total_items, 'first' => $collection_id . '?before=' . date(DATE_ISO8601, time()), 'last' => $collection_id . '?after=' . date(DATE_ISO8601, 0), )); die(); } function get_actor_inbox(string $objectId) { die(); } switch ($_SERVER['REQUEST_METHOD']) { case 'GET': if (sizeof(REQUEST_PATH) === 3) { if (REQUEST_PATH[2] === 'inbox') get_actor_inbox(REQUEST_PATH[1]); else if (REQUEST_PATH[2] === 'outbox') { if (array_key_exists('before', $_GET) || array_key_exists('after', $_GET)) { get_actor_outbox_paged(REQUEST_PATH[1]); } else { get_actor_outbox(REQUEST_PATH[1]); } } } get_actor(REQUEST_PATH[1]); break; default: http_response_code(405); die(); }