Merge branch 'develop' into ed25519

This commit is contained in:
tamaina
2024-06-11 14:32:54 +09:00
316 changed files with 12361 additions and 1693 deletions

View File

@@ -53,7 +53,7 @@ export class FileServerService {
private internalStorageService: InternalStorageService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray', false);
this.logger = this.loggerService.getLogger('server', 'gray');
//this.createServer = this.createServer.bind(this);
}

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import { readyRef } from '@/boot/ready.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
import type { MeiliSearch } from 'meilisearch';
@Injectable()
export class HealthServerService {
constructor(
@Inject(DI.redis)
private redis: Redis.Redis,
@Inject(DI.redisForPub)
private redisForPub: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.meilisearch)
private meilisearch: MeiliSearch | null,
) {}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/', async (request, reply) => {
reply.code(await Promise.all([
new Promise<void>((resolve, reject) => readyRef.value ? resolve() : reject()),
this.redis.ping(),
this.redisForPub.ping(),
this.redisForSub.ping(),
this.redisForTimelines.ping(),
this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503));
reply.header('Cache-Control', 'no-store');
});
done();
}
}

View File

@@ -115,6 +115,7 @@ export class NodeinfoServerService {
langs: meta.langs,
tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
inquiryUrl: meta.inquiryUrl,
impressumUrl: meta.impressumUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,

View File

@@ -8,6 +8,7 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
@@ -55,6 +56,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ClientServerService,
ClientLoggerService,
FeedService,
HealthServerService,
UrlPreviewService,
ActivityPubServerService,
FileServerService,

View File

@@ -28,6 +28,7 @@ import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@@ -61,12 +62,13 @@ export class ServerService implements OnApplicationShutdown {
private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService,
private healthServerService: HealthServerService,
private clientServerService: ClientServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
) {
this.logger = this.loggerService.getLogger('server', 'gray', false);
this.logger = this.loggerService.getLogger('server', 'gray');
}
@bindThis
@@ -108,6 +110,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;

View File

@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
@@ -88,6 +93,51 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
#onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
const errId = randomUUID();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
});
if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
level: 'error',
user: {
id: userId,
},
extra: {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
},
});
}
throw new ApiError(null, {
e: {
message: err.message,
code: err.name,
id: errId,
},
});
}
}
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
@@ -362,31 +412,15 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
const errId = randomUUID();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
});
console.error(err, errId);
throw new ApiError(null, {
e: {
message: err.message,
code: err.name,
id: errId,
},
});
}
});
if (this.config.sentryForBackend) {
return await Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
} else {
return await ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
}
}
@bindThis

View File

@@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
isSuspended: false,
suspensionState: 'none',
},
});

View File

