Skip to content

Commit

Permalink
Updated follow action to use follows table when checking if already…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
mike182uk committed Jan 24, 2025
1 parent d901fed commit b08b08a
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 57 deletions.
6 changes: 2 additions & 4 deletions features/step_definitions/stepdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -611,12 +611,10 @@ Given('we follow {string}', async function (name) {
},
);
if (this.response.ok) {
const actor = await this.response.clone().json();

this.activities[`Follow(${name})`] = await createActivity(
'Follow',
actor,
actor,
this.actors[name],
this.actors.Us,
);
}
});
Expand Down
33 changes: 33 additions & 0 deletions src/account/account.service.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,39 @@ describe('AccountService', () => {
});
});

describe('checkIfAccountIsFollowing', () => {
it('should check if an account is following another account', async () => {
const account = await service.createInternalAccount(
site,
'account',
);
const followee = await service.createInternalAccount(
site,
'followee',
);
const nonFollowee = await service.createInternalAccount(
site,
'non-followee',
);

await service.recordAccountFollow(followee, account);

const isFollowing = await service.checkIfAccountIsFollowing(
account,
followee,
);

expect(isFollowing).toBe(true);

const isNotFollowing = await service.checkIfAccountIsFollowing(
account,
nonFollowee,
);

expect(isNotFollowing).toBe(false);
});
});

it('should update accounts and emit an account.updated event if they have changed', async () => {
const account = await service.createInternalAccount(
site,
Expand Down
19 changes: 19 additions & 0 deletions src/account/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,25 @@ export class AccountService {
return Number(result[0].count);
}

/**
* Check if an account is following another account
*
* @param account Account to check
* @param followee Followee account
*/
async checkIfAccountIsFollowing(
account: Account,
followee: Account,
): Promise<boolean> {
const result = await this.db(TABLE_FOLLOWS)
.where('follower_id', account.id)
.where('following_id', followee.id)
.select(1)
.first();

return result !== undefined;
}

async getByInternalId(id: number): Promise<Account | null> {
const rows = await this.db(TABLE_ACCOUNTS).select('*').where({ id });

Expand Down
4 changes: 2 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import {
updateDispatcher,
} from './dispatchers';
import {
followAction,
createFollowActionHandler,
getSiteDataHandler,
inboxHandler,
likeAction,
Expand Down Expand Up @@ -755,7 +755,7 @@ app.get(
app.post(
'/.ghost/activitypub/actions/follow/:handle',
requireRole(GhostRole.Owner),
spanWrapper(followAction),
spanWrapper(createFollowActionHandler(accountService)),
);
app.post(
'/.ghost/activitypub/actions/like/:id',
Expand Down
133 changes: 82 additions & 51 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ import {
import { Temporal } from '@js-temporal/polyfill';
import type { Context } from 'hono';
import { v4 as uuidv4 } from 'uuid';
import z from 'zod';

import type { AccountService } from './account/account.service';
import { mapActorToExternalAccountData } from './account/utils';
import { type HonoContextVariables, fedify } from './app';
import { ACTOR_DEFAULT_HANDLE } from './constants';
import { buildActivity } from './helpers/activitypub/activity';
import { updateSiteActor } from './helpers/activitypub/actor';
import { getSiteSettings } from './helpers/ghost';
import { escapeHtml } from './helpers/html';
import { getUserData } from './helpers/user';
import { addToList, removeFromList } from './kv-helpers';
import { lookupActor, lookupObject } from './lookup-helpers';

import z from 'zod';
import { updateSiteActor } from './helpers/activitypub/actor';
import { getSiteSettings } from './helpers/ghost';
import type { SiteService } from './site/site.service';

export async function unlikeAction(
Expand Down Expand Up @@ -302,61 +304,90 @@ export async function replyAction(
});
}

export async function followAction(
ctx: Context<{ Variables: HonoContextVariables }>,
) {
const handle = ctx.req.param('handle');
const apCtx = fedify.createContext(ctx.req.raw as Request, {
db: ctx.get('db'),
globaldb: ctx.get('globaldb'),
logger: ctx.get('logger'),
});
const actorToFollow = await lookupObject(apCtx, handle);
if (!isActor(actorToFollow)) {
// Not Found?
return new Response(null, {
status: 404,
export function createFollowActionHandler(accountService: AccountService) {
return async function followAction(
ctx: Context<{ Variables: HonoContextVariables }>,
) {
const handle = ctx.req.param('handle');
const apCtx = fedify.createContext(ctx.req.raw as Request, {
db: ctx.get('db'),
globaldb: ctx.get('globaldb'),
logger: ctx.get('logger'),
});
}
const actorToFollow = await lookupObject(apCtx, handle);

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

if (actorToFollow.id!.href === actor!.id!.href) {
return new Response(null, {
status: 400,
const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); // TODO This should be the actor making the request

if (actorToFollow.id!.href === actor!.id!.href) {
return new Response(null, {
status: 400,
});
}

const followerAccount = await accountService.getAccountByApId(
actor!.id!.href,
);

if (!followerAccount) {
return new Response(null, {
status: 404,
});
}

let followeeAccount = await accountService.getAccountByApId(
actorToFollow.id!.href,
);
if (!followeeAccount) {
followeeAccount = await accountService.createExternalAccount(
await mapActorToExternalAccountData(actorToFollow),
);
}

if (
await accountService.checkIfAccountIsFollowing(
followerAccount,
followeeAccount,
)
) {
return new Response(null, {
status: 409,
});
}

const followId = apCtx.getObjectUri(Follow, {
id: uuidv4(),
});
}

const following = (await ctx.get('db').get<string[]>(['following'])) || [];
if (following.includes(actorToFollow.id!.href)) {
return new Response(null, {
status: 409,
const follow = new Follow({
id: followId,
actor: actor,
object: actorToFollow,
});
}

const followId = apCtx.getObjectUri(Follow, {
id: uuidv4(),
});
const follow = new Follow({
id: followId,
actor: actor,
object: actorToFollow,
});
const followJson = await follow.toJsonLd();
ctx.get('globaldb').set([follow.id!.href], followJson);
const followJson = await follow.toJsonLd();

await apCtx.sendActivity(
{ handle: ACTOR_DEFAULT_HANDLE },
actorToFollow,
follow,
);
// We return the actor because the serialisation of the object property is not working as expected
return new Response(JSON.stringify(await actorToFollow.toJsonLd()), {
headers: {
'Content-Type': 'application/activity+json',
},
status: 200,
});
ctx.get('globaldb').set([follow.id!.href], followJson);

await apCtx.sendActivity(
{ handle: ACTOR_DEFAULT_HANDLE },
actorToFollow,
follow,
);

// We return the actor because the serialisation of the object property is not working as expected
return new Response(JSON.stringify(await actorToFollow.toJsonLd()), {
headers: {
'Content-Type': 'application/activity+json',
},
status: 200,
});
};
}

export const getSiteDataHandler =
Expand Down

0 comments on commit b08b08a

Please sign in to comment.