diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index fd802b9a7..34c06d094 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -13,6 +13,7 @@ import { MetaService } from '@/core/MetaService.js'; import multer from 'fastify-multer'; import { apiAuthMastodon } from './endpoints/auth.js'; import { apiAccountMastodon } from './endpoints/account.js'; +import { apiSearchMastodon } from './endpoints/search.js'; const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url)); @@ -549,6 +550,64 @@ export class MastodonApiServerService { reply.code(401).send(e.response.data); } }); + //#endregion + + //#region Search + fastify.get("/v1/search", async (_request, reply) => { + const BASE_URL = `${_request.protocol}://${_request.hostname}`; + const accessTokens = _request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const search = new apiSearchMastodon(_request, client, BASE_URL); + reply.send(await search.SearchV1()); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + reply.code(401).send(e.response.data); + } + }); + + fastify.get("/v2/search", async (_request, reply) => { + const BASE_URL = `${_request.protocol}://${_request.hostname}`; + const accessTokens = _request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const search = new apiSearchMastodon(_request, client, BASE_URL); + reply.send(await search.SearchV2()); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + reply.code(401).send(e.response.data); + } + }); + + fastify.get("/v1/trends/statuses", async (_request, reply) => { + const BASE_URL = `${_request.protocol}://${_request.hostname}`; + const accessTokens = _request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const search = new apiSearchMastodon(_request, client, BASE_URL); + reply.send(await search.getStatusTrends()); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + reply.code(401).send(e.response.data); + } + }); + + fastify.get("/v2/suggestions", async (_request, reply) => { + const BASE_URL = `${_request.protocol}://${_request.hostname}`; + const accessTokens = _request.headers.authorization; + const client = getClient(BASE_URL, accessTokens); + try { + const search = new apiSearchMastodon(_request, client, BASE_URL); + reply.send(await search.getSuggestions()); + } catch (e: any) { + console.error(e); + console.error(e.response.data); + reply.code(401).send(e.response.data); + } + }); //#endregion done(); } diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts new file mode 100644 index 000000000..d55831640 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -0,0 +1,132 @@ +import type { MegalodonInterface } from "megalodon"; +import { Converter } from "megalodon"; +import type { FastifyRequest } from 'fastify'; +import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; +import { convertAccount, convertStatus } from '../converters.js'; + +async function getHighlight( + BASE_URL: string, + domain: string, + accessTokens: string | undefined, +) { + const accessTokenArr = accessTokens?.split(" ") ?? [null]; + const accessToken = accessTokenArr[accessTokenArr.length - 1]; + try { + + const apicall = await fetch(`${BASE_URL}/api/notes/featured`, + { + method: 'POST', + headers: { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({i: accessToken}) + }); + const api = await apicall.json(); + const data: MisskeyEntity.Note[] = api; + return data.map((note) => Converter.note(note, domain)); + } catch (e: any) { + console.log(e); + 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 apicall = await fetch(`${BASE_URL}/api/users`, + { + method: 'POST', + headers: { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({i: accessToken, limit, origin: "local", sort: "+follower", state: "alive"}) + }); + const api = await apicall.json(); + const data: MisskeyEntity.UserDetail[] = api; + return data.map((u) => { + return { + source: "past_interactions", + account: Converter.userDetail(u, host), + }; + }); + } catch (e: any) { + console.log(e); + console.log(e.response.data); + return []; + } +} +export class apiSearchMastodon { + private request: FastifyRequest; + private client: MegalodonInterface; + private BASE_URL: string; + + constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string) { + this.request = request; + this.client = client; + this.BASE_URL = BASE_URL; + } + + public async SearchV1() { + try { + const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any)); + const type = query.type || ""; + const data = await this.client.search(query.q, { type: type, ...query }); + return data.data; + } catch (e: any) { + console.error(e); + return e.response.data; + } + } + + public async SearchV2() { + try { + const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any)); + const type = query.type; + const acct = !type || type === "accounts" ? await this.client.search(query.q, { type: "accounts", ...query }) : null; + const stat = !type || type === "statuses" ? await this.client.search(query.q, { type: "statuses", ...query }) : null; + const tags = !type || type === "hashtags" ? await this.client.search(query.q, { type: "hashtags", ...query }) : null; + const data = { + accounts: acct?.data.accounts.map((account) => convertAccount(account)) ?? [], + statuses: stat?.data.statuses.map((status) => convertStatus(status)) ?? [], + hashtags: tags?.data.hashtags ?? [] + }; + return data; + } catch (e: any) { + console.error(e); + return e.response.data; + } + } + + public async getStatusTrends() { + try { + const data = await getHighlight( + this.BASE_URL, + this.request.hostname, + this.request.headers.authorization, + ); + return data.map((status) => convertStatus(status)); + } catch (e: any) { + console.error(e); + return e.response.data; + } + } + + public async getSuggestions() { + try { + const data = await getFeaturedUser( + this.BASE_URL, + this.request.hostname, + this.request.headers.authorization, + (this.request.query as any).limit || 20, + ); + return data.map((suggestion) => { suggestion.account = convertAccount(suggestion.account); return suggestion; }); + } catch (e: any) { + console.error(e); + return e.response.data; + } + } +} \ No newline at end of file