@@ -6,8 +6,13 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -82,7 +87,13 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
@@ -380,6 +391,11 @@ import type { Provider } from '@nestjs/common';
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
@@ -454,7 +470,13 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
@@ -756,6 +778,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
ApiLoggerService,
$admin_meta,
$admin_abuseUserReports,
$admin_abuseReport_notificationRecipient_list,
$admin_abuseReport_notificationRecipient_show,
$admin_abuseReport_notificationRecipient_create,
$admin_abuseReport_notificationRecipient_update,
$admin_abuseReport_notificationRecipient_delete,
$admin_accounts_create,
$admin_accounts_delete,
$admin_accounts_findByEmail,
@@ -830,7 +857,13 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$admin_systemWebhook_create,
$admin_systemWebhook_delete,
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
@@ -1126,6 +1159,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
exports: [
$admin_meta,
$admin_abuseUserReports,
$admin_abuseReport_notificationRecipient_list,
$admin_abuseReport_notificationRecipient_show,
$admin_abuseReport_notificationRecipient_create,
$admin_abuseReport_notificationRecipient_update,
$admin_abuseReport_notificationRecipient_delete,
$admin_accounts_create,
$admin_accounts_delete,
$admin_accounts_findByEmail,
@@ -1200,7 +1238,13 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$admin_systemWebhook_create,
$admin_systemWebhook_delete,
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$announcements,
$announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,

View File

@@ -29,13 +29,13 @@ export class SigninService {
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
// Append signin history
const record = await this.signinsRepository.insert({
const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
ip: request.ip,
headers: request.headers as any,
success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
});
// Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));

View File

@@ -183,13 +183,13 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
const pendingUser = await this.userPendingsRepository.insert({
const pendingUser = await this.userPendingsRepository.insertOne({
id: this.idService.gen(),
code,
email: emailAddress!,
username: username,
password: hash,
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
});
const link = `${this.config.url}/signup-complete/${code}`;

View File

@@ -6,8 +6,18 @@
import { permissions } from 'misskey-js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseReport_notificationRecipient_list
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
import * as ep___admin_abuseReport_notificationRecipient_show
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
import * as ep___admin_abuseReport_notificationRecipient_create
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
import * as ep___admin_abuseReport_notificationRecipient_update
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
import * as ep___admin_abuseReport_notificationRecipient_delete
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
@@ -44,7 +54,8 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata
from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
@@ -82,7 +93,13 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
@@ -378,6 +395,11 @@ import * as ep___reversi_verify from './endpoints/reversi/verify.js';
const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete],
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
@@ -452,7 +474,13 @@ const eps = [
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
['admin/system-webhook/create', ep___admin_systemWebhook_create],
['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
['antennas/list', ep___antennas_list],
@@ -871,8 +899,12 @@ export interface IEndpoint {
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
return {
name: name,
get meta() { return ep.meta ?? {}; },
get params() { return ep.paramDef; },
get meta() {
return ep.meta ?? {};
},
get params() {
return ep.paramDef;
},
};
});

View File

@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { DI } from '@/di-symbols.js';
import type { UserProfilesRepository } from '@/models/_.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
correlationCheckEmail: {
message: 'If "method" is email, "userId" must be set.',
code: 'CORRELATION_CHECK_EMAIL',
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
httpStatusCode: 400,
},
correlationCheckWebhook: {
message: 'If "method" is webhook, "systemWebhookId" must be set.',
code: 'CORRELATION_CHECK_WEBHOOK',
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
httpStatusCode: 400,
},
emailAddressNotSet: {
message: 'Email address is not set.',
code: 'EMAIL_ADDRESS_NOT_SET',
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
httpStatusCode: 400,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
method: {
type: 'string',
enum: ['email', 'webhook'],
},
userId: {
type: 'string',
format: 'misskey:id',
},
systemWebhookId: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'isActive',
'name',
'method',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.method === 'email') {
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
if (!ps.userId || !userProfile) {
throw new ApiError(meta.errors.correlationCheckEmail);
}
if (!userProfile.email || !userProfile.emailVerified) {
throw new ApiError(meta.errors.emailAddressNotSet);
}
}
if (ps.method === 'webhook' && !ps.systemWebhookId) {
throw new ApiError(meta.errors.correlationCheckWebhook);
}
const userId = ps.method === 'email' ? ps.userId : null;
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
const result = await this.abuseReportNotificationService.createRecipient(
{
isActive: ps.isActive,
name: ps.name,
method: ps.method,
userId: userId ?? null,
systemWebhookId: systemWebhookId ?? null,
},
me,
);
return this.abuseReportNotificationRecipientEntityService.pack(result);
});
}
}

View File

@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.abuseReportNotificationService.deleteRecipient(
ps.id,
me,
);
});
}
}

View File

@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:abuse-report:notification-recipient',
res: {
type: 'array',
items: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
method: {
type: 'array',
items: {
type: 'string',
enum: ['email', 'webhook'],
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps) => {
const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method });
return this.abuseReportNotificationRecipientEntityService.packMany(recipients);
});
}
}

View File

@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
noSuchRecipient: {
message: 'No such recipient.',
code: 'NO_SUCH_RECIPIENT',
id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps) => {
const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] });
if (recipients.length === 0) {
throw new ApiError(meta.errors.noSuchRecipient);
}
return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]);
});
}
}

View File

@@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { DI } from '@/di-symbols.js';
import type { UserProfilesRepository } from '@/models/_.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
correlationCheckEmail: {
message: 'If "method" is email, "userId" must be set.',
code: 'CORRELATION_CHECK_EMAIL',
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
httpStatusCode: 400,
},
correlationCheckWebhook: {
message: 'If "method" is webhook, "systemWebhookId" must be set.',
code: 'CORRELATION_CHECK_WEBHOOK',
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
httpStatusCode: 400,
},
emailAddressNotSet: {
message: 'Email address is not set.',
code: 'EMAIL_ADDRESS_NOT_SET',
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
httpStatusCode: 400,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
method: {
type: 'string',
enum: ['email', 'webhook'],
},
userId: {
type: 'string',
format: 'misskey:id',
},
systemWebhookId: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
'isActive',
'name',
'method',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.method === 'email') {
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
if (!ps.userId || !userProfile) {
throw new ApiError(meta.errors.correlationCheckEmail);
}
if (!userProfile.email || !userProfile.emailVerified) {
throw new ApiError(meta.errors.emailAddressNotSet);
}
}
if (ps.method === 'webhook' && !ps.systemWebhookId) {
throw new ApiError(meta.errors.correlationCheckWebhook);
}
const userId = ps.method === 'email' ? ps.userId : null;
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
const result = await this.abuseReportNotificationService.updateRecipient(
{
id: ps.id,
isActive: ps.isActive,
name: ps.name,
method: ps.method,
userId: userId ?? null,
systemWebhookId: systemWebhookId ?? null,
},
me,
);
return this.abuseReportNotificationRecipientEntityService.pack(result);
});
}
}

View File

@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const ad = await this.adsRepository.insert({
const ad = await this.adsRepository.insertOne({
id: this.idService.gen(),
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
});
this.moderationLogService.log(me, 'createAd', {
adId: ad.id,

View File

@@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
const isSuspendedBefore = instance.suspensionState !== 'none';
let suspensionState: undefined | 'manuallySuspended' | 'none';
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
}
await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended,
suspensionState,
moderationNote: ps.moderationNote,
});
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,

View File

@@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const ticketsPromises = [];
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insert({
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
}));
}
const tickets = await Promise.all(ticketsPromises);

View File

@@ -427,6 +427,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
inquiryUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
@@ -513,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
super(meta, paramDef, async (ps, me) => {
const deliverJobCounts = await this.deliverQueue.getJobCounts();

View File

@@ -5,12 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { QueueService } from '@/core/QueueService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import type { AbuseUserReportsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '@/server/api/error.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
export const meta = {
tags: ['admin'],
@@ -18,6 +16,16 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
errors: {
noSuchAbuseReport: {
message: 'No such abuse report.',
code: 'NO_SUCH_ABUSE_REPORT',
id: 'ac3794dd-2ce4-d878-e546-73c60c06b398',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
@@ -29,47 +37,20 @@ export const paramDef = {
required: ['reportId'],
} as const;
// TODO: ロジックをサービスに切り出す
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
if (report == null) {
throw new Error('report not found');
if (!report) {
throw new ApiError(meta.errors.noSuchAbuseReport);
}
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
}
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: me.id,
forwarded: ps.forward && report.targetUserHost != null,
});
this.moderationLogService.log(me, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost != null,
});
await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
});
}
}

View File

@@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});

View File

@@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:show-users',
kind: 'read:admin:show-user',
res: {
type: 'array',

View File

@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
url: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
},
required: [
'isActive',
'name',
'on',
'url',
'secret',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.systemWebhookService.createSystemWebhook(
{
isActive: ps.isActive,
name: ps.name,
on: ps.on,
url: ps.url,
secret: ps.secret,
},
me,
);
return this.systemWebhookEntityService.pack(result);
});
}
}

View File

@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
) {
super(meta, paramDef, async (ps, me) => {
await this.systemWebhookService.deleteSystemWebhook(
ps.id,
me,
);
});
}
}

View File

@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'array',
items: {
type: 'object',
ref: 'SystemWebhook',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps) => {
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({
isActive: ps.isActive,
on: ps.on,
});
return this.systemWebhookEntityService.packMany(webhooks);
});
}
}

View File

@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
errors: {
noSuchSystemWebhook: {
message: 'No such SystemWebhook.',
code: 'NO_SUCH_SYSTEM_WEBHOOK',
id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps) => {
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] });
if (webhooks.length === 0) {
throw new ApiError(meta.errors.noSuchSystemWebhook);
}
return this.systemWebhookEntityService.pack(webhooks[0]);
});
}
}

View File

@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
url: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
},
required: [
'id',
'isActive',
'name',
'on',
'url',
'secret',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.systemWebhookService.updateSystemWebhook(
{
id: ps.id,
isActive: ps.isActive,
name: ps.name,
on: ps.on,
url: ps.url,
secret: ps.secret,
},
me,
);
return this.systemWebhookEntityService.pack(result);
});
}
}

View File

@@ -107,6 +107,7 @@ export const paramDef = {
feedbackUrl: { type: 'string', nullable: true },
impressumUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
inquiryUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true },
@@ -422,6 +423,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
if (ps.inquiryUrl !== undefined) {
set.inquiryUrl = ps.inquiryUrl;
}
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}

View File

@@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { DI } from '@/di-symbols.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
import type { AnnouncementsRepository } from '@/models/_.js';
export const meta = {
tags: ['meta'],
@@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
private queryService: QueryService,
private announcementService: AnnouncementService,
private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
@@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const announcements = await query.limit(ps.limit).getMany();
return this.announcementService.packMany(announcements, me);
return this.announcementEntityService.packMany(announcements, me);
});
}
}

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { EntityNotFoundError } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['meta'],
requireCredential: false,
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Announcement',
},
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
announcementId: { type: 'string', format: 'misskey:id' },
},
required: ['announcementId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
try {
return await this.announcementService.getAnnouncement(ps.announcementId, me);
} catch (err) {
if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
throw err;
}
});
}
}

