Skip to content

Commit d30377c

Browse files
committed
feat: add followers and following to merchant actor
1 parent bdf80f8 commit d30377c

File tree

5 files changed

+113
-2
lines changed

5 files changed

+113
-2
lines changed

backend/src/modules/crossroads/activitypub/activityPub.controller.ts

+19
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ActivityPubActorObject } from '@/modules/crossroads/activitypub/ac
1313
import { crossroadsBasePath } from '@/config/apiPaths';
1414
import { contentTypeActivityStreams } from '@/modules/crossroads/activitypub/utils/contentType';
1515
import { OutboxDto } from '@/modules/crossroads/activitypub/dto/outbox.dto';
16+
import { FollowerDto } from '@/modules/crossroads/activitypub/dto/followers.dto';
1617

1718
@Controller({ path: crossroadsBasePath })
1819
export class ActivityPubController {
@@ -59,6 +60,24 @@ export class ActivityPubController {
5960
return outbox;
6061
}
6162

63+
@Get('/followers')
64+
@Header('content-type', contentTypeActivityStreams)
65+
async getFollowers(): Promise<FollowerDto> {
66+
// TODO pagination
67+
const followers = await this.activityPubService.getFollowersCollection();
68+
69+
return followers;
70+
}
71+
72+
@Get('/following')
73+
@Header('content-type', contentTypeActivityStreams)
74+
async getFollowing(): Promise<FollowerDto> {
75+
// TODO pagination
76+
const following = await this.activityPubService.getFollowingCollection();
77+
78+
return following;
79+
}
80+
6281
@Post('/inbox')
6382
@Header('content-type', contentTypeActivityStreams)
6483
async postToInbox(@Body() body: unknown): Promise<void> {

backend/src/modules/crossroads/activitypub/activityPub.service.ts

+62-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
getUsernameFromWebfingerSubject,
1111
mapActorToWebfingerResponse,
1212
} from '@/modules/crossroads/activitypub/webfinger';
13-
import type { APActivity, APObject, APRoot } from 'activitypub-types';
13+
import type { APActivity, APActor, APObject, APRoot } from 'activitypub-types';
1414
import { drizz } from 'db';
15-
import { and, asc, eq, inArray, isNotNull } from 'drizzle-orm';
15+
import { and, asc, eq, inArray, isNotNull, desc } from 'drizzle-orm';
1616
import {
1717
ActivityPubActivity,
1818
ActivityPubActor,
@@ -24,6 +24,7 @@ import {
2424
activityPubActivityQueue,
2525
activityPubActor,
2626
activityPubObject,
27+
storedTreaty,
2728
} from 'db/schema';
2829
import { SupportedObjectType } from '@/modules/crossroads/activitypub/object';
2930
import { SupportedActivityType } from '@/modules/crossroads/activitypub/activity';
@@ -57,6 +58,8 @@ import { GameContent } from 'db/schemas/ActivityPubObject.schema';
5758
import { contentTypeActivityStreams } from '@/modules/crossroads/activitypub/utils/contentType';
5859
import { createSignedRequestConfig } from '@/modules/crossroads/activitypub/utils/signing';
5960
import { OutboxDto } from '@/modules/crossroads/activitypub/dto/outbox.dto';
61+
import { FollowerDto } from '@/modules/crossroads/activitypub/dto/followers.dto';
62+
import { TreatyStatus } from '@/modules/treaty/types/treatyStatus';
6063

6164
type GameActivityObject = APRoot<APObject> & {
6265
gameContent: GameContent;
@@ -82,6 +85,16 @@ function mapActivityPubObjectToDto(
8285
};
8386
}
8487

88+
function mapActivityPubActorToFollowerDto(
89+
actor: ActivityPubActor,
90+
): Partial<APRoot<APActor>> {
91+
return {
92+
'@context': 'https://www.w3.org/ns/activitystreams',
93+
id: actor.id,
94+
type: actor.type,
95+
};
96+
}
97+
8598
function mapActivityPubActivityToDto(
8699
activity: ActivityPubActivity,
87100
): APRoot<APActivity & { object: APRoot<APActivity> }> | null {
@@ -692,6 +705,53 @@ export class ActivityPubService {
692705
return followers;
693706
}
694707

708+
async getFollowersCollection(): Promise<FollowerDto> {
709+
const followers = await drizz.query.activityPubActor.findMany({
710+
where: (actor) => eq(actor.isFollowingThisServer, true),
711+
// TODO store and order by follow date
712+
orderBy: (actor) => asc(actor.id),
713+
});
714+
715+
return {
716+
'@context': 'https://www.w3.org/ns/activitystreams',
717+
summary: 'Followers',
718+
type: 'OrderedCollection',
719+
totalItems: followers.length,
720+
orderedItems: followers.map(mapActivityPubActorToFollowerDto),
721+
};
722+
}
723+
724+
async getFollowingCollection(): Promise<FollowerDto> {
725+
const followedActors = await drizz
726+
.select()
727+
.from(storedTreaty)
728+
.innerJoin(
729+
activityPubActor,
730+
eq(storedTreaty.activityPubActorId, activityPubActor.id),
731+
)
732+
.where(
733+
inArray(storedTreaty.status, [
734+
// All statuses that indicate we are following them
735+
TreatyStatus.Signed,
736+
TreatyStatus.Requested,
737+
TreatyStatus.Rejected,
738+
]),
739+
)
740+
.orderBy(desc(storedTreaty.createdOn));
741+
742+
const following = followedActors.map(
743+
({ activityPubActor: actor }) => actor,
744+
);
745+
746+
return {
747+
'@context': 'https://www.w3.org/ns/activitystreams',
748+
summary: 'Following',
749+
type: 'OrderedCollection',
750+
totalItems: following.length,
751+
orderedItems: following.map(mapActivityPubActorToFollowerDto),
752+
};
753+
}
754+
695755
async createNoteObject(
696756
content: string,
697757
gameContent: GameContent,

backend/src/modules/crossroads/activitypub/actor/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
import {
66
getActorPublicKeyUrl,
77
getActorUrl,
8+
getFollowersUrl,
9+
getFollowingUrl,
810
getInboxUrl,
911
getOutboxUrl,
1012
} from '@/modules/crossroads/activitypub/utils/apUrl';
@@ -30,6 +32,8 @@ async function getActorFromId(
3032
preferredUsername: username ?? id,
3133
inbox: getInboxUrl().toString(),
3234
outbox: getOutboxUrl().toString(),
35+
followers: getFollowersUrl().toString(),
36+
following: getFollowingUrl().toString(),
3337

3438
publicKey: {
3539
id: getActorPublicKeyUrl(id).toString(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { APActor, APRoot } from 'activitypub-types';
2+
import { IsArray, IsNumber, IsString, Min } from 'class-validator';
3+
4+
export class FollowerDto {
5+
@IsString()
6+
'@context': 'https://www.w3.org/ns/activitystreams';
7+
8+
@IsString()
9+
'summary': string;
10+
11+
@IsString()
12+
'type': 'OrderedCollection';
13+
14+
@IsNumber()
15+
@Min(0)
16+
'totalItems': number;
17+
18+
@IsArray()
19+
'orderedItems': Array<Partial<APRoot<APActor>>>;
20+
}

backend/src/modules/crossroads/activitypub/utils/apUrl.ts

+8
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ export function getInboxUrl(): URL {
3232
export function getOutboxUrl(): URL {
3333
return new URL(`${activityPubBaseUrl}/outbox`);
3434
}
35+
36+
export function getFollowersUrl(): URL {
37+
return new URL(`${activityPubBaseUrl}/followers`);
38+
}
39+
40+
export function getFollowingUrl(): URL {
41+
return new URL(`${activityPubBaseUrl}/following`);
42+
}

0 commit comments

Comments
 (0)