diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 4fe34c0af..f312ba371 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -58,10 +58,11 @@ export function setupEndpointsAccount(router: Router): void { const userId = convertId(ctx.params.id, IdType.IceshrimpId); const query = await UserHelpers.getUserCachedOr404(userId, ctx.cache); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query)))); - const tl = await UserHelpers.getUserStatuses(query, ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged) - .then(n => NoteConverter.encodeMany(n, ctx.user, ctx.cache)); + const res = await UserHelpers.getUserStatuses(query, ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged); + const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = tl.map(s => convertStatusIds(s)); + ctx.pagination = res.pagination; }, ); router.get<{ Params: { id: string } }>( diff --git a/packages/backend/src/server/api/mastodon/endpoints/misc.ts b/packages/backend/src/server/api/mastodon/endpoints/misc.ts index 1c59f97a8..d53244699 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/misc.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/misc.ts @@ -42,6 +42,7 @@ export function setupEndpointsMisc(router: Router): void { }, ); + //FIXME: add link pagination to trends (ref: https://mastodon.social/api/v1/trends/tags?offset=10&limit=1) router.get(["/v1/trends/tags", "/v1/trends"], async (ctx) => { const args = limitToInt(ctx.query); diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 3105456a8..af07bfebe 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -13,11 +13,11 @@ export function setupEndpointsNotifications(router: Router): void { async (ctx) => { const cache = UserHelpers.getFreshAccountCache(); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']); - const data = NotificationHelpers.getNotifications(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id) - .then(p => NotificationConverter.encodeMany(p, ctx.user, cache)) - .then(p => p.map(n => convertNotificationIds(n))); + const res = await NotificationHelpers.getNotifications(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id); + const data = await NotificationConverter.encodeMany(res.data, ctx.user, cache); - ctx.body = await data; + ctx.body = data.map(n => convertNotificationIds(n)); + ctx.pagination = res.pagination; } ); diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 1cdf2cc6e..d4f72c66d 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -4,9 +4,7 @@ import { convertConversationIds, convertStatusIds, } from "../converters.js"; import { convertId, IdType } from "../../index.js"; import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; -import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserLists } from "@/models/index.js"; -import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; @@ -69,11 +67,11 @@ export function setupEndpointsTimeline(router: Router): void { auth(true, ['read:statuses']), async (ctx, reply) => { const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query)))); - const cache = UserHelpers.getFreshAccountCache(); - const tl = await TimelineHelpers.getPublicTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote) - .then(n => NoteConverter.encodeMany(n, ctx.user, cache)); + const res = await TimelineHelpers.getPublicTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote); + const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = tl.map(s => convertStatusIds(s)); + ctx.pagination = res.pagination; }); router.get<{ Params: { hashtag: string } }>( "/v1/timelines/tag/:hashtag", @@ -81,22 +79,22 @@ export function setupEndpointsTimeline(router: Router): void { async (ctx, reply) => { const tag = (ctx.params.hashtag ?? '').trim(); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))), ['any[]', 'all[]', 'none[]']); - const cache = UserHelpers.getFreshAccountCache(); - const tl = await TimelineHelpers.getTagTimeline(ctx.user, tag, args.max_id, args.since_id, args.min_id, args.limit, args['any[]'] ?? [], args['all[]'] ?? [], args['none[]'] ?? [], args.only_media, args.local, args.remote) - .then(n => NoteConverter.encodeMany(n, ctx.user, cache)); + const res = await TimelineHelpers.getTagTimeline(ctx.user, tag, args.max_id, args.since_id, args.min_id, args.limit, args['any[]'] ?? [], args['all[]'] ?? [], args['none[]'] ?? [], args.only_media, args.local, args.remote); + const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = tl.map(s => convertStatusIds(s)); + ctx.pagination = res.pagination; }, ); router.get("/v1/timelines/home", auth(true, ['read:statuses']), async (ctx, reply) => { const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); - const cache = UserHelpers.getFreshAccountCache(); - const tl = await TimelineHelpers.getHomeTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit) - .then(n => NoteConverter.encodeMany(n, ctx.user, cache)); + const res = await TimelineHelpers.getHomeTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); + const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = tl.map(s => convertStatusIds(s)); + ctx.pagination = res.pagination; }); router.get<{ Params: { listId: string } }>( "/v1/timelines/list/:listId", @@ -107,11 +105,11 @@ export function setupEndpointsTimeline(router: Router): void { if (!list) throw new MastoApiError(404); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); - const cache = UserHelpers.getFreshAccountCache(); - const tl = await TimelineHelpers.getListTimeline(ctx.user, list, args.max_id, args.since_id, args.min_id, args.limit) - .then(n => NoteConverter.encodeMany(n, ctx.user, cache)); + const res = await TimelineHelpers.getListTimeline(ctx.user, list, args.max_id, args.since_id, args.min_id, args.limit); + const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = tl.map(s => convertStatusIds(s)); + ctx.pagination = res.pagination; }, ); router.get("/v1/conversations", diff --git a/packages/backend/src/server/api/mastodon/helpers/notification.ts b/packages/backend/src/server/api/mastodon/helpers/notification.ts index 532693a0e..d00637016 100644 --- a/packages/backend/src/server/api/mastodon/helpers/notification.ts +++ b/packages/backend/src/server/api/mastodon/helpers/notification.ts @@ -3,9 +3,10 @@ import { Notes, Notifications } from "@/models/index.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { Notification } from "@/models/entities/notification.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; export class NotificationHelpers { - public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise { + public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise> { if (limit > 80) limit = 80; if (types && excludeTypes) throw new Error("types and exclude_types can not be used simultaneously"); @@ -32,7 +33,7 @@ export class NotificationHelpers { query.leftJoinAndSelect("notification.note", "note"); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } public static async getNotification(id: string, user: ILocalUser): Promise { diff --git a/packages/backend/src/server/api/mastodon/helpers/pagination.ts b/packages/backend/src/server/api/mastodon/helpers/pagination.ts index 084aed5b4..60eb1fef2 100644 --- a/packages/backend/src/server/api/mastodon/helpers/pagination.ts +++ b/packages/backend/src/server/api/mastodon/helpers/pagination.ts @@ -1,6 +1,5 @@ import { ObjectLiteral, SelectQueryBuilder } from "typeorm"; -import config from "@/config/index.js"; -import { convertId, IdType } from "../../index.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; export class PaginationHelpers { public static makePaginationQuery( @@ -45,4 +44,19 @@ export class PaginationHelpers { public static async execQuery(query: SelectQueryBuilder, limit: number, reverse: boolean): Promise { return query.take(limit).getMany().then(found => reverse ? found.reverse() : found); } + + public static async execQueryLinkPagination(query: SelectQueryBuilder, limit: number, reverse: boolean): Promise> { + return this.execQuery(query, limit, reverse) + .then(p => { + const ids = p.map(x => x.id); + return { + data: p, + pagination: p.length > 0 ? { + limit: limit, + maxId: ids.at(reverse ? 0 : -1), + minId: ids.at(reverse ? -1 : 0) + } : undefined + } + }); + } } diff --git a/packages/backend/src/server/api/mastodon/helpers/timeline.ts b/packages/backend/src/server/api/mastodon/helpers/timeline.ts index b9e892da1..b664cb9ec 100644 --- a/packages/backend/src/server/api/mastodon/helpers/timeline.ts +++ b/packages/backend/src/server/api/mastodon/helpers/timeline.ts @@ -21,7 +21,7 @@ import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js" import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; export class TimelineHelpers { - public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise { + public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise> { if (limit > 40) limit = 40; const followingQuery = Followings.createQueryBuilder("following") @@ -52,10 +52,10 @@ export class TimelineHelpers { query.andWhere("note.visibility != 'hidden'"); query.andWhere("note.visibility != 'specified'"); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } - public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise { + public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise> { if (limit > 40) limit = 40; if (local && remote) { @@ -95,10 +95,10 @@ export class TimelineHelpers { if (onlyMedia) query.andWhere("note.fileIds != '{}'"); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } - public static async getListTimeline(user: ILocalUser, list: UserList, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise { + public static async getListTimeline(user: ILocalUser, list: UserList, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise> { if (limit > 40) limit = 40; if (user.id != list.userId) throw new Error("List is not owned by user"); @@ -119,10 +119,10 @@ export class TimelineHelpers { generateVisibilityQuery(query, user); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } - public static async getTagTimeline(user: ILocalUser, tag: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, any: string[], all: string[], none: string[], onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise { + public static async getTagTimeline(user: ILocalUser, tag: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, any: string[], all: string[], none: string[], onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise> { if (limit > 40) limit = 40; if (tag.length < 1) throw new MastoApiError(400, "Tag cannot be empty"); @@ -160,7 +160,7 @@ export class TimelineHelpers { if (onlyMedia) query.andWhere("note.fileIds != '{}'"); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } public static async getConversations(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise> { diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 1ee875ba8..d79a2d37c 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -323,12 +323,12 @@ export class UserHelpers { }); } - public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise { + public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise> { if (limit > 40) limit = 40; if (tagged !== undefined) { //FIXME respect tagged - return []; + return {data: []}; } const query = PaginationHelpers.makePaginationQuery( @@ -381,7 +381,7 @@ export class UserHelpers { query.setParameters({ userId: user.id }); - return PaginationHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); } public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise> {