Skip to content

Commit

Permalink
Updated thread implementation to work with posts instead of activities
Browse files Browse the repository at this point in the history
refs [AP-721](https://linear.app/ghost/issue/AP-721/update-getactivitythread-to-work-with-posts-instead-of-activities)

Updated thread implementation to work with posts instead of activities as part
of the posts migration
  • Loading branch information
mike182uk committed Feb 27, 2025
1 parent 2136aa3 commit 99244e2
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 174 deletions.
6 changes: 3 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ import {
createGetProfileFollowersHandler,
createGetProfileFollowingHandler,
createGetProfileHandler,
createGetThreadHandler,
createPostPublishedWebhookHandler,
createSearchHandler,
handleCreateNote,
handleGetActivities,
handleGetActivityThread,
handleGetProfilePosts,
handleWebhookSiteChanged,
} from './http/api';
Expand Down Expand Up @@ -916,8 +916,8 @@ app.get(
spanWrapper(handleGetProfilePosts),
);
app.get(
'/.ghost/activitypub/thread/:object_id',
spanWrapper(handleGetActivityThread),
'/.ghost/activitypub/thread/:post_ap_id',
spanWrapper(createGetThreadHandler(postRepository, accountService)),
);
app.get(
'/.ghost/activitypub/account/:handle',
Expand Down
74 changes: 0 additions & 74 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,32 +141,6 @@ export async function getActivityMetaWithoutJoin(
return map;
}

export async function getActivityChildren(activity: ActivityJsonLd) {
const objectId = activity.object.id;

const results = await client
.select('value')
.from('key_value')
.where(function () {
// If inReplyTo is a string
this.where(
client.raw(
`JSON_EXTRACT(value, "$.object.inReplyTo") = "${objectId}"`,
),
);

// If inReplyTo is an object
this.orWhere(
client.raw(
`JSON_EXTRACT(value, "$.object.inReplyTo.id") = "${objectId}"`,
),
);
})
.andWhere(client.raw(`JSON_EXTRACT(value, "$.type") = "Create"`));

return results.map((result) => result.value);
}

export async function getActivityChildrenCount(activity: ActivityJsonLd) {
const objectId = activity.object.id;

Expand Down Expand Up @@ -210,51 +184,3 @@ export async function getRepostCount(activity: ActivityJsonLd) {

return result[0].count;
}

export async function getActivityParents(activity: ActivityJsonLd) {
const parents: ActivityJsonLd[] = [];

const getParent = async (objectId: string) => {
const result = await client
.select('value')
.from('key_value')
.where(
client.raw(
`JSON_EXTRACT(value, "$.object.id") = "${objectId}"`,
),
)
.andWhere(client.raw(`JSON_EXTRACT(value, "$.type") = "Create"`));

if (result.length === 1) {
const parent = result[0];

parents.unshift(parent.value);

// inReplyTo can be a string or an object
const inReplyToId =
parent.value.object.inReplyTo?.id ??
parent.value.object.inReplyTo;

if (inReplyToId) {
await getParent(inReplyToId);
}
}
};

await getParent(
// inReplyTo can be a string or an object
activity.object.inReplyTo?.id ?? activity.object.inReplyTo,
);

return parents;
}

export async function getActivityForObject(objectId: string) {
const result = await client
.select('value')
.from('key_value')
.where(client.raw(`JSON_EXTRACT(value, "$.object.id") = "${objectId}"`))
.andWhere(client.raw(`JSON_EXTRACT(value, "$.type") = "Create"`));

return result[0].value;
}
98 changes: 1 addition & 97 deletions src/http/api/activities.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { type AppContext, fedify } from '../../app';
import {
getActivityChildren,
getActivityForObject,
getActivityMeta,
getActivityParents,
} from '../../db';
import { getActivityMeta } from '../../db';
import { buildActivity } from '../../helpers/activitypub/activity';
import { isUri } from '../../helpers/uri';
import { spanWrapper } from '../../instrumentation';

const GET_ACTIVITIES_DEFAULT_LIMIT = 10;
Expand Down Expand Up @@ -259,93 +253,3 @@ export async function handleGetActivities(ctx: AppContext) {
},
);
}

interface ActivityJsonLd {
[key: string]: any;
}

/**
* Handle a request for an activity thread
*
* @param ctx App context instance
*/
export async function handleGetActivityThread(ctx: AppContext) {
const db = ctx.get('db');
const globaldb = ctx.get('globaldb');
const logger = ctx.get('logger');
const apCtx = fedify.createContext(ctx.req.raw as Request, {
db,
globaldb,
logger,
});

// Parse "object_id" from request parameters
// /thread/:object_id
const paramObjectId = ctx.req.param('object_id');
const objectId = paramObjectId ? decodeURIComponent(paramObjectId) : '';

// If the provided objectId is invalid, return early
if (isUri(objectId) === false) {
return new Response(null, { status: 400 });
}

const activityJsonLd = await getActivityForObject(objectId);

// If the activity can not be found, return early
if (activityJsonLd === undefined) {
return new Response(null, { status: 404 });
}

const items: ActivityJsonLd[] = [activityJsonLd];

// If the object is a string, fetch the object from the database. We need to
// do this because we need the inReplyTo property of the object to find the
// parent(s) and children of the activity
if (typeof activityJsonLd.object === 'string') {
const object = await globaldb.get<ActivityJsonLd>([
activityJsonLd.object,
]);

if (object) {
activityJsonLd.object = object;
}
}

// Find children (replies) and append to the thread
const children = await getActivityChildren(activityJsonLd);
items.push(...children);

// Find parent(s) and prepend to the thread
const parents = await getActivityParents(activityJsonLd);
items.unshift(...parents);

// Build the activities so that they have all the data expected by the client
const likedRefs = (await db.get<string[]>(['liked'])) || [];
const repostedRefs = (await db.get<string[]>(['reposted'])) || [];

const builtActivities = await Promise.all(
items.map((item) =>
buildActivity(
item.id,
globaldb,
apCtx,
likedRefs,
repostedRefs,
true,
),
),
);

// Return the response
return new Response(
JSON.stringify({
items: builtActivities,
}),
{
headers: {
'Content-Type': 'application/json',
},
status: 200,
},
);
}
1 change: 1 addition & 0 deletions src/http/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './feed';
export * from './note';
export * from './profile';
export * from './search';
export * from './thread';
export * from './webhook';
51 changes: 51 additions & 0 deletions src/http/api/thread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AccountService } from 'account/account.service';
import type { AppContext } from '../../app';
import type { KnexPostRepository } from '../../post/post.repository.knex';
import { postToDTO } from './helpers/post';

/**
* Create a handler for a request for a thread
*
* @param postRepository Post repository instance
*/
export function createGetThreadHandler(
postRepository: KnexPostRepository,
accountService: AccountService,
) {
/**
* Handle a request for a thread
*
* @param ctx App context instance
*/
return async function handleGetThread(ctx: AppContext) {
const paramPostApId = ctx.req.param('post_ap_id');
const postApId = paramPostApId ? decodeURIComponent(paramPostApId) : '';

if (!postApId) {
return new Response(null, { status: 400 });
}

const account = await accountService.getDefaultAccountForSite(
ctx.get('site'),
);

const posts = (
await postRepository.getThreadByApId(postApId, account.id)
).map(({ post, likedByAccount, repostedByAccount }) => {
return postToDTO(post, {
likedByMe: likedByAccount,
repostedByMe: repostedByAccount,
repostedBy: null,
});
});

return new Response(
JSON.stringify({
posts,
}),
{
status: 200,
},
);
};
}
Loading

0 comments on commit 99244e2

Please sign in to comment.