Merge tag '13.14.1' into merge-upstream
This commit is contained in:
@@ -181,7 +181,7 @@ export class ActivityPubServerService {
|
||||
undefined,
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
cursor: followings.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -189,7 +189,11 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.followersCount,
|
||||
`${partOf}?page=true`,
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
@@ -269,7 +273,7 @@ export class ActivityPubServerService {
|
||||
undefined,
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
cursor: followings.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -277,7 +281,11 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.followingCount,
|
||||
`${partOf}?page=true`,
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
@@ -310,7 +318,10 @@ export class ActivityPubServerService {
|
||||
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
`${this.config.url}/users/${userId}/collections/featured`,
|
||||
renderedNotes.length, undefined, undefined, renderedNotes,
|
||||
renderedNotes.length,
|
||||
undefined,
|
||||
undefined,
|
||||
renderedNotes,
|
||||
);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
@@ -369,7 +380,7 @@ export class ActivityPubServerService {
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE');
|
||||
|
||||
const notes = await query.take(limit).getMany();
|
||||
const notes = await query.limit(limit).getMany();
|
||||
|
||||
if (sinceId) notes.reverse();
|
||||
|
||||
@@ -387,7 +398,7 @@ export class ActivityPubServerService {
|
||||
})}` : undefined,
|
||||
notes.length ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
until_id: notes[notes.length - 1].id,
|
||||
until_id: notes.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -395,7 +406,9 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.notesCount,
|
||||
`${partOf}?page=true`,
|
||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
|
@@ -3,6 +3,8 @@ import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rename from 'rename';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -18,11 +20,9 @@ import { contentDisposition } from '@/misc/content-disposition.js';
|
||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -180,8 +180,8 @@ export class FileServerService {
|
||||
reply.header('Content-Disposition',
|
||||
contentDisposition(
|
||||
'inline',
|
||||
correctFilename(file.filename, image.ext)
|
||||
)
|
||||
correctFilename(file.filename, image.ext),
|
||||
),
|
||||
);
|
||||
return image.data;
|
||||
}
|
||||
@@ -278,11 +278,11 @@ export class FileServerService {
|
||||
};
|
||||
} else {
|
||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
||||
.resize({
|
||||
height: 'emoji' in request.query ? 128 : 320,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault);
|
||||
.resize({
|
||||
height: 'emoji' in request.query ? 128 : 320,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault);
|
||||
|
||||
image = {
|
||||
data,
|
||||
@@ -355,8 +355,8 @@ export class FileServerService {
|
||||
reply.header('Content-Disposition',
|
||||
contentDisposition(
|
||||
'inline',
|
||||
correctFilename(file.filename, image.ext)
|
||||
)
|
||||
correctFilename(file.filename, image.ext),
|
||||
),
|
||||
);
|
||||
return image.data;
|
||||
} catch (e) {
|
||||
|
@@ -17,6 +17,7 @@ import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ApiServerService } from './api/ApiServerService.js';
|
||||
@@ -46,6 +47,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private userEntityService: UserEntityService,
|
||||
private apiServerService: ApiServerService,
|
||||
private openApiServerService: OpenApiServerService,
|
||||
@@ -168,14 +170,16 @@ export class ServerService implements OnApplicationShutdown {
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => {
|
||||
/*
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(request.params.x, fs.createWriteStream(temp));
|
||||
reply.header('Content-Type', 'image/png');
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
return fs.createReadStream(temp).on('close', () => cleanup());
|
||||
*/
|
||||
return reply.redirect('/static-assets/avatar.png');
|
||||
|
||||
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(request.params.x, fs.createWriteStream(temp));
|
||||
return fs.createReadStream(temp).on('close', () => cleanup());
|
||||
} else {
|
||||
return reply.redirect('/static-assets/avatar.png');
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => {
|
||||
@@ -227,14 +231,25 @@ export class ServerService implements OnApplicationShutdown {
|
||||
}
|
||||
});
|
||||
|
||||
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
|
||||
if (this.config.socket) {
|
||||
if (fs.existsSync(this.config.socket)) {
|
||||
fs.unlinkSync(this.config.socket);
|
||||
}
|
||||
fastify.listen({ path: this.config.socket }, (err, address) => {
|
||||
if (this.config.chmodSocket) {
|
||||
fs.chmodSync(this.config.socket!, this.config.chmodSocket);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
|
||||
}
|
||||
|
||||
await fastify.ready();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
await this.streamingApiServerService.detach();
|
||||
await this.streamingApiServerService.detach();
|
||||
await this.#fastify.close();
|
||||
}
|
||||
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import vary from 'vary';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
|
||||
@Injectable()
|
||||
export class WellKnownServerService {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { pipeline } from 'node:stream';
|
||||
import * as fs from 'node:fs';
|
||||
import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
@@ -53,44 +53,72 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}, 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
#sendApiError(reply: FastifyReply, err: ApiError): void {
|
||||
let statusCode = err.httpStatusCode;
|
||||
if (err.httpStatusCode === 401) {
|
||||
reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
|
||||
} else if (err.kind === 'client') {
|
||||
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
|
||||
statusCode = statusCode ?? 400;
|
||||
} else if (err.kind === 'permission') {
|
||||
// (ROLE_PERMISSION_DENIEDは関係ない)
|
||||
if (err.code === 'PERMISSION_DENIED') {
|
||||
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
|
||||
}
|
||||
statusCode = statusCode ?? 403;
|
||||
} else if (!statusCode) {
|
||||
statusCode = 500;
|
||||
}
|
||||
this.send(reply, statusCode, err);
|
||||
}
|
||||
|
||||
#sendAuthenticationError(reply: FastifyReply, err: unknown): void {
|
||||
if (err instanceof AuthenticationError) {
|
||||
const message = 'Authentication failed. Please ensure your token is correct.';
|
||||
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_token", error_description="${message}"`);
|
||||
this.send(reply, 401, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
this.send(reply, 500, new ApiError());
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public handleRequest(
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
): void {
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
|
||||
const token = body?.['i'];
|
||||
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: body?.['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, body, null, request).then((res) => {
|
||||
if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) {
|
||||
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
|
||||
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||
}
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
|
||||
this.#sendApiError(reply, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err instanceof AuthenticationError) {
|
||||
this.send(reply, 403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
this.send(reply, 500, new ApiError());
|
||||
}
|
||||
this.#sendAuthenticationError(reply, err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,7 +127,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const multipartData = await request.file().catch(() => {
|
||||
/* Fastify throws if the remote didn't send multipart data. Return 400 below. */
|
||||
});
|
||||
@@ -117,7 +145,10 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
|
||||
}
|
||||
|
||||
const token = fields['i'];
|
||||
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: fields['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
@@ -129,22 +160,14 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}, request).then((res) => {
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
|
||||
this.#sendApiError(reply, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err instanceof AuthenticationError) {
|
||||
this.send(reply, 403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
} else {
|
||||
this.send(reply, 500, new ApiError());
|
||||
}
|
||||
this.#sendAuthenticationError(reply, err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,7 +236,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
if (ep.meta.limit) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
@@ -255,8 +278,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'Your account has been suspended.',
|
||||
code: 'YOUR_ACCOUNT_SUSPENDED',
|
||||
kind: 'permission',
|
||||
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
|
||||
httpStatusCode: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -266,8 +289,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'You have moved your account.',
|
||||
code: 'YOUR_ACCOUNT_MOVED',
|
||||
kind: 'permission',
|
||||
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
|
||||
httpStatusCode: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -278,6 +301,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to a moderator role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
|
||||
});
|
||||
}
|
||||
@@ -285,6 +309,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to an administrator role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
||||
});
|
||||
}
|
||||
@@ -296,6 +321,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to a required role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
|
||||
});
|
||||
}
|
||||
@@ -305,6 +331,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
throw new ApiError({
|
||||
message: 'Your app does not have the necessary permissions to use this endpoint.',
|
||||
code: 'PERMISSION_DENIED',
|
||||
kind: 'permission',
|
||||
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||
});
|
||||
}
|
||||
@@ -317,7 +344,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
try {
|
||||
data[k] = JSON.parse(data[k]);
|
||||
} catch (e) {
|
||||
throw new ApiError({
|
||||
throw new ApiError({
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||
@@ -335,7 +362,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
} else {
|
||||
const errId = uuid();
|
||||
const errId = randomUUID();
|
||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
|
@@ -40,15 +40,15 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||
if (token == null) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
||||
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationError('user not found');
|
||||
}
|
||||
|
||||
|
||||
return [user, null];
|
||||
} else {
|
||||
const accessToken = await this.accessTokensRepository.findOne({
|
||||
@@ -58,24 +58,24 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||
token: token, // miauth
|
||||
}],
|
||||
});
|
||||
|
||||
|
||||
if (accessToken == null) {
|
||||
throw new AuthenticationError('invalid signature');
|
||||
}
|
||||
|
||||
|
||||
this.accessTokensRepository.update(accessToken.id, {
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
|
||||
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
|
||||
() => this.usersRepository.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<LocalUser>);
|
||||
|
||||
|
||||
if (accessToken.appId) {
|
||||
const app = await this.appCache.fetch(accessToken.appId,
|
||||
() => this.appsRepository.findOneByOrFail({ id: accessToken.appId! }));
|
||||
|
||||
|
||||
return [user, {
|
||||
id: accessToken.id,
|
||||
permission: app.permission,
|
||||
|
@@ -38,7 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
@@ -378,7 +383,8 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati
|
||||
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
|
||||
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
|
||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||
const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default };
|
||||
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
|
||||
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
|
||||
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
||||
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
||||
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
||||
@@ -570,6 +576,10 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
|
||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
||||
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
|
||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
|
||||
@@ -722,7 +732,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
@@ -914,6 +925,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
@@ -1060,7 +1075,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
@@ -1252,6 +1268,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
|
@@ -38,14 +38,14 @@ export class RateLimiterService {
|
||||
max: 1,
|
||||
db: this.redisClient,
|
||||
});
|
||||
|
||||
|
||||
minIntervalLimiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
||||
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('BRIEF_REQUEST_INTERVAL');
|
||||
} else {
|
||||
@@ -57,7 +57,7 @@ export class RateLimiterService {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Long term limit
|
||||
const max = (): void => {
|
||||
const limiter = new Limiter({
|
||||
@@ -66,14 +66,14 @@ export class RateLimiterService {
|
||||
max: limitation.max! / factor,
|
||||
db: this.redisClient,
|
||||
});
|
||||
|
||||
|
||||
limiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
||||
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('RATE_LIMIT_EXCEEDED');
|
||||
} else {
|
||||
@@ -81,13 +81,13 @@ export class RateLimiterService {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||
|
||||
|
||||
const hasLongTermLimit =
|
||||
typeof limitation.duration === 'number' &&
|
||||
typeof limitation.max === 'number';
|
||||
|
||||
|
||||
if (hasShortTermLimit) {
|
||||
min();
|
||||
} else if (hasLongTermLimit) {
|
||||
|
@@ -36,7 +36,7 @@ export class SigninService {
|
||||
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));
|
||||
});
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, RegistrationTicket } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||
@@ -14,6 +13,7 @@ import { EmailService } from '@/core/EmailService.js';
|
||||
import { LocalUser } from '@/models/entities/User.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
|
||||
@@ -67,7 +67,7 @@ export class SignupApiService {
|
||||
const body = request.body;
|
||||
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
|
||||
// Verify *Captcha
|
||||
// ただしテスト時はこの機構は障害となるため無効にする
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
@@ -76,7 +76,7 @@ export class SignupApiService {
|
||||
throw new FastifyReplyError(400, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
@@ -89,51 +89,61 @@ export class SignupApiService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const username = body['username'];
|
||||
const password = body['password'];
|
||||
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
|
||||
const invitationCode = body['invitationCode'];
|
||||
const emailAddress = body['emailAddress'];
|
||||
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
if (emailAddress == null || typeof emailAddress !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const res = await this.emailService.validateEmailForAccount(emailAddress);
|
||||
if (!res.available) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let ticket: RegistrationTicket | null = null;
|
||||
|
||||
if (instance.disableRegistration) {
|
||||
if (invitationCode == null || typeof invitationCode !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
|
||||
ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
code: invitationCode,
|
||||
});
|
||||
|
||||
|
||||
if (ticket == null) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
this.registrationTicketsRepository.delete(ticket.id);
|
||||
|
||||
if (ticket.expiresAt && ticket.expiresAt < new Date()) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.usedAt) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
||||
}
|
||||
|
||||
@@ -142,20 +152,20 @@ export class SignupApiService {
|
||||
throw new FastifyReplyError(400, 'DENIED_USERNAME');
|
||||
}
|
||||
|
||||
const code = rndstr('a-z0-9', 16);
|
||||
const code = secureRndstr(16, { chars: L_CHARS });
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
await this.userPendingsRepository.insert({
|
||||
const pendingUser = await this.userPendingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
||||
@@ -163,6 +173,13 @@ export class SignupApiService {
|
||||
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
pendingUserId: pendingUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
@@ -170,12 +187,20 @@ export class SignupApiService {
|
||||
const { account, secret } = await this.signupService.signup({
|
||||
username, password, host,
|
||||
});
|
||||
|
||||
|
||||
const res = await this.userEntityService.pack(account, account, {
|
||||
detail: true,
|
||||
includeSecrets: true,
|
||||
});
|
||||
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
token: secret,
|
||||
@@ -212,6 +237,15 @@ export class SignupApiService {
|
||||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id });
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
pendingUserId: null,
|
||||
});
|
||||
}
|
||||
|
||||
return this.signinService.signin(request, reply, account as LocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
|
@@ -10,7 +10,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { LocalUser } from '@/models/entities/User';
|
||||
import { LocalUser } from '@/models/entities/User.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
@@ -58,11 +58,21 @@ export class StreamingApiServerService {
|
||||
let user: LocalUser | null = null;
|
||||
let app: AccessToken | null = null;
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1
|
||||
// Note that the standard WHATWG WebSocket API does not support setting any headers,
|
||||
// but non-browser apps may still be able to set it.
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: q.get('i');
|
||||
|
||||
try {
|
||||
[user, app] = await this.authenticateService.authenticate(q.get('i'));
|
||||
[user, app] = await this.authenticateService.authenticate(token);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthenticationError) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.write([
|
||||
'HTTP/1.1 401 Unauthorized',
|
||||
'WWW-Authenticate: Bearer realm="Misskey", error="invalid_token", error_description="Failed to authenticate"',
|
||||
].join('\r\n') + '\r\n\r\n');
|
||||
} else {
|
||||
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import * as fs from 'node:fs';
|
||||
import Ajv from 'ajv';
|
||||
import _Ajv from 'ajv';
|
||||
import type { Schema, SchemaType } from '@/misc/json-schema.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { ApiError } from './error.js';
|
||||
import type { IEndpointMeta } from './endpoints.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
|
||||
const ajv = new Ajv({
|
||||
useDefaults: true,
|
||||
});
|
||||
@@ -32,23 +34,23 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
let cleanup: undefined | (() => void) = undefined;
|
||||
|
||||
|
||||
if (meta.requireFile) {
|
||||
cleanup = () => {
|
||||
if (file) fs.unlink(file.path, () => {});
|
||||
};
|
||||
|
||||
|
||||
if (file == null) return Promise.reject(new ApiError({
|
||||
message: 'File required.',
|
||||
code: 'FILE_REQUIRED',
|
||||
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
const valid = validate(params);
|
||||
if (!valid) {
|
||||
if (file) cleanup!();
|
||||
|
||||
|
||||
const errors = validate.errors!;
|
||||
const err = new ApiError({
|
||||
message: 'Invalid param.',
|
||||
@@ -60,7 +62,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
});
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
|
||||
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
|
||||
};
|
||||
}
|
||||
|
@@ -38,7 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
@@ -376,7 +381,8 @@ const eps = [
|
||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||
['admin/get-table-stats', ep___admin_getTableStats],
|
||||
['admin/get-user-ips', ep___admin_getUserIps],
|
||||
['invite', ep___invite],
|
||||
['admin/invite/create', ep___admin_invite_create],
|
||||
['admin/invite/list', ep___admin_invite_list],
|
||||
['admin/promo/create', ep___admin_promo_create],
|
||||
['admin/queue/clear', ep___admin_queue_clear],
|
||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||
@@ -568,6 +574,10 @@ const eps = [
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['invite/create', ep___invite_create],
|
||||
['invite/delete', ep___invite_delete],
|
||||
['invite/list', ep___invite_list],
|
||||
['invite/limit', ep___invite_limit],
|
||||
['meta', ep___meta],
|
||||
['emojis', ep___emojis],
|
||||
['emoji', ep___emoji],
|
||||
|
@@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break;
|
||||
}
|
||||
|
||||
const reports = await query.take(ps.limit).getMany();
|
||||
const reports = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.abuseUserReportEntityService.packMany(reports);
|
||||
});
|
||||
|
@@ -22,8 +22,9 @@ export const paramDef = {
|
||||
expiresAt: { type: 'integer' },
|
||||
startsAt: { type: 'integer' },
|
||||
imageUrl: { type: 'string', minLength: 1 },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
},
|
||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'],
|
||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
startsAt: new Date(ps.startsAt),
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
priority: ps.priority,
|
||||
|
@@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
||||
const ads = await query.take(ps.limit).getMany();
|
||||
const ads = await query.limit(ps.limit).getMany();
|
||||
|
||||
return ads;
|
||||
});
|
||||
|
@@ -31,8 +31,9 @@ export const paramDef = {
|
||||
ratio: { type: 'integer' },
|
||||
expiresAt: { type: 'integer' },
|
||||
startsAt: { type: 'integer' },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
},
|
||||
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'],
|
||||
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@@ -56,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
imageUrl: ps.imageUrl,
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
startsAt: new Date(ps.startsAt),
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
|
||||
|
||||
const announcements = await query.take(ps.limit).getMany();
|
||||
const announcements = await query.limit(ps.limit).getMany();
|
||||
|
||||
const reads = new Map<Announcement, number>();
|
||||
|
||||
|
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
title: ps.title,
|
||||
text: ps.text,
|
||||
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
|
||||
imageUrl: ps.imageUrl || null,
|
||||
imageUrl: ps.imageUrl || null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
}
|
||||
|
||||
const files = await query.take(ps.limit).getMany();
|
||||
const files = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.driveFileEntityService.packMany(files, { detail: true, withUser: true, self: true });
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -56,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -78,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
emojiId: emoji.id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
return this.emojiEntityService.packDetailed(emoji);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const emojis = await q
|
||||
.orderBy('emoji.id', 'DESC')
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.emojiEntityService.packDetailedMany(emojis);
|
||||
|
@@ -84,14 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
if (ps.query) {
|
||||
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||
//const emojis = await q.take(ps.limit).getMany();
|
||||
//const emojis = await q.limit(ps.limit).getMany();
|
||||
|
||||
emojis = await q.getMany();
|
||||
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
|
||||
|
||||
if (queryarry) {
|
||||
emojis = emojis.filter(emoji =>
|
||||
queryarry.includes(`:${emoji.name}:`)
|
||||
emojis = emojis.filter(emoji =>
|
||||
queryarry.includes(`:${emoji.name}:`),
|
||||
);
|
||||
} else {
|
||||
emojis = emojis.filter(emoji =>
|
||||
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
emojis.splice(ps.limit + 1);
|
||||
} else {
|
||||
emojis = await q.take(ps.limit).getMany();
|
||||
emojis = await q.limit(ps.limit).getMany();
|
||||
}
|
||||
|
||||
return this.emojiEntityService.packDetailedMany(emojis);
|
||||
|
@@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
|
@@ -0,0 +1,80 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
invalidDateTime: {
|
||||
message: 'Invalid date-time format',
|
||||
code: 'INVALID_DATE_TIME',
|
||||
id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
count: { type: 'integer', minimum: 1, maximum: 100, default: 1 },
|
||||
expiresAt: { type: 'string', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
|
||||
throw new ApiError(meta.errors.invalidDateTime);
|
||||
}
|
||||
|
||||
const ticketsPromises = [];
|
||||
|
||||
for (let i = 0; i < ps.count; i++) {
|
||||
ticketsPromises.push(this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
code: generateInviteCode(),
|
||||
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
|
||||
}
|
||||
|
||||
const tickets = await Promise.all(ticketsPromises);
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' },
|
||||
sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.registrationTicketsRepository.createQueryBuilder('ticket')
|
||||
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
|
||||
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
|
||||
|
||||
switch (ps.type) {
|
||||
case 'unused': query.andWhere('ticket.usedBy IS NULL'); break;
|
||||
case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break;
|
||||
case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break;
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('ticket.createdAt', 'DESC'); break;
|
||||
case '-createdAt': query.orderBy('ticket.createdAt', 'ASC'); break;
|
||||
case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break;
|
||||
case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break;
|
||||
default: query.orderBy('ticket.id', 'DESC'); break;
|
||||
}
|
||||
|
||||
query.limit(ps.limit);
|
||||
query.skip(ps.offset);
|
||||
|
||||
const tickets = await query.getMany();
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -20,6 +19,10 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cacheRemoteSensitiveFiles: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
emailRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -262,6 +265,14 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableServerMachineStats: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableIdenticonGeneration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
policies: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
@@ -324,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||
pinnedUsers: instance.pinnedUsers,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
@@ -364,6 +376,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
};
|
||||
});
|
||||
|
@@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id });
|
||||
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyPromoted);
|
||||
}
|
||||
|
||||
|
@@ -33,15 +33,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
delayedQueues = await this.queueService.deliverQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
await queue.promote();
|
||||
try {
|
||||
await queue.promote();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.indexOf('not in a delayed state') !== -1) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'inbox':
|
||||
delayedQueues = await this.queueService.inboxQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
await queue.promote();
|
||||
try {
|
||||
await queue.promote();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.indexOf('not in a delayed state') !== -1) {
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import rndstr from 'rndstr';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, UserProfilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new Error('cannot reset password of root');
|
||||
}
|
||||
|
||||
const passwd = rndstr('a-zA-Z0-9', 8);
|
||||
const passwd = secureRndstr(8);
|
||||
|
||||
// Generate hash of password
|
||||
const hash = bcrypt.hashSync(passwd);
|
||||
|
@@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
|
||||
if (!roleExist) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
|
@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.innerJoinAndSelect('assign.user', 'user');
|
||||
|
||||
const assigns = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(assigns.map(async assign => ({
|
||||
|
@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
||||
|
||||
const reports = await query.take(ps.limit).getMany();
|
||||
const reports = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.moderationLogEntityService.packMany(reports);
|
||||
});
|
||||
|
@@ -61,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
||||
|
||||
const roleAssigns = await this.roleService.getUserAssigns(user.id);
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
return {
|
||||
@@ -85,6 +86,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
signins,
|
||||
policies: await this.roleService.getUserPolicies(user.id),
|
||||
roles: await this.roleEntityService.packMany(roles, me),
|
||||
roleAssigns: roleAssigns.map(a => ({
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
|
||||
roleId: a.roleId,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
default: query.orderBy('user.id', 'ASC'); break;
|
||||
}
|
||||
|
||||
query.take(ps.limit);
|
||||
query.limit(ps.limit);
|
||||
query.skip(ps.offset);
|
||||
|
||||
const users = await query.getMany();
|
||||
|
@@ -43,6 +43,7 @@ export const paramDef = {
|
||||
defaultLightTheme: { type: 'string', nullable: true },
|
||||
defaultDarkTheme: { type: 'string', nullable: true },
|
||||
cacheRemoteFiles: { type: 'boolean' },
|
||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
enableHcaptcha: { type: 'boolean' },
|
||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||
@@ -96,6 +97,8 @@ export const paramDef = {
|
||||
enableActiveEmailValidation: { type: 'boolean' },
|
||||
enableChartsForRemoteUser: { type: 'boolean' },
|
||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||
enableServerMachineStats: { type: 'boolean' },
|
||||
enableIdenticonGeneration: { type: 'boolean' },
|
||||
serverRules: { type: 'array', items: { type: 'string' } },
|
||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
@@ -134,7 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (Array.isArray(ps.sensitiveWords)) {
|
||||
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
if (ps.themeColor !== undefined) {
|
||||
set.themeColor = ps.themeColor;
|
||||
}
|
||||
@@ -191,6 +194,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||
}
|
||||
|
||||
if (ps.cacheRemoteSensitiveFiles !== undefined) {
|
||||
set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles;
|
||||
}
|
||||
|
||||
if (ps.emailRequiredForSignup !== undefined) {
|
||||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||
}
|
||||
@@ -399,6 +406,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
|
||||
}
|
||||
|
||||
if (ps.enableServerMachineStats !== undefined) {
|
||||
set.enableServerMachineStats = ps.enableServerMachineStats;
|
||||
}
|
||||
|
||||
if (ps.enableIdenticonGeneration !== undefined) {
|
||||
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
|
||||
}
|
||||
|
||||
if (ps.serverRules !== undefined) {
|
||||
set.serverRules = ps.serverRules;
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
|
||||
|
||||
const announcements = await query.take(ps.limit).getMany();
|
||||
const announcements = await query.limit(ps.limit).getMany();
|
||||
|
||||
if (me) {
|
||||
const reads = (await this.announcementReadsRepository.findBy({
|
||||
|
@@ -76,6 +76,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
@@ -112,11 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.noteReadService.read(me.id, notes);
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
|
@@ -112,6 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
|
||||
|
@@ -44,7 +44,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Generate secret
|
||||
const secret = secureRndstr(32, true);
|
||||
const secret = secureRndstr(32);
|
||||
|
||||
// for backward compatibility
|
||||
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
|
||||
|
@@ -55,15 +55,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
|
||||
const accessToken = secureRndstr(32, true);
|
||||
const accessToken = secureRndstr(32);
|
||||
|
||||
// Fetch exist access token
|
||||
const exist = await this.accessTokensRepository.findOneBy({
|
||||
appId: session.appId,
|
||||
userId: me.id,
|
||||
const exist = await this.accessTokensRepository.exist({
|
||||
where: {
|
||||
appId: session.appId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
||||
|
||||
// Generate Hash
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
|
||||
@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = uuid();
|
||||
const token = randomUUID();
|
||||
|
||||
// Create session token document
|
||||
const doc = await this.authSessionsRepository.insert({
|
||||
|
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,8 @@ import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
|
||||
|
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id });
|
||||
|
||||
const blockings = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.blockingEntityService.packMany(blockings, me);
|
||||
|
@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('channel.isArchived = FALSE')
|
||||
.orderBy('channel.lastNotedAt', 'DESC');
|
||||
|
||||
const channels = await query.take(10).getMany();
|
||||
const channels = await query.limit(10).getMany();
|
||||
|
||||
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));
|
||||
});
|
||||
|
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere({ followerId: me.id });
|
||||
|
||||
const followings = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me)));
|
||||
|
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere({ userId: me.id });
|
||||
|
||||
const channels = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));
|
||||
|
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
const channels = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me)));
|
||||
|
@@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
let noteIdsRes: [string, string[]][] = [];
|
||||
|
||||
|
||||
if (!ps.sinceId && !ps.sinceDate) {
|
||||
noteIdsRes = await this.redisClient.xrevrange(
|
||||
`channelTimeline:${channel.id}`,
|
||||
@@ -105,7 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
timeline = await query.take(ps.limit).getMany();
|
||||
timeline = await query.limit(ps.limit).getMany();
|
||||
} else {
|
||||
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||
|
||||
|
@@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const exist = await this.clipNotesRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
const exist = await this.clipNotesRepository.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyClipped);
|
||||
}
|
||||
|
||||
|
@@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
const exist = await this.clipFavoritesRepository.exist({
|
||||
where: {
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
|
@@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
const notes = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
|
@@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'notes', 'clips'],
|
||||
|
@@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
case '-size': query.orderBy('file.size', 'ASC'); break;
|
||||
}
|
||||
|
||||
const files = await query.take(ps.limit).getMany();
|
||||
const files = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.driveFileEntityService.packMany(files, { detail: false, self: true });
|
||||
});
|
||||
|
@@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
md5: ps.md5,
|
||||
userId: me.id,
|
||||
const exist = await this.driveFilesRepository.exist({
|
||||
where: {
|
||||
md5: ps.md5,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
return file != null;
|
||||
return exist;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ export const meta = {
|
||||
code: 'NO_SUCH_FOLDER',
|
||||
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
|
||||
},
|
||||
|
||||
|
||||
restrictedByRole: {
|
||||
message: 'This feature is restricted by your role.',
|
||||
code: 'RESTRICTED_BY_ROLE',
|
||||
|
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
query.andWhere('folder.parentId IS NULL');
|
||||
}
|
||||
|
||||
const folders = await query.take(ps.limit).getMany();
|
||||
const folders = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder)));
|
||||
});
|
||||
|
@@ -56,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
}
|
||||
|
||||
const files = await query.take(ps.limit).getMany();
|
||||
const files = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.driveFileEntityService.packMany(files, { detail: false, self: true });
|
||||
});
|
||||
|
@@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
|
@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
|
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('following.followeeHost = :host', { host: ps.host });
|
||||
|
||||
const followings = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
|
||||
|
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('following.followerHost = :host', { host: ps.host });
|
||||
|
||||
const followings = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
|
||||
|
@@ -126,7 +126,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' });
|
||||
}
|
||||
|
||||
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
|
||||
const instances = await query.limit(ps.limit).skip(ps.offset).getMany();
|
||||
|
||||
return await this.instanceEntityService.packMany(instances);
|
||||
});
|
||||
|
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('user.host = :host', { host: ps.host });
|
||||
|
||||
const users = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.userEntityService.packMany(users, me, { detail: true });
|
||||
|
@@ -40,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('flash.likedCount > 0')
|
||||
.orderBy('flash.likedCount', 'DESC');
|
||||
|
||||
const flashs = await query.take(10).getMany();
|
||||
const flashs = await query.limit(10).getMany();
|
||||
|
||||
return await this.flashEntityService.packMany(flashs, me);
|
||||
});
|
||||
|
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.flashLikesRepository.findOneBy({
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
const exist = await this.flashLikesRepository.exist({
|
||||
where: {
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('like.flash', 'flash');
|
||||
|
||||
const likes = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.flashLikeEntityService.packMany(likes, me);
|
||||
|
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('flash.userId = :meId', { meId: me.id });
|
||||
|
||||
const flashs = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.flashEntityService.packMany(flashs);
|
||||
|
@@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check if already following
|
||||
const exist = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const exist = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFollowing);
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,8 @@ import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const exist = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,8 @@ import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
|
@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('request.followeeId = :meId', { meId: me.id });
|
||||
|
||||
const requests = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
|
||||
|
@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('post.likedCount > 0')
|
||||
.orderBy('post.likedCount', 'DESC');
|
||||
|
||||
const posts = await query.take(10).getMany();
|
||||
const posts = await query.limit(10).getMany();
|
||||
|
||||
return await this.galleryPostEntityService.packMany(posts, me);
|
||||
});
|
||||
|
@@ -40,7 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('post.likedCount > 0')
|
||||
.orderBy('post.likedCount', 'DESC');
|
||||
|
||||
const posts = await query.take(10).getMany();
|
||||
const posts = await query.limit(10).getMany();
|
||||
|
||||
return await this.galleryPostEntityService.packMany(posts, me);
|
||||
});
|
||||
|
@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||
.innerJoinAndSelect('post.user', 'user');
|
||||
|
||||
const posts = await query.take(ps.limit).getMany();
|
||||
const posts = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.galleryPostEntityService.packMany(posts, me);
|
||||
});
|
||||
|
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.galleryLikesRepository.findOneBy({
|
||||
postId: post.id,
|
||||
userId: me.id,
|
||||
const exist = await this.galleryLikesRepository.exist({
|
||||
where: {
|
||||
postId: post.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
@@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
'tag.attachedRemoteUsersCount',
|
||||
]);
|
||||
|
||||
const tags = await query.take(ps.limit).getMany();
|
||||
const tags = await query.limit(ps.limit).getMany();
|
||||
|
||||
return this.hashtagEntityService.packMany(tags);
|
||||
});
|
||||
|
@@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
|
||||
.orderBy('tag.count', 'DESC')
|
||||
.groupBy('tag.id')
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.skip(ps.offset)
|
||||
.getMany();
|
||||
|
||||
|
@@ -39,7 +39,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
|
||||
}
|
||||
|
||||
const users = await query.take(ps.limit).getMany();
|
||||
const users = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.userEntityService.packMany(users, me, { detail: true });
|
||||
});
|
||||
|
@@ -23,7 +23,7 @@ export const meta = {
|
||||
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
|
||||
kind: 'permission',
|
||||
},
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
||||
}
|
||||
|
||||
|
||||
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
|
||||
detail: true,
|
||||
includeSecrets: isSecure,
|
||||
|
@@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const procedures = this.twoFactorAuthenticationService.getProcedures();
|
||||
|
||||
if (!(procedures as any)[attestation.fmt]) {
|
||||
throw new Error('unsupported fmt');
|
||||
throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`);
|
||||
}
|
||||
|
||||
const verificationData = (procedures as any)[attestation.fmt].verify({
|
||||
|
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (key.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
|
||||
await this.userSecurityKeysRepository.update(key.id, {
|
||||
name: ps.name,
|
||||
});
|
||||
|
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('favorite.note', 'note');
|
||||
|
||||
const favorites = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.noteFavoriteEntityService.packMany(favorites, me);
|
||||
|
@@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('like.post', 'post');
|
||||
|
||||
const likes = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.galleryLikeEntityService.packMany(likes, me);
|
||||
|
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('post.userId = :meId', { meId: me.id });
|
||||
|
||||
const posts = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.galleryPostEntityService.packMany(posts, me);
|
||||
|
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor (
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private downloadService: DownloadService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const users = await this.usersRepository.findOneBy({ id: me.id });
|
||||
if (users === null) throw new ApiError(meta.errors.noSuchUser);
|
||||
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
|
||||
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||
@@ -79,6 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.queueService.createImportAntennasJob(me, antennas);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Antenna = (_Antenna & { userListAccts: string[] | null })[];
|
||||
|
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||
me,
|
||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||
true
|
||||
true,
|
||||
);
|
||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||
|
||||
|
@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||
me,
|
||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||
true
|
||||
true,
|
||||
);
|
||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||
|
||||
|
@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||
me,
|
||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||
true
|
||||
true,
|
||||
);
|
||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||
|
||||
|
@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||
me,
|
||||
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||
true
|
||||
true,
|
||||
);
|
||||
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||
|
||||
|
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('like.page', 'page');
|
||||
|
||||
const likes = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.pageLikeEntityService.packMany(likes, me);
|
||||
|
@@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('page.userId = :meId', { meId: me.id });
|
||||
|
||||
const pages = await query
|
||||
.take(ps.limit)
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.pageEntityService.packMany(pages);
|
||||
|
@@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Check if announcement exists
|
||||
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });
|
||||
const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
|
||||
|
||||
if (announcement == null) {
|
||||
if (!announcementExist) {
|
||||
throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
}
|
||||
|
||||
// Check if already read
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: ps.announcementId,
|
||||
userId: me.id,
|
||||
const alreadyRead = await this.announcementReadsRepository.exist({
|
||||
where: {
|
||||
announcementId: ps.announcementId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (read != null) {
|
||||
if (alreadyRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
|
||||
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
||||
|
||||
if (token) {
|
||||
if (tokenExist) {
|
||||
await this.accessTokensRepository.delete({
|
||||
id: ps.tokenId,
|
||||
userId: me.id,
|
||||
|
@@ -35,7 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
|
||||
.andWhere('signin.userId = :meId', { meId: me.id });
|
||||
|
||||
const history = await query.take(ps.limit).getMany();
|
||||
const history = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await Promise.all(history.map(record => this.signinEntityService.pack(record)));
|
||||
});
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import ms from 'ms';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -9,6 +8,7 @@ import { EmailService } from '@/core/EmailService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -94,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
|
||||
|
||||
if (ps.email != null) {
|
||||
const code = rndstr('a-z0-9', 16);
|
||||
const code = secureRndstr(16, { chars: L_CHARS });
|
||||
|
||||
await this.userProfilesRepository.update(me.id, {
|
||||
emailVerifyCode: code,
|
||||
|
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
exceededCreateLimit: {
|
||||
message: 'You have exceeded the limit for creating an invitation code.',
|
||||
code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE',
|
||||
id: '8b165dd3-6f37-4557-8db1-73175d63c641',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
if (policies.inviteLimit) {
|
||||
const count = await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteLimitCycle * 1000 * 60))),
|
||||
createdById: me.id,
|
||||
});
|
||||
|
||||
if (count >= policies.inviteLimit) {
|
||||
throw new ApiError(meta.errors.exceededCreateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
noSuchCode: {
|
||||
message: 'No such invite code.',
|
||||
code: 'NO_SUCH_INVITE_CODE',
|
||||
id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634',
|
||||
},
|
||||
|
||||
cantDelete: {
|
||||
message: 'You can\'t delete this invite code.',
|
||||
code: 'CAN_NOT_DELETE_INVITE_CODE',
|
||||
id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '5eb8d909-2540-4970-90b8-dd6f86088121',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inviteId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['inviteId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId });
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
|
||||
if (ticket == null) {
|
||||
throw new ApiError(meta.errors.noSuchCode);
|
||||
}
|
||||
|
||||
if (ticket.createdById !== me.id && !isModerator) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
if (ticket.usedAt && !isModerator) {
|
||||
throw new ApiError(meta.errors.cantDelete);
|
||||
}
|
||||
|
||||
await this.registrationTicketsRepository.delete(ticket.id);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
import rndstr from 'rndstr';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -15,12 +15,9 @@ export const meta = {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: '2ERUA5VR',
|
||||
maxLength: 8,
|
||||
minLength: 8,
|
||||
remaining: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -39,22 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const code = rndstr({
|
||||
length: 8,
|
||||
chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
|
||||
});
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
});
|
||||
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
|
||||
createdById: me.id,
|
||||
}) : null;
|
||||
|
||||
return {
|
||||
code,
|
||||
remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null,
|
||||
};
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user