diff --git a/backend/src/modules/crossroads/activitypub/activityPub.controller.ts b/backend/src/modules/crossroads/activitypub/activityPub.controller.ts index 62bdaa93..7c72e429 100644 --- a/backend/src/modules/crossroads/activitypub/activityPub.controller.ts +++ b/backend/src/modules/crossroads/activitypub/activityPub.controller.ts @@ -13,6 +13,7 @@ import type { ActivityPubActorObject } from '@/modules/crossroads/activitypub/ac import { crossroadsBasePath } from '@/config/apiPaths'; import { contentTypeActivityStreams } from '@/modules/crossroads/activitypub/utils/contentType'; import { OutboxDto } from '@/modules/crossroads/activitypub/dto/outbox.dto'; +import { FollowerDto } from '@/modules/crossroads/activitypub/dto/followers.dto'; @Controller({ path: crossroadsBasePath }) export class ActivityPubController { @@ -59,6 +60,24 @@ export class ActivityPubController { return outbox; } + @Get('/followers') + @Header('content-type', contentTypeActivityStreams) + async getFollowers(): Promise { + // TODO pagination + const followers = await this.activityPubService.getFollowersCollection(); + + return followers; + } + + @Get('/following') + @Header('content-type', contentTypeActivityStreams) + async getFollowing(): Promise { + // TODO pagination + const following = await this.activityPubService.getFollowingCollection(); + + return following; + } + @Post('/inbox') @Header('content-type', contentTypeActivityStreams) async postToInbox(@Body() body: unknown): Promise { diff --git a/backend/src/modules/crossroads/activitypub/activityPub.service.ts b/backend/src/modules/crossroads/activitypub/activityPub.service.ts index d724294c..4decc707 100644 --- a/backend/src/modules/crossroads/activitypub/activityPub.service.ts +++ b/backend/src/modules/crossroads/activitypub/activityPub.service.ts @@ -10,9 +10,9 @@ import { getUsernameFromWebfingerSubject, mapActorToWebfingerResponse, } from '@/modules/crossroads/activitypub/webfinger'; -import type { APActivity, APObject, APRoot } from 'activitypub-types'; +import type { APActivity, APActor, APObject, APRoot } from 'activitypub-types'; import { drizz } from 'db'; -import { and, asc, eq, inArray, isNotNull } from 'drizzle-orm'; +import { and, asc, eq, inArray, isNotNull, desc } from 'drizzle-orm'; import { ActivityPubActivity, ActivityPubActor, @@ -24,6 +24,7 @@ import { activityPubActivityQueue, activityPubActor, activityPubObject, + storedTreaty, } from 'db/schema'; import { SupportedObjectType } from '@/modules/crossroads/activitypub/object'; import { SupportedActivityType } from '@/modules/crossroads/activitypub/activity'; @@ -57,6 +58,8 @@ import { GameContent } from 'db/schemas/ActivityPubObject.schema'; import { contentTypeActivityStreams } from '@/modules/crossroads/activitypub/utils/contentType'; import { createSignedRequestConfig } from '@/modules/crossroads/activitypub/utils/signing'; import { OutboxDto } from '@/modules/crossroads/activitypub/dto/outbox.dto'; +import { FollowerDto } from '@/modules/crossroads/activitypub/dto/followers.dto'; +import { TreatyStatus } from '@/modules/treaty/types/treatyStatus'; type GameActivityObject = APRoot & { gameContent: GameContent; @@ -82,6 +85,16 @@ function mapActivityPubObjectToDto( }; } +function mapActivityPubActorToFollowerDto( + actor: ActivityPubActor, +): Partial> { + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actor.id, + type: actor.type, + }; +} + function mapActivityPubActivityToDto( activity: ActivityPubActivity, ): APRoot }> | null { @@ -692,6 +705,53 @@ export class ActivityPubService { return followers; } + async getFollowersCollection(): Promise { + const followers = await drizz.query.activityPubActor.findMany({ + where: (actor) => eq(actor.isFollowingThisServer, true), + // TODO store and order by follow date + orderBy: (actor) => asc(actor.id), + }); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + summary: 'Followers', + type: 'OrderedCollection', + totalItems: followers.length, + orderedItems: followers.map(mapActivityPubActorToFollowerDto), + }; + } + + async getFollowingCollection(): Promise { + const followedActors = await drizz + .select() + .from(storedTreaty) + .innerJoin( + activityPubActor, + eq(storedTreaty.activityPubActorId, activityPubActor.id), + ) + .where( + inArray(storedTreaty.status, [ + // All statuses that indicate we are following them + TreatyStatus.Signed, + TreatyStatus.Requested, + TreatyStatus.Rejected, + ]), + ) + .orderBy(desc(storedTreaty.createdOn)); + + const following = followedActors.map( + ({ activityPubActor: actor }) => actor, + ); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + summary: 'Following', + type: 'OrderedCollection', + totalItems: following.length, + orderedItems: following.map(mapActivityPubActorToFollowerDto), + }; + } + async createNoteObject( content: string, gameContent: GameContent, diff --git a/backend/src/modules/crossroads/activitypub/actor/index.ts b/backend/src/modules/crossroads/activitypub/actor/index.ts index ba918d3b..d3cbe07f 100644 --- a/backend/src/modules/crossroads/activitypub/actor/index.ts +++ b/backend/src/modules/crossroads/activitypub/actor/index.ts @@ -5,6 +5,8 @@ import { import { getActorPublicKeyUrl, getActorUrl, + getFollowersUrl, + getFollowingUrl, getInboxUrl, getOutboxUrl, } from '@/modules/crossroads/activitypub/utils/apUrl'; @@ -30,6 +32,8 @@ async function getActorFromId( preferredUsername: username ?? id, inbox: getInboxUrl().toString(), outbox: getOutboxUrl().toString(), + followers: getFollowersUrl().toString(), + following: getFollowingUrl().toString(), publicKey: { id: getActorPublicKeyUrl(id).toString(), diff --git a/backend/src/modules/crossroads/activitypub/dto/followers.dto.ts b/backend/src/modules/crossroads/activitypub/dto/followers.dto.ts new file mode 100644 index 00000000..4cb2360c --- /dev/null +++ b/backend/src/modules/crossroads/activitypub/dto/followers.dto.ts @@ -0,0 +1,20 @@ +import type { APActor, APRoot } from 'activitypub-types'; +import { IsArray, IsNumber, IsString, Min } from 'class-validator'; + +export class FollowerDto { + @IsString() + '@context': 'https://www.w3.org/ns/activitystreams'; + + @IsString() + 'summary': string; + + @IsString() + 'type': 'OrderedCollection'; + + @IsNumber() + @Min(0) + 'totalItems': number; + + @IsArray() + 'orderedItems': Array>>; +} diff --git a/backend/src/modules/crossroads/activitypub/utils/apUrl.ts b/backend/src/modules/crossroads/activitypub/utils/apUrl.ts index 6d4d5163..a6c0800d 100644 --- a/backend/src/modules/crossroads/activitypub/utils/apUrl.ts +++ b/backend/src/modules/crossroads/activitypub/utils/apUrl.ts @@ -32,3 +32,11 @@ export function getInboxUrl(): URL { export function getOutboxUrl(): URL { return new URL(`${activityPubBaseUrl}/outbox`); } + +export function getFollowersUrl(): URL { + return new URL(`${activityPubBaseUrl}/followers`); +} + +export function getFollowingUrl(): URL { + return new URL(`${activityPubBaseUrl}/following`); +}