From 5f0d140bbe2063824b9ca0d980657a24cc8bbc17 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 5 Oct 2023 01:38:18 +0200 Subject: [PATCH] [mastodon-client] GET /v2/suggestions --- packages/backend/src/prelude/array.ts | 8 +++ .../src/server/api/mastodon/converters.ts | 5 ++ .../src/server/api/mastodon/endpoints/misc.ts | 23 +++++++- .../server/api/mastodon/endpoints/search.ts | 57 +------------------ .../server/api/mastodon/entities/account.ts | 5 ++ .../src/server/api/mastodon/helpers/misc.ts | 52 +++++++++++++++++ 6 files changed, 93 insertions(+), 57 deletions(-) diff --git a/packages/backend/src/prelude/array.ts b/packages/backend/src/prelude/array.ts index 71a24c89b..24270038b 100644 --- a/packages/backend/src/prelude/array.ts +++ b/packages/backend/src/prelude/array.ts @@ -52,6 +52,14 @@ export function unique(xs: T[]): T[] { return [...new Set(xs)]; } +export function uniqBy(a: T[], key: Function): T[] { + const seen = new Set(); + return a.filter(function(item) { + const k = key(item); + return seen.has(k) ? false : seen.add(k); + }) +} + export function sum(xs: number[]): number { return xs.reduce((a, b) => a + b, 0); } diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 3ff84327f..ecfaba5a0 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -23,6 +23,11 @@ export function convertListId(list: MastodonEntity.List) { return simpleConvertId(list); } +export function convertSuggestionIds(suggestion: MastodonEntity.SuggestedAccount) { + suggestion.account = convertAccountId(suggestion.account) + return suggestion +} + export function convertNotificationIds(notification: MastodonEntity.Notification) { notification.account = convertAccountId(notification.account); notification.id = convertId(notification.id, IdType.MastodonId); diff --git a/packages/backend/src/server/api/mastodon/endpoints/misc.ts b/packages/backend/src/server/api/mastodon/endpoints/misc.ts index 0f47aa7b9..90edb34d6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/misc.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/misc.ts @@ -2,9 +2,9 @@ import Router from "@koa/router"; import { getClient } from "@/server/api/mastodon/index.js"; import { MiscHelpers } from "@/server/api/mastodon/helpers/misc.js"; import authenticate from "@/server/api/authenticate.js"; -import { argsToBools } from "@/server/api/mastodon/endpoints/timeline.js"; +import { argsToBools, limitToInt } from "@/server/api/mastodon/endpoints/timeline.js"; import { Announcements } from "@/models/index.js"; -import { convertAnnouncementId } from "@/server/api/mastodon/converters.js"; +import { convertAnnouncementId, convertSuggestionIds } from "@/server/api/mastodon/converters.js"; import { convertId, IdType } from "@/misc/convert-id.js"; export function setupEndpointsMisc(router: Router): void { @@ -109,4 +109,23 @@ export function setupEndpointsMisc(router: Router): void { ctx.body = e.response.data; } }); + + router.get("/v2/suggestions", async (ctx) => { + try { + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? undefined; + + if (!user) { + ctx.status = 401; + return; + } + + const args = limitToInt(ctx.query); + ctx.body = await MiscHelpers.getFollowSuggestions(user, args.limit) + .then(p => p.map(x => convertSuggestionIds(x))); + } catch (e: any) { + ctx.status = 500; + ctx.body = { error: e.message }; + } + }); } diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index 630ea777f..113c6c6fb 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -6,6 +6,7 @@ import { convertAccountId, convertSearchIds, convertStatusIds } from "../convert import authenticate from "@/server/api/authenticate.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { SearchHelpers } from "@/server/api/mastodon/helpers/search.js"; +import { MiscHelpers } from "@/server/api/mastodon/helpers/misc.js"; export function setupEndpointsSearch(router: Router): void { router.get("/v1/search", async (ctx) => { @@ -69,29 +70,6 @@ export function setupEndpointsSearch(router: Router): void { ctx.body = e.response.data; } }); - router.get("/v2/suggestions", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.headers.authorization; - try { - const query: any = ctx.query; - let data = await getFeaturedUser( - BASE_URL, - ctx.request.hostname, - accessTokens, - query.limit || 20, - ); - data = data.map((suggestion) => { - suggestion.account = convertAccountId(suggestion.account); - return suggestion; - }); - console.log(data); - ctx.body = data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); } async function getHighlight( @@ -112,35 +90,4 @@ async function getHighlight( console.log(e.response.data); return []; } -} - -async function getFeaturedUser( - BASE_URL: string, - host: string, - accessTokens: string | undefined, - limit: number, -) { - const accessTokenArr = accessTokens?.split(" ") ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - try { - const api = await axios.post(`${BASE_URL}/api/users`, { - i: accessToken, - limit, - origin: "local", - sort: "+follower", - state: "alive", - }); - const data: MisskeyEntity.UserDetail[] = api.data; - console.log(data); - return data.map((u) => { - return { - source: "past_interactions", - account: new Converter(BASE_URL).userDetail(u, host), - }; - }); - } catch (e: any) { - console.log(e); - console.log(e.response.data); - return []; - } -} +} \ No newline at end of file diff --git a/packages/backend/src/server/api/mastodon/entities/account.ts b/packages/backend/src/server/api/mastodon/entities/account.ts index 87f7de019..5c1100190 100644 --- a/packages/backend/src/server/api/mastodon/entities/account.ts +++ b/packages/backend/src/server/api/mastodon/entities/account.ts @@ -29,4 +29,9 @@ namespace MastodonEntity { export type MutedAccount = Account | { mute_expires_at: string | null; } + + export type SuggestedAccount = { + source: "staff" | "past_interactions" | "global", + account: Account + } } diff --git a/packages/backend/src/server/api/mastodon/helpers/misc.ts b/packages/backend/src/server/api/mastodon/helpers/misc.ts index acefa9d4f..1b0c20fd4 100644 --- a/packages/backend/src/server/api/mastodon/helpers/misc.ts +++ b/packages/backend/src/server/api/mastodon/helpers/misc.ts @@ -10,6 +10,12 @@ import { Announcement } from "@/models/entities/announcement.js"; import { ILocalUser } from "@/models/entities/user.js"; import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js"; import { genId } from "@/misc/gen-id.js"; +import * as Acct from "@/misc/acct.js"; +import { User } from "@/models/entities/user.js"; +import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; +import { generateMutedUserQueryForUsers } from "@/server/api/common/generate-muted-user-query.js"; +import { generateBlockQueryForUsers } from "@/server/api/common/generate-block-query.js"; +import { uniqBy } from "@/prelude/array.js"; export class MiscHelpers { public static async getInstance(): Promise { @@ -123,4 +129,50 @@ export class MiscHelpers { }); } } + + public static async getFollowSuggestions(user: ILocalUser, limit: number): Promise { + const cache = UserHelpers.getFreshAccountCache(); + const results: Promise[] = []; + + const pinned = fetchMeta().then(meta => Promise.all( + meta.pinnedUsers + .map((acct) => Acct.parse(acct)) + .map((acct) => + Users.findOneBy({ + usernameLower: acct.username.toLowerCase(), + host: acct.host ?? IsNull(), + })) + ) + .then(p => p.filter(x => !!x) as User[]) + .then(p => UserConverter.encodeMany(p, cache)) + .then(p => p.map(x => { + return {source: "staff", account: x} as MastodonEntity.SuggestedAccount + })) + ); + + const query = Users.createQueryBuilder("user") + .where("user.isExplorable = TRUE") + .andWhere("user.host IS NULL") + .orderBy("user.followersCount", "DESC") + .andWhere("user.updatedAt > :date", { + date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5), + }); + + generateMutedUserQueryForUsers(query, user); + generateBlockQueryForUsers(query, user); + + const global = query + .take(limit) + .getMany() + .then(p => UserConverter.encodeMany(p, cache)) + .then(p => p.map(x => { + return {source: "global", account: x} as MastodonEntity.SuggestedAccount + })); + + results.push(pinned); + results.push(global); + + + return Promise.all(results).then(p => uniqBy(p.flat(), (x: MastodonEntity.SuggestedAccount) => x.account.id).slice(0, limit)); + } } \ No newline at end of file