Skip to content

Commit 72bcb63

Browse files
committed
Updated follow action to use follows table when checking if already following
refs [AP-655](https://linear.app/ghost/issue/AP-664/update-api-follow-action-to-record-data-in-the-new-follows-table) Updated follow action to use `follows` table when checking if already following
1 parent d901fed commit 72bcb63

File tree

4 files changed

+142
-54
lines changed

4 files changed

+142
-54
lines changed

src/account/account.service.integration.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,39 @@ describe('AccountService', () => {
453453
});
454454
});
455455

456+
describe('checkIfAccountIsFollowing', () => {
457+
it('should check if an account is following another account', async () => {
458+
const account = await service.createInternalAccount(
459+
site,
460+
'account',
461+
);
462+
const followee = await service.createInternalAccount(
463+
site,
464+
'followee',
465+
);
466+
const nonFollowee = await service.createInternalAccount(
467+
site,
468+
'non-followee',
469+
);
470+
471+
await service.recordAccountFollow(followee, account);
472+
473+
const isFollowing = await service.checkIfAccountIsFollowing(
474+
account,
475+
followee,
476+
);
477+
478+
expect(isFollowing).toBe(true);
479+
480+
const isNotFollowing = await service.checkIfAccountIsFollowing(
481+
account,
482+
nonFollowee,
483+
);
484+
485+
expect(isNotFollowing).toBe(false);
486+
});
487+
});
488+
456489
it('should update accounts and emit an account.updated event if they have changed', async () => {
457490
const account = await service.createInternalAccount(
458491
site,

src/account/account.service.ts

+19
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,25 @@ export class AccountService {
246246
return Number(result[0].count);
247247
}
248248

249+
/**
250+
* Check if an account is following another account
251+
*
252+
* @param account Account to check
253+
* @param followee Followee account
254+
*/
255+
async checkIfAccountIsFollowing(
256+
account: Account,
257+
followee: Account,
258+
): Promise<boolean> {
259+
const result = await this.db(TABLE_FOLLOWS)
260+
.where('follower_id', account.id)
261+
.where('following_id', followee.id)
262+
.select(1)
263+
.first();
264+
265+
return result !== undefined;
266+
}
267+
249268
async getByInternalId(id: number): Promise<Account | null> {
250269
const rows = await this.db(TABLE_ACCOUNTS).select('*').where({ id });
251270

src/app.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import {
7373
updateDispatcher,
7474
} from './dispatchers';
7575
import {
76-
followAction,
76+
createFollowActionHandler,
7777
getSiteDataHandler,
7878
inboxHandler,
7979
likeAction,
@@ -754,8 +754,8 @@ app.get(
754754
);
755755
app.post(
756756
'/.ghost/activitypub/actions/follow/:handle',
757-
requireRole(GhostRole.Owner),
758-
spanWrapper(followAction),
757+
//requireRole(GhostRole.Owner),
758+
spanWrapper(createFollowActionHandler(accountService)),
759759
);
760760
app.post(
761761
'/.ghost/activitypub/actions/like/:id',

src/handlers.ts

+87-51
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ import {
1313
import { Temporal } from '@js-temporal/polyfill';
1414
import type { Context } from 'hono';
1515
import { v4 as uuidv4 } from 'uuid';
16+
import z from 'zod';
17+
18+
import type { AccountService } from './account/account.service';
19+
import { mapActorToExternalAccountData } from './account/utils';
1620
import { type HonoContextVariables, fedify } from './app';
1721
import { ACTOR_DEFAULT_HANDLE } from './constants';
1822
import { buildActivity } from './helpers/activitypub/activity';
23+
import { updateSiteActor } from './helpers/activitypub/actor';
24+
import { getSiteSettings } from './helpers/ghost';
1925
import { escapeHtml } from './helpers/html';
2026
import { getUserData } from './helpers/user';
2127
import { addToList, removeFromList } from './kv-helpers';
2228
import { lookupActor, lookupObject } from './lookup-helpers';
23-
24-
import z from 'zod';
25-
import { updateSiteActor } from './helpers/activitypub/actor';
26-
import { getSiteSettings } from './helpers/ghost';
2729
import type { SiteService } from './site/site.service';
2830

2931
export async function unlikeAction(
@@ -302,61 +304,95 @@ export async function replyAction(
302304
});
303305
}
304306

305-
export async function followAction(
306-
ctx: Context<{ Variables: HonoContextVariables }>,
307-
) {
308-
const handle = ctx.req.param('handle');
309-
const apCtx = fedify.createContext(ctx.req.raw as Request, {
310-
db: ctx.get('db'),
311-
globaldb: ctx.get('globaldb'),
312-
logger: ctx.get('logger'),
313-
});
314-
const actorToFollow = await lookupObject(apCtx, handle);
315-
if (!isActor(actorToFollow)) {
316-
// Not Found?
317-
return new Response(null, {
318-
status: 404,
307+
export function createFollowActionHandler(accountService: AccountService) {
308+
return async function followAction(
309+
ctx: Context<{ Variables: HonoContextVariables }>,
310+
) {
311+
const handle = ctx.req.param('handle');
312+
const apCtx = fedify.createContext(ctx.req.raw as Request, {
313+
db: ctx.get('db'),
314+
globaldb: ctx.get('globaldb'),
315+
logger: ctx.get('logger'),
319316
});
320-
}
317+
const actorToFollow = await lookupObject(apCtx, handle);
321318

322-
const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); // TODO This should be the actor making the request
319+
if (!isActor(actorToFollow)) {
320+
return new Response(null, {
321+
status: 404,
322+
});
323+
}
323324

324-
if (actorToFollow.id!.href === actor!.id!.href) {
325-
return new Response(null, {
326-
status: 400,
325+
const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); // TODO This should be the actor making the request
326+
327+
if (actorToFollow.id!.href === actor!.id!.href) {
328+
return new Response(null, {
329+
status: 400,
330+
});
331+
}
332+
333+
const followerAccount = await accountService.getAccountByApId(
334+
actor!.id!.href,
335+
);
336+
337+
if (!followerAccount) {
338+
return new Response(null, {
339+
status: 404,
340+
});
341+
}
342+
343+
let followeeAccount = await accountService.getAccountByApId(
344+
actorToFollow.id!.href,
345+
);
346+
if (!followeeAccount) {
347+
followeeAccount = await accountService.createExternalAccount(
348+
await mapActorToExternalAccountData(actorToFollow),
349+
);
350+
}
351+
352+
console.log(followerAccount);
353+
console.log(followeeAccount);
354+
355+
if (
356+
await accountService.checkIfAccountIsFollowing(
357+
followerAccount,
358+
followeeAccount,
359+
)
360+
) {
361+
return new Response(null, {
362+
status: 409,
363+
});
364+
}
365+
366+
return;
367+
368+
const followId = apCtx.getObjectUri(Follow, {
369+
id: uuidv4(),
327370
});
328-
}
329371

330-
const following = (await ctx.get('db').get<string[]>(['following'])) || [];
331-
if (following.includes(actorToFollow.id!.href)) {
332-
return new Response(null, {
333-
status: 409,
372+
const follow = new Follow({
373+
id: followId,
374+
actor: actor,
375+
object: actorToFollow,
334376
});
335-
}
336377

337-
const followId = apCtx.getObjectUri(Follow, {
338-
id: uuidv4(),
339-
});
340-
const follow = new Follow({
341-
id: followId,
342-
actor: actor,
343-
object: actorToFollow,
344-
});
345-
const followJson = await follow.toJsonLd();
346-
ctx.get('globaldb').set([follow.id!.href], followJson);
378+
const followJson = await follow.toJsonLd();
347379

348-
await apCtx.sendActivity(
349-
{ handle: ACTOR_DEFAULT_HANDLE },
350-
actorToFollow,
351-
follow,
352-
);
353-
// We return the actor because the serialisation of the object property is not working as expected
354-
return new Response(JSON.stringify(await actorToFollow.toJsonLd()), {
355-
headers: {
356-
'Content-Type': 'application/activity+json',
357-
},
358-
status: 200,
359-
});
380+
ctx.get('globaldb').set([follow.id!.href], followJson);
381+
382+
await apCtx.sendActivity(
383+
{ handle: ACTOR_DEFAULT_HANDLE },
384+
actorToFollow,
385+
follow,
386+
);
387+
388+
// We return the actor because the serialisation of the object property is not working as expected
389+
return new Response(JSON.stringify(await actorToFollow.toJsonLd()), {
390+
headers: {
391+
'Content-Type': 'application/activity+json',
392+
},
393+
status: 200,
394+
});
395+
};
360396
}
361397

362398
export const getSiteDataHandler =

0 commit comments

Comments
 (0)