View File

@@ -67,9 +67,8 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@Injectable()
@@ -113,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const now = new Date();
const antenna = await this.antennasRepository.insert({
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
@@ -128,8 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
notify: ps.notify,
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);

View File

@@ -66,7 +66,6 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
required: ['antennaId'],
} as const;
@@ -124,7 +123,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
notify: ps.notify,
isActive: true,
lastUsedAt: new Date(),
});

View File

@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
// Create account
const app = await this.appsRepository.insert({
const app = await this.appsRepository.insertOne({
id: this.idService.gen(),
userId: me ? me.id : null,
name: ps.name,
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission,
callbackUrl: ps.callbackUrl,
secret: secret,
}).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.appEntityService.pack(app, null, {
detail: true,

View File

@@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const token = randomUUID();
// Create session token document
const doc = await this.authSessionsRepository.insert({
const doc = await this.authSessionsRepository.insertOne({
id: this.idService.gen(),
appId: app.id,
token: token,
}).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0]));
});
return {
token: doc.token,

View File

@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const channel = await this.channelsRepository.insert({
const channel = await this.channelsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
@@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}),
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
} as MiChannel);
return await this.channelEntityService.pack(channel, me);
});

View File

@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(),
});
return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
return await this.driveFileEntityService.packMany(files, { self: true });
});
}
}

View File

@@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create folder
const folder = await this.driveFoldersRepository.insert({
const folder = await this.driveFoldersRepository.insertOne({
id: this.idService.gen(),
name: ps.name,
parentId: parent !== null ? parent.id : null,
userId: me.id,
}).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0]));
});
const folderObj = await this.driveFolderEntityService.pack(folder);

View File

@@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.suspended === 'boolean') {
if (ps.suspended) {
query.andWhere('instance.isSuspended = TRUE');
query.andWhere('instance.suspensionState != \'none\'');
} else {
query.andWhere('instance.isSuspended = FALSE');
query.andWhere('instance.suspensionState = \'none\'');
}
}

View File

@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.insert({
const flash = await this.flashsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
updatedAt: new Date(),
@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
script: ps.script,
permissions: ps.permissions,
visibility: ps.visibility,
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.flashEntityService.pack(flash);
});

View File

@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
return await this.followRequestEntityService.packMany(requests, me);
});
}
}

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error();
}
const post = await this.galleryPostsRepository.insert(new MiGalleryPost({
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id,
isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id),
})).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.galleryPostEntityService.pack(post, me);
});

View File

@@ -7,7 +7,7 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
'COUNT', limit);
let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
if (notificationsRes.length === 0) {
return [];
}
let notifications: MiNotification[];
for (;;) {
let notificationsRes: [id: string, fields: string[]][];
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
if (sinceTime && !untilTime) {
notificationsRes = await this.redisClient.xrange(
`notificationTimeline:${me.id}`,
'(' + sinceTime,
'+',
'COUNT', ps.limit);
} else {
notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
untilTime ? '(' + untilTime : '+',
sinceTime ? '(' + sinceTime : '-',
'COUNT', ps.limit);
}
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
} else if (excludeTypes && excludeTypes.length > 0) {
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
if (notificationsRes.length === 0) {
return [];
}
if (notifications.length === 0) {
return [];
notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
} else if (excludeTypes && excludeTypes.length > 0) {
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
if (notifications.length !== 0) {
// 通知が1件以上ある場合は返す
break;
}
// フィルタしたことで通知が0件になった場合、次のページを取得する
if (ps.sinceId && !ps.untilId) {
sinceTime = notificationsRes[notificationsRes.length - 1][0];
} else {
untilTime = notificationsRes[notificationsRes.length - 1][0];
}
}
// Mark all as read

View File

@@ -498,26 +498,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
const html = await this.httpRequestService.getHtml(url);
try {
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
const doc = window.document;
const { window } = new JSDOM(html);
const doc = window.document;
const myLink = `${this.config.url}/@${user.username}`;
const myLink = `${this.config.url}/@${user.username}`;
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
}
window.close();
} catch (err) {
// なにもしない
}
}
}

View File

@@ -89,14 +89,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyWebhooks);
}
const webhook = await this.webhooksRepository.insert({
const webhook = await this.webhooksRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
}).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('webhookCreated', webhook);

View File

@@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const ticket = await this.registrationTicketsRepository.insert({
const ticket = await this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.inviteCodeEntityService.pack(ticket, me);
});

View File

@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
excludeChannels: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.setParameters(mutingQuery.getParameters());
//#endregion
//#region exclude channels
if (ps.excludeChannels) {
query.andWhere('poll.channelId IS NULL');
}
//#endregion
const polls = await query
.orderBy('poll.noteId', 'DESC')
.limit(ps.limit)

View File

@@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create vote
const vote = await this.pollVotesRepository.insert({
const vote = await this.pollVotesRepository.insertOne({
id: this.idService.gen(createdAt.getTime()),
noteId: note.id,
userId: me.id,
choice: ps.choice,
}).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0]));
});
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany();
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
return await this.noteReactionEntityService.packMany(reactions, me);
});
}
}

View File

@@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
});
const page = await this.pagesRepository.insert(new MiPage({
const page = await this.pagesRepository.insertOne(new MiPage({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font,
})).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.pageEntityService.pack(page);
});

View File

@@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}

View File

@@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
weight: repliedUsers[user] / peak,
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
weight: repliedUsers[userId] / peak,
})));
return repliesObj;

View File

@@ -104,11 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
const users = (await this.userListMembershipsRepository.findBy({
userListId: ps.listId,

View File

@@ -65,11 +65,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
return await this.userListEntityService.pack(userList);
});

View File

@@ -3,17 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import sanitizeHtml from 'sanitize-html';
import { Inject, Injectable } from '@nestjs/common';
import type { AbuseUserReportsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { EmailService } from '@/core/EmailService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -57,60 +51,32 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private idService: IdService,
private metaService: MetaService,
private emailService: EmailService,
private getterService: GetterService,
private roleService: RoleService,
private globalEventService: GlobalEventService,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
// Lookup user
const user = await this.getterService.getUser(ps.userId).catch(err => {
const targetUser = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
if (user.id === me.id) {
if (targetUser.id === me.id) {
throw new ApiError(meta.errors.cannotReportYourself);
}
if (await this.roleService.isAdministrator(user)) {
if (await this.roleService.isAdministrator(targetUser)) {
throw new ApiError(meta.errors.cannotReportAdmin);
}
const report = await this.abuseUserReportsRepository.insert({
id: this.idService.gen(),
targetUserId: user.id,
targetUserHost: user.host,
await this.abuseReportService.report([{
targetUserId: targetUser.id,
targetUserHost: targetUser.host,
reporterId: me.id,
reporterHost: null,
comment: ps.comment,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
// Publish event to moderators
setImmediate(async () => {
const moderators = await this.roleService.getModerators();
for (const moderator of moderators) {
this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
id: report.id,
targetUserId: report.targetUserId,
reporterId: report.reporterId,
comment: report.comment,
});
}
const meta = await this.metaService.fetch();
if (meta.email) {
this.emailService.sendEmail(meta.email, 'New abuse report',
sanitizeHtml(ps.comment),
sanitizeHtml(ps.comment));
}
});
}]);
});
}
}

View File

@@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// リクエストされた通りに並べ替え
// 順番は保持されるけど数は減ってる可能性がある
const _users: MiUser[] = [];
for (const id of ps.userIds) {
_users.push(users.find(x => x.id === id)!);
const user = users.find(x => x.id === id);
if (user != null) _users.push(user);
}
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
schema: 'UserDetailed',
})));
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') {

View File

@@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@@ -111,7 +120,8 @@ export class ClientServerService {
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -239,7 +249,8 @@ export class ClientServerService {
this.inboxQueue,
this.dbQueue,
this.objectStorageQueue,
this.webhookDeliverQueue,
this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
serverAdapter,
});
@@ -438,7 +449,7 @@ export class ClientServerService {
//#endregion
const renderBase = async (reply: FastifyReply) => {
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
@@ -447,6 +458,7 @@ export class ClientServerService {
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
...data,
});
};
@@ -465,7 +477,9 @@ export class ClientServerService {
};
// Atom
fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {
@@ -478,7 +492,9 @@ export class ClientServerService {
});
// RSS
fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {
@@ -491,7 +507,9 @@ export class ClientServerService {
});
// JSON
fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {
@@ -744,6 +762,18 @@ export class ClientServerService {
});
//#endregion
//region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
// User with Tags
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
//endregion
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);

View File

@@ -50,6 +50,9 @@ html
block title
= title || 'Misskey'
if noindex
meta(name='robots' content='noindex')
block desc
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')