[mastodon-client] GET /accounts/:id/followers

This commit is contained in:
Laura Hausmann 2023-09-24 17:50:28 +02:00
parent f825dcc811
commit 05c32e719c
No known key found for this signature in database
GPG Key ID: D044E84C5BE01605
6 changed files with 89 additions and 45 deletions

View File

@ -1,4 +1,4 @@
import { User } from "@/models/entities/user.js";
import { ILocalUser, User } from "@/models/entities/user.js";
import config from "@/config/index.js";
import { DriveFiles, UserProfiles, Users } from "@/models/index.js";
import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.js";
@ -8,6 +8,7 @@ import { escapeMFM } from "@/server/api/mastodon/converters/mfm.js";
import mfm from "mfm-js";
import { awaitAll } from "@/prelude/await-all.js";
import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { Note } from "@/models/entities/note.js";
type Field = {
name: string;
@ -65,6 +66,11 @@ export class UserConverter {
});
}
public static async encodeMany(users: User[], cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Account[]> {
const encoded = users.map(u => this.encode(u, cache));
return Promise.all(encoded);
}
private static encodeField(f: Field): MastodonEntity.Field {
return {
name: f.name,
@ -72,5 +78,4 @@ export class UserConverter {
verified_at: f.verified ? (new Date()).toISOString() : null,
}
}
}

View File

@ -198,15 +198,19 @@ export function apiAccountMastodon(router: Router): void {
router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/followers",
async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.IceshrimpId),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((account) => convertAccount(account));
const auth = await authenticate(ctx.headers.authorization, null);
const user = auth[0] ?? null;
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
const cache = UserHelpers.getFreshAccountCache();
const query = await UserHelpers.getUserCached(userId, cache);
const args = normalizeUrlQuery(convertTimelinesArgsId(limitToInt(ctx.query as any)));
const followers = await UserHelpers.getUserFollowers(query, user, args.max_id, args.since_id, args.min_id, args.limit)
.then(f => UserConverter.encodeMany(f, cache));
ctx.body = followers.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);

View File

@ -46,37 +46,6 @@ export class NoteHelpers {
return notes;
}
public static makePaginationQuery<T extends ObjectLiteral>(
q: SelectQueryBuilder<T>,
sinceId?: string,
maxId?: string,
minId?: string
) {
if (sinceId && minId) throw new Error("Can't user both sinceId and minId params");
if (sinceId && maxId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "DESC");
} if (minId && maxId) {
q.andWhere(`${q.alias}.id > :minId`, { minId: minId });
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "ASC");
} else if (sinceId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.id`, "DESC");
} else if (minId) {
q.andWhere(`${q.alias}.id > :minId`, { minId: minId });
q.orderBy(`${q.alias}.id`, "ASC");
} else if (maxId) {
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "DESC");
} else {
q.orderBy(`${q.alias}.id`, "DESC");
}
return q;
}
/**
*
* @param query

View File

@ -0,0 +1,34 @@
import { ObjectLiteral, SelectQueryBuilder } from "typeorm";
export class PaginationHelpers {
public static makePaginationQuery<T extends ObjectLiteral>(
q: SelectQueryBuilder<T>,
sinceId?: string,
maxId?: string,
minId?: string
) {
if (sinceId && minId) throw new Error("Can't user both sinceId and minId params");
if (sinceId && maxId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "DESC");
} if (minId && maxId) {
q.andWhere(`${q.alias}.id > :minId`, { minId: minId });
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "ASC");
} else if (sinceId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.id`, "DESC");
} else if (minId) {
q.andWhere(`${q.alias}.id > :minId`, { minId: minId });
q.orderBy(`${q.alias}.id`, "ASC");
} else if (maxId) {
q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId });
q.orderBy(`${q.alias}.id`, "DESC");
} else {
q.orderBy(`${q.alias}.id`, "DESC");
}
return q;
}
}

View File

@ -14,6 +14,7 @@ import { fetchMeta } from "@/misc/fetch-meta.js";
import { ApiError } from "@/server/api/error.js";
import { meta } from "@/server/api/endpoints/notes/global-timeline.js";
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
export class TimelineHelpers {
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
@ -31,7 +32,7 @@ export class TimelineHelpers {
.select("following.followeeId")
.where("following.followerId = :followerId", {followerId: user.id});
const query = NoteHelpers.makePaginationQuery(
const query = PaginationHelpers.makePaginationQuery(
Notes.createQueryBuilder("note"),
sinceId,
maxId,
@ -84,7 +85,7 @@ export class TimelineHelpers {
throw new Error("local and remote are mutually exclusive options");
}
const query = NoteHelpers.makePaginationQuery(
const query = PaginationHelpers.makePaginationQuery(
Notes.createQueryBuilder("note"),
sinceId,
maxId,

View File

@ -1,6 +1,6 @@
import { Note } from "@/models/entities/note.js";
import { ILocalUser, User } from "@/models/entities/user.js";
import { Notes } from "@/models/index.js";
import { Followings, Notes, UserProfiles } from "@/models/index.js";
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js";
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
@ -10,6 +10,7 @@ import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
import Entity from "megalodon/src/entity.js";
import AsyncLock from "async-lock";
import { getUser } from "@/server/api/common/getters.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
export type AccountCache = {
locks: AsyncLock;
@ -31,7 +32,7 @@ export class UserHelpers {
return [];
}
const query = NoteHelpers.makePaginationQuery(
const query = PaginationHelpers.makePaginationQuery(
Notes.createQueryBuilder("note"),
sinceId,
maxId,
@ -69,6 +70,36 @@ export class UserHelpers {
return NoteHelpers.execQuery(query, limit, minId !== undefined);
}
public static async getUserFollowers(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<User[]> {
if (limit > 80) limit = 80;
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === "private") {
if (!localUser || user.id != localUser.id) return [];
}
else if (profile.ffVisibility === "followers") {
if (!localUser) return [];
const isFollowed = await Followings.exist({
where: {
followeeId: user.id,
followerId: localUser.id,
},
});
if (!isFollowed) return [];
}
const query = PaginationHelpers.makePaginationQuery(
Followings.createQueryBuilder("following"),
sinceId,
maxId,
minId
)
.andWhere("following.followeeId = :userId", { userId: user.id })
.innerJoinAndSelect("following.follower", "follower");
return query.take(limit).getMany().then(p => p.map(p => p.follower).filter(p => p) as User[]);
}
public static async getUserCached(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<User> {
return cache.locks.acquire(id, async () => {
const cacheHit = cache.users.find(p => p.id == id);