mirror of
https://iceshrimp.dev/iceshrimp/iceshrimp
synced 2024-11-24 15:16:07 +09:00
[mastodon-client] GET /accounts/:id/followers
This commit is contained in:
parent
f825dcc811
commit
05c32e719c
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user