implement /api/v1/accounts/lookup
Some checks failed
Dockle / dockle (pull_request) Has been cancelled
Lint / pnpm_install (pull_request) Has been cancelled
Lint / lint (backend) (pull_request) Has been cancelled
Lint / lint (frontend) (pull_request) Has been cancelled
Lint / lint (misskey-js) (pull_request) Has been cancelled
Lint / lint (sw) (pull_request) Has been cancelled
Lint / typecheck (backend) (pull_request) Has been cancelled
Lint / typecheck (misskey-js) (pull_request) Has been cancelled
Test (backend) / unit (22.x) (pull_request) Has been cancelled
Test (backend) / e2e (22.x) (pull_request) Has been cancelled
Test (frontend) / vitest (22.x) (pull_request) Has been cancelled
Test (production install and build) / production (22.x) (pull_request) Has been cancelled
Test (backend) / validate-api-json (22.x) (pull_request) Has been cancelled
Pull Request Labeler / triage (pull_request_target) Has been cancelled
Some checks failed
Dockle / dockle (pull_request) Has been cancelled
Lint / pnpm_install (pull_request) Has been cancelled
Lint / lint (backend) (pull_request) Has been cancelled
Lint / lint (frontend) (pull_request) Has been cancelled
Lint / lint (misskey-js) (pull_request) Has been cancelled
Lint / lint (sw) (pull_request) Has been cancelled
Lint / typecheck (backend) (pull_request) Has been cancelled
Lint / typecheck (misskey-js) (pull_request) Has been cancelled
Test (backend) / unit (22.x) (pull_request) Has been cancelled
Test (backend) / e2e (22.x) (pull_request) Has been cancelled
Test (frontend) / vitest (22.x) (pull_request) Has been cancelled
Test (production install and build) / production (22.x) (pull_request) Has been cancelled
Test (backend) / validate-api-json (22.x) (pull_request) Has been cancelled
Pull Request Labeler / triage (pull_request_target) Has been cancelled
This commit is contained in:
parent
613bf64b25
commit
28fad4fd5d
14 changed files with 449 additions and 22 deletions
BIN
packages/backend/assets/transparent.png
Normal file
BIN
packages/backend/assets/transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 B |
|
@ -140,6 +140,7 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
|||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
import { MastodonUserConverterService } from './mastodon/MastodonUserConverterService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
|
@ -422,6 +423,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ApQuestionService,
|
||||
QueueService,
|
||||
|
||||
// Mastodon
|
||||
MastodonUserConverterService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AccountMoveService,
|
||||
|
@ -696,6 +700,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ApQuestionService,
|
||||
QueueService,
|
||||
|
||||
// Mastodon
|
||||
MastodonUserConverterService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
$AccountMoveService,
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||
import { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { UserEntityService, UserRelation } from '../entities/UserEntityService.js';
|
||||
import { RoleService } from '../RoleService.js';
|
||||
import { MfmService } from '../MfmService.js';
|
||||
import { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
import { ApPersonService } from '../activitypub/models/ApPersonService.js';
|
||||
import { IdService } from '../IdService.js';
|
||||
|
||||
export type Account = {
|
||||
id: string;
|
||||
username: string;
|
||||
acct: string;
|
||||
url: string;
|
||||
uri: string; // undocumented
|
||||
display_name: string;
|
||||
note: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
header: string;
|
||||
header_static: string;
|
||||
locked: boolean;
|
||||
fields: Field[];
|
||||
emojis: Emoji[];
|
||||
bot: boolean;
|
||||
group: boolean;
|
||||
discoverable: boolean | null;
|
||||
noindex?: boolean | null;
|
||||
moved?: Account | null;
|
||||
suspended?: true;
|
||||
limited?: true;
|
||||
created_at: string;
|
||||
last_status_at: string | null;
|
||||
statuses_count: number;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
};
|
||||
|
||||
export type MutedAccount = Account | {
|
||||
mute_expires_at: string | null;
|
||||
}
|
||||
|
||||
export type SuggestedAccount = {
|
||||
source: 'staff' | 'past_interactions' | 'global',
|
||||
account: Account
|
||||
}
|
||||
|
||||
export type Emoji = {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
visible_in_picker: boolean;
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
privacy: string | null;
|
||||
sensitive: boolean | null;
|
||||
language: string | null;
|
||||
note: string;
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MastodonUserConverterService {
|
||||
public constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private roleService: RoleService,
|
||||
private mfmService: MfmService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private apPersonService: ApPersonService,
|
||||
private idService: IdService,
|
||||
) {}
|
||||
|
||||
public async encode(u: MiUser, me: MiLocalUser | null): Promise<Account | null> {
|
||||
return await this.#encodeInner(u, me, true);
|
||||
}
|
||||
|
||||
async #encodeInner(u: MiUser, me: MiLocalUser | null, recurse: boolean): Promise<Account | null> {
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: u.id });
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host ?? this.config.host}/@${u.username}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
const meId = me ? me.id : null;
|
||||
const isMe = meId === u.id;
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me) : false;
|
||||
if (!iAmModerator && (u.isDeleted || u.isSuspended)) {
|
||||
return null;
|
||||
}
|
||||
let relation: UserRelation | null = null;
|
||||
if (meId && !isMe) {
|
||||
relation = await this.userEntityService.getRelation(meId, u.id);
|
||||
}
|
||||
const followingCount =
|
||||
(profile.followingVisibility === 'public') || isMe || iAmModerator ? u.followingCount :
|
||||
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? u.followingCount :
|
||||
0;
|
||||
const followersCount =
|
||||
(profile.followersVisibility === 'public') || isMe || iAmModerator ? u.followersCount :
|
||||
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? u.followersCount :
|
||||
0;
|
||||
const avatarUrl = u.avatarUrl ?? this.userEntityService.getIdenticonUrl(u);
|
||||
const bannerUrl = u.bannerUrl ?? `${this.config.url}/static-assets/transparent.png`;
|
||||
const parsedDescription = mfm.parse(profile.description ?? '');
|
||||
const parsedFields = profile.fields.map(({ name, value }) => ({ name, value, parsed: mfm.parse(value) }));
|
||||
const mentions = await this.#extractMentions([...parsedDescription, ...parsedFields.flatMap(field => field.parsed)]);
|
||||
const verifiedLinks = new Set(profile.verifiedLinks);
|
||||
const fields = parsedFields.map(({ name, value, parsed }) => {
|
||||
let verifiedAt: string | null = null;
|
||||
if (verifiedLinks.has(value)) {
|
||||
verifiedAt = new Date().toISOString();
|
||||
}
|
||||
return {
|
||||
name,
|
||||
value: this.mfmService.toHtml(parsed, mentions) ?? '',
|
||||
verified_at: verifiedAt,
|
||||
};
|
||||
});
|
||||
const emojis = Object.entries(await this.customEmojiService.populateEmojis(u.emojis, u.host)).map(([shortcode, url]) => ({
|
||||
shortcode,
|
||||
url,
|
||||
static_url: url,
|
||||
visible_in_picker: true,
|
||||
}));
|
||||
const lastStatus = await this.notesRepository.createQueryBuilder()
|
||||
.where({ userId: u.id })
|
||||
.orderBy('id', 'DESC')
|
||||
.select('id')
|
||||
.getOne();
|
||||
const lastStatusAt = lastStatus ? this.idService.parse(lastStatus.id).date.toDateString() : null;
|
||||
let moved = undefined;
|
||||
if (recurse && u.movedToUri) {
|
||||
moved = await this.#encodeInner(await this.apPersonService.resolvePerson(u.movedToUri), me, false);
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct,
|
||||
display_name: u.name ?? u.username,
|
||||
locked: u.isLocked,
|
||||
bot: u.isBot,
|
||||
discoverable: u.isExplorable,
|
||||
group: false, // unimplemented
|
||||
created_at: u.createdAt.toISOString(),
|
||||
note: this.mfmService.toHtml(parsedDescription, mentions) ?? '',
|
||||
url: profile.url ?? acctUrl,
|
||||
uri: u.uri ?? acctUrl,
|
||||
avatar: avatarUrl,
|
||||
avatar_static: avatarUrl,
|
||||
header: bannerUrl,
|
||||
header_static: bannerUrl,
|
||||
followers_count: followersCount || 0,
|
||||
following_count: followingCount || 0,
|
||||
statuses_count: u.notesCount,
|
||||
noindex: profile.noCrawle,
|
||||
emojis,
|
||||
last_status_at: lastStatusAt,
|
||||
suspended: u.isSuspended || undefined,
|
||||
fields,
|
||||
moved,
|
||||
};
|
||||
}
|
||||
|
||||
async #extractMentions(nodes: mfm.MfmNode[]): Promise<IMentionedRemoteUsers> {
|
||||
const mentions = extractMentions(nodes).filter(mention => mention.host !== null);
|
||||
if (mentions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let query = this.usersRepository.createQueryBuilder('user');
|
||||
for (const { username, host } of mentions) {
|
||||
query = query.orWhere(
|
||||
'("user"."usernameLower" = :username AND host = :host)',
|
||||
{ username: username.toLowerCase(), host: host?.toLowerCase() },
|
||||
);
|
||||
}
|
||||
return await query
|
||||
.innerJoinAndSelect(this.userProfilesRepository.metadata.targetName, 'profile', '"user".id = profile."userId"')
|
||||
.select(['uri', 'url', 'username', 'host'])
|
||||
.getRawMany();
|
||||
}
|
||||
}
|
|
@ -46,10 +46,12 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
|||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||
import { MastodonEndpointsModule } from './api/MastodonEndpointsModule.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
EndpointsModule,
|
||||
MastodonEndpointsModule,
|
||||
CoreModule,
|
||||
],
|
||||
providers: [
|
||||
|
|
|
@ -19,10 +19,11 @@ import { bindThis } from '@/decorators.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { ApiError, MastodonApiError } from './error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import { IMastodonEndpoint } from './mastodon-endpoints.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { IEndpointMeta, IEndpoint } from './endpoints.js';
|
||||
|
@ -182,6 +183,32 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async handleMastodonRequest(
|
||||
endpoint: IMastodonEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: null;
|
||||
const [user] = await this.authenticateService.authenticate(token);
|
||||
let res;
|
||||
try {
|
||||
res = await endpoint.exec(body, user, token, null, request.ip, request.headers);
|
||||
} catch (e) {
|
||||
if (e instanceof MastodonApiError) {
|
||||
reply.status(e.httpStatusCode).send({ error: e.message });
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
reply.send(res);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private send(reply: FastifyReply, x?: any, y?: ApiError) {
|
||||
if (x == null) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import endpoints from './endpoints.js';
|
|||
import { ApiCallService } from './ApiCallService.js';
|
||||
import { SignupApiService } from './SignupApiService.js';
|
||||
import { SigninApiService } from './SigninApiService.js';
|
||||
import { IMastodonEndpoint, mastodonEndpoints } from './mastodon-endpoints.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
|
@ -105,6 +106,32 @@ export class ApiServerService {
|
|||
}
|
||||
}
|
||||
|
||||
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
|
||||
name: endpoint.name,
|
||||
method: endpoint.method,
|
||||
meta: endpoint.meta,
|
||||
params: endpoint.params,
|
||||
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
|
||||
});
|
||||
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
|
||||
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
|
||||
for (const { name, endpoints } of groupedMastodonEndpoints) {
|
||||
fastify.all<{
|
||||
Params: { endpoint: string; },
|
||||
Body: Record<string, unknown>,
|
||||
Querystring: Record<string, unknown>,
|
||||
}>('/' + name, async (request, reply) => {
|
||||
const ep = endpoints.get(request.method);
|
||||
if (!ep) {
|
||||
reply.code(405);
|
||||
reply.send();
|
||||
return;
|
||||
}
|
||||
await this.apiCallService.handleMastodonRequest(ep, request, reply);
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
|
||||
fastify.post<{
|
||||
Body: {
|
||||
username: string;
|
||||
|
|
19
packages/backend/src/server/api/MastodonEndpointsModule.ts
Normal file
19
packages/backend/src/server/api/MastodonEndpointsModule.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Module, Provider } from '@nestjs/common';
|
||||
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
||||
|
||||
const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default };
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CoreModule,
|
||||
],
|
||||
providers: [
|
||||
$accounts_lookup_v1_get,
|
||||
],
|
||||
exports: [
|
||||
$accounts_lookup_v1_get,
|
||||
],
|
||||
})
|
||||
export class MastodonEndpointsModule {}
|
22
packages/backend/src/server/api/ajv.ts
Normal file
22
packages/backend/src/server/api/ajv.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import _Ajv from 'ajv';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
|
||||
const ajv = new Ajv({
|
||||
useDefaults: true,
|
||||
});
|
||||
|
||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
ajv.addFormat('url', {
|
||||
type: 'string',
|
||||
validate: (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export { ajv };
|
|
@ -4,32 +4,13 @@
|
|||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import _Ajv from 'ajv';
|
||||
import type { Schema, SchemaType } from '@/misc/json-schema.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import type { MiAccessToken } from '@/models/AccessToken.js';
|
||||
import { ajv } from './ajv.js';
|
||||
import { ApiError } from './error.js';
|
||||
import type { IEndpointMeta } from './endpoints.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
|
||||
const ajv = new Ajv({
|
||||
useDefaults: true,
|
||||
});
|
||||
|
||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||
ajv.addFormat('url', {
|
||||
type: 'string',
|
||||
validate: (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type Response = Record<string, any> | void;
|
||||
|
||||
type File = {
|
||||
|
|
|
@ -23,3 +23,15 @@ export class ApiError extends Error {
|
|||
this.info = info ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class MastodonApiError extends Error {
|
||||
constructor(message: string | undefined, public httpStatusCode: number) {
|
||||
super(message ?? 'no error message available');
|
||||
}
|
||||
}
|
||||
|
||||
export class MastodonNotFoundError extends MastodonApiError {
|
||||
constructor() {
|
||||
super('Record not found', 404);
|
||||
}
|
||||
}
|
||||
|
|
39
packages/backend/src/server/api/mastodon-endpoint-base.ts
Normal file
39
packages/backend/src/server/api/mastodon-endpoint-base.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { Schema, SchemaType } from '@/misc/json-schema.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
import { IMastodonEndpointMeta } from './mastodon-endpoints.js';
|
||||
import { ajv } from './ajv.js';
|
||||
import { MastodonApiError } from './error.js';
|
||||
|
||||
type Executor<T extends IMastodonEndpointMeta, Ps extends Schema> =
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null) => Promise<unknown>;
|
||||
|
||||
export abstract class MastodonEndpoint<T extends IMastodonEndpointMeta, Ps extends Schema> {
|
||||
public exec: (
|
||||
params: SchemaType<Ps>,
|
||||
user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null,
|
||||
token: MiAccessToken | null,
|
||||
file?: File,
|
||||
ip?: string | null,
|
||||
headers?: Record<string, string> | null,
|
||||
) => Promise<unknown>;
|
||||
|
||||
constructor(meta: T, paramDef: Ps, exec: (
|
||||
params: SchemaType<Ps>,
|
||||
user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null,
|
||||
token: MiAccessToken | null,
|
||||
file?: File,
|
||||
ip?: string | null,
|
||||
headers?: Record<string, string> | null,
|
||||
) => Promise<unknown>) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
this.exec = (params, user, token, file, ip, headers) => {
|
||||
const valid = validate(params);
|
||||
if (!valid) {
|
||||
const error = validate.errors![0];
|
||||
throw new MastodonApiError(`${error.schemaPath}: ${error.message}`, 400);
|
||||
}
|
||||
return exec(params, user, token, file, ip, headers);
|
||||
};
|
||||
}
|
||||
}
|
30
packages/backend/src/server/api/mastodon-endpoints.ts
Normal file
30
packages/backend/src/server/api/mastodon-endpoints.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Schema } from '@/misc/json-schema.js';
|
||||
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
|
||||
|
||||
const eps = [
|
||||
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
|
||||
];
|
||||
|
||||
export interface IMastodonEndpointMeta {
|
||||
readonly requireCredential: boolean;
|
||||
}
|
||||
|
||||
export interface IMastodonEndpoint {
|
||||
name: string;
|
||||
method: string;
|
||||
meta: IMastodonEndpointMeta;
|
||||
params: Schema;
|
||||
}
|
||||
|
||||
export const mastodonEndpoints: IMastodonEndpoint[] = (eps as [string, string, any]).map(([method, name, ep]) => {
|
||||
return {
|
||||
name,
|
||||
method,
|
||||
get meta() {
|
||||
return ep.meta;
|
||||
},
|
||||
get params() {
|
||||
return ep.schema;
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { MastodonEndpoint } from '@/server/api/mastodon-endpoint-base.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { MastodonUserConverterService } from '@/core/mastodon/MastodonUserConverterService.js';
|
||||
import { MastodonNotFoundError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
acct: { type: 'string', minLength: 1 },
|
||||
},
|
||||
required: ['acct'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends MastodonEndpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private mastodonUserConverterService: MastodonUserConverterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const { username, host } = Acct.parse(ps.acct);
|
||||
let user;
|
||||
try {
|
||||
user = await this.remoteUserResolveService.resolveUser(username, host);
|
||||
} catch (e) {
|
||||
throw new MastodonNotFoundError();
|
||||
}
|
||||
const encoded = await this.mastodonUserConverterService.encode(user, me);
|
||||
if (!encoded) {
|
||||
throw new MastodonNotFoundError();
|
||||
}
|
||||
return encoded;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import * as assert from 'assert';
|
|||
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||
import { Blob } from 'node-fetch';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
import { api, initTestDb, post, sendEnvUpdateRequest, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import { api, initTestDb, post, relativeFetch, sendEnvUpdateRequest, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Endpoints', () => {
|
||||
|
@ -1062,4 +1062,16 @@ describe('Endpoints', () => {
|
|||
assert.strictEqual(resCarol.body.memo, memoCarolToBob);
|
||||
});
|
||||
});
|
||||
|
||||
describe('v1/accounts/lookup', () => {
|
||||
test('regular usage', async () => {
|
||||
const response: any = await (await relativeFetch('api/v1/accounts/lookup?acct=alice')).json();
|
||||
assert.strictEqual(response.username, 'alice');
|
||||
});
|
||||
|
||||
test('404', async () => {
|
||||
const response = await relativeFetch('api/v1/accounts/lookup?acct=fourohfour');
|
||||
assert.strictEqual(response.status, 404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue