[mastodon-client] GET /accounts/:id/followers
This commit is contained in:
parent
f825dcc811
commit
05c32e719c
6 changed files with 89 additions and 45 deletions
|
|
@ -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 config from "@/config/index.js";
|
||||||
import { DriveFiles, UserProfiles, Users } from "@/models/index.js";
|
import { DriveFiles, UserProfiles, Users } from "@/models/index.js";
|
||||||
import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.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 mfm from "mfm-js";
|
||||||
import { awaitAll } from "@/prelude/await-all.js";
|
import { awaitAll } from "@/prelude/await-all.js";
|
||||||
import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||||
|
import { Note } from "@/models/entities/note.js";
|
||||||
|
|
||||||
type Field = {
|
type Field = {
|
||||||
name: string;
|
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 {
|
private static encodeField(f: Field): MastodonEntity.Field {
|
||||||
return {
|
return {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
|
|
@ -72,5 +78,4 @@ export class UserConverter {
|
||||||
verified_at: f.verified ? (new Date()).toISOString() : null,
|
verified_at: f.verified ? (new Date()).toISOString() : null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,15 +198,19 @@ export function apiAccountMastodon(router: Router): void {
|
||||||
router.get<{ Params: { id: string } }>(
|
router.get<{ Params: { id: string } }>(
|
||||||
"/v1/accounts/:id/followers",
|
"/v1/accounts/:id/followers",
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
|
||||||
const accessTokens = ctx.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const data = await client.getAccountFollowers(
|
const auth = await authenticate(ctx.headers.authorization, null);
|
||||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
const user = auth[0] ?? null;
|
||||||
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
|
||||||
);
|
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||||
ctx.body = data.data.map((account) => convertAccount(account));
|
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) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
console.error(e.response.data);
|
console.error(e.response.data);
|
||||||
|
|
|
||||||
|
|
@ -46,37 +46,6 @@ export class NoteHelpers {
|
||||||
return notes;
|
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
|
* @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 { ApiError } from "@/server/api/error.js";
|
||||||
import { meta } from "@/server/api/endpoints/notes/global-timeline.js";
|
import { meta } from "@/server/api/endpoints/notes/global-timeline.js";
|
||||||
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
||||||
|
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||||
|
|
||||||
export class TimelineHelpers {
|
export class TimelineHelpers {
|
||||||
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
|
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")
|
.select("following.followeeId")
|
||||||
.where("following.followerId = :followerId", {followerId: user.id});
|
.where("following.followerId = :followerId", {followerId: user.id});
|
||||||
|
|
||||||
const query = NoteHelpers.makePaginationQuery(
|
const query = PaginationHelpers.makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
sinceId,
|
sinceId,
|
||||||
maxId,
|
maxId,
|
||||||
|
|
@ -84,7 +85,7 @@ export class TimelineHelpers {
|
||||||
throw new Error("local and remote are mutually exclusive options");
|
throw new Error("local and remote are mutually exclusive options");
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = NoteHelpers.makePaginationQuery(
|
const query = PaginationHelpers.makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
sinceId,
|
sinceId,
|
||||||
maxId,
|
maxId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Note } from "@/models/entities/note.js";
|
import { Note } from "@/models/entities/note.js";
|
||||||
import { ILocalUser, User } from "@/models/entities/user.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 { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||||
import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js";
|
import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js";
|
||||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-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 Entity from "megalodon/src/entity.js";
|
||||||
import AsyncLock from "async-lock";
|
import AsyncLock from "async-lock";
|
||||||
import { getUser } from "@/server/api/common/getters.js";
|
import { getUser } from "@/server/api/common/getters.js";
|
||||||
|
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||||
|
|
||||||
export type AccountCache = {
|
export type AccountCache = {
|
||||||
locks: AsyncLock;
|
locks: AsyncLock;
|
||||||
|
|
@ -31,7 +32,7 @@ export class UserHelpers {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = NoteHelpers.makePaginationQuery(
|
const query = PaginationHelpers.makePaginationQuery(
|
||||||
Notes.createQueryBuilder("note"),
|
Notes.createQueryBuilder("note"),
|
||||||
sinceId,
|
sinceId,
|
||||||
maxId,
|
maxId,
|
||||||
|
|
@ -69,6 +70,36 @@ export class UserHelpers {
|
||||||
return NoteHelpers.execQuery(query, limit, minId !== undefined);
|
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> {
|
public static async getUserCached(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<User> {
|
||||||
return cache.locks.acquire(id, async () => {
|
return cache.locks.acquire(id, async () => {
|
||||||
const cacheHit = cache.users.find(p => p.id == id);
|
const cacheHit = cache.users.find(p => p.id == id);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue