[mastodon-client] GET /v2/suggestions

This commit is contained in:
Laura Hausmann 2023-10-05 01:38:18 +02:00
parent 44b72a2ecc
commit 5f0d140bbe
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
6 changed files with 93 additions and 57 deletions

View file

@ -52,6 +52,14 @@ export function unique<T>(xs: T[]): T[] {
return [...new Set(xs)];
}
export function uniqBy<T>(a: T[], key: Function): T[] {
const seen = new Set<any>();
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);
}

View file

@ -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);

View file

@ -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 };
}
});
}

View file

@ -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 [];
}
}
}

View file

@ -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
}
}

View file

@ -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<MastodonEntity.Instance> {
@ -123,4 +129,50 @@ export class MiscHelpers {
});
}
}
public static async getFollowSuggestions(user: ILocalUser, limit: number): Promise<MastodonEntity.SuggestedAccount[]> {
const cache = UserHelpers.getFreshAccountCache();
const results: Promise<MastodonEntity.SuggestedAccount[]>[] = [];
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));
}
}