Merge tag '13.11.0' into io
# Conflicts: # packages/backend/src/server/ServerService.ts # packages/backend/src/server/api/endpoints/notes/timeline.ts
This commit is contained in:
@@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import type { Following } from '@/models/entities/Following.js';
|
||||
import { countIf } from '@/misc/prelude/array.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
@@ -58,7 +58,7 @@ export class ActivityPubServerService {
|
||||
private userEntityService: UserEntityService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
@@ -540,7 +540,7 @@ export class ActivityPubServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
|
@@ -2,7 +2,6 @@ import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import rename from 'rename';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||
@@ -22,6 +21,8 @@ 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';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -51,15 +52,6 @@ export class FileServerService {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public commonReadableHandlerGenerator(reply: FastifyReply) {
|
||||
return (err: Error): void => {
|
||||
this.logger.error(err);
|
||||
reply.code(500);
|
||||
reply.header('Cache-Control', 'max-age=300');
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
@@ -67,11 +59,6 @@ export class FileServerService {
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.get('/files/app-default.jpg', (request, reply) => {
|
||||
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
||||
reply.header('Content-Type', 'image/jpeg');
|
||||
@@ -140,7 +127,7 @@ export class FileServerService {
|
||||
let image: IImageStreamable | null = null;
|
||||
|
||||
if (file.fileRole === 'thumbnail') {
|
||||
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
|
||||
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/static.webp`);
|
||||
@@ -190,13 +177,19 @@ export class FileServerService {
|
||||
}
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||
reply.header('Content-Disposition',
|
||||
contentDisposition(
|
||||
'inline',
|
||||
correctFilename(file.filename, image.ext)
|
||||
)
|
||||
);
|
||||
return image.data;
|
||||
}
|
||||
|
||||
if (file.fileRole !== 'original') {
|
||||
const filename = rename(file.file.name, {
|
||||
const filename = rename(file.filename, {
|
||||
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
|
||||
extname: file.ext ? `.${file.ext}` : undefined,
|
||||
extname: file.ext ? `.${file.ext}` : '.unknown',
|
||||
}).toString();
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
|
||||
@@ -204,12 +197,10 @@ export class FileServerService {
|
||||
reply.header('Content-Disposition', contentDisposition('inline', filename));
|
||||
return fs.createReadStream(file.path);
|
||||
} else {
|
||||
const stream = fs.createReadStream(file.path);
|
||||
stream.on('error', this.commonReadableHandlerGenerator(reply));
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', file.file.name));
|
||||
return stream;
|
||||
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
|
||||
return fs.createReadStream(file.path);
|
||||
}
|
||||
} catch (e) {
|
||||
if ('cleanup' in file) file.cleanup();
|
||||
@@ -261,8 +252,8 @@ export class FileServerService {
|
||||
}
|
||||
|
||||
try {
|
||||
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
|
||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
|
||||
|
||||
if (
|
||||
'emoji' in request.query ||
|
||||
@@ -286,7 +277,7 @@ export class FileServerService {
|
||||
type: file.mime,
|
||||
};
|
||||
} else {
|
||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
||||
.resize({
|
||||
height: 'emoji' in request.query ? 128 : 320,
|
||||
withoutEnlargement: true,
|
||||
@@ -300,11 +291,11 @@ export class FileServerService {
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
|
||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
||||
} else if ('preview' in request.query) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
|
||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
||||
} else if ('badge' in request.query) {
|
||||
const mask = sharp(file.path)
|
||||
const mask = (await sharpBmp(file.path, file.mime))
|
||||
.resize(96, 96, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: false,
|
||||
@@ -314,20 +305,20 @@ export class FileServerService {
|
||||
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||
.flatten({ background: '#000' })
|
||||
.toColorspace('b-w');
|
||||
|
||||
|
||||
const stats = await mask.clone().stats();
|
||||
|
||||
|
||||
if (stats.entropy < 0.1) {
|
||||
// エントロピーがあまりない場合は404にする
|
||||
throw new StatusError('Skip to provide badge', 404);
|
||||
}
|
||||
|
||||
|
||||
const data = sharp({
|
||||
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||
})
|
||||
.pipelineColorspace('b-w')
|
||||
.boolean(await mask.png().toBuffer(), 'eor');
|
||||
|
||||
|
||||
image = {
|
||||
data: await data.png().toBuffer(),
|
||||
ext: 'png',
|
||||
@@ -360,6 +351,12 @@ export class FileServerService {
|
||||
|
||||
reply.header('Content-Type', image.type);
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition',
|
||||
contentDisposition(
|
||||
'inline',
|
||||
correctFilename(file.filename, image.ext)
|
||||
)
|
||||
);
|
||||
return image.data;
|
||||
} catch (e) {
|
||||
if ('cleanup' in file) file.cleanup();
|
||||
@@ -369,8 +366,8 @@ export class FileServerService {
|
||||
|
||||
@bindThis
|
||||
private async getStreamAndTypeFromUrl(url: string): Promise<
|
||||
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
|
||||
| '404'
|
||||
| '204'
|
||||
> {
|
||||
@@ -386,18 +383,19 @@ export class FileServerService {
|
||||
|
||||
@bindThis
|
||||
private async downloadAndDetectTypeFromUrl(url: string): Promise<
|
||||
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
|
||||
> {
|
||||
const [path, cleanup] = await createTemp();
|
||||
try {
|
||||
await this.downloadService.downloadUrl(url, path);
|
||||
const { filename } = await this.downloadService.downloadUrl(url, path);
|
||||
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
|
||||
|
||||
return {
|
||||
state: 'remote',
|
||||
mime, ext,
|
||||
path, cleanup,
|
||||
filename,
|
||||
};
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
@@ -407,8 +405,8 @@ export class FileServerService {
|
||||
|
||||
@bindThis
|
||||
private async getFileFromKey(key: string): Promise<
|
||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; }
|
||||
| '404'
|
||||
| '204'
|
||||
> {
|
||||
@@ -432,6 +430,7 @@ export class FileServerService {
|
||||
url: file.uri,
|
||||
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||
file,
|
||||
filename: file.name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -443,6 +442,7 @@ export class FileServerService {
|
||||
state: 'stored_internal',
|
||||
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
|
||||
file,
|
||||
filename: file.name,
|
||||
mime, ext,
|
||||
path,
|
||||
};
|
||||
@@ -452,6 +452,7 @@ export class FileServerService {
|
||||
state: 'stored_internal',
|
||||
fileRole: 'original',
|
||||
file,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
ext: null,
|
||||
path,
|
||||
|
@@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import NotesChart from '@/core/chart/charts/notes.js';
|
||||
@@ -100,7 +100,7 @@ export class NodeinfoServerService {
|
||||
email: meta.maintainerEmail,
|
||||
},
|
||||
langs: meta.langs,
|
||||
tosUrl: meta.ToSUrl,
|
||||
tosUrl: meta.termsOfServiceUrl,
|
||||
repositoryUrl: meta.repositoryUrl,
|
||||
feedbackUrl: meta.feedbackUrl,
|
||||
disableRegistration: meta.disableRegistration,
|
||||
@@ -118,17 +118,17 @@ export class NodeinfoServerService {
|
||||
};
|
||||
};
|
||||
|
||||
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||
|
||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||
const base = await cache.fetch(null, () => nodeinfo2());
|
||||
const base = await cache.fetch(() => nodeinfo2());
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
return { version: '2.1', ...base };
|
||||
});
|
||||
|
||||
fastify.get(nodeinfo2_0path, async (request, reply) => {
|
||||
const base = await cache.fetch(null, () => nodeinfo2());
|
||||
const base = await cache.fetch(() => nodeinfo2());
|
||||
|
||||
delete (base as any).software.repository;
|
||||
|
||||
|
@@ -33,6 +33,7 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
|
||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -72,6 +73,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
QueueStatsChannelService,
|
||||
ServerStatsChannelService,
|
||||
UserListChannelService,
|
||||
OpenApiServerService,
|
||||
],
|
||||
exports: [
|
||||
ServerService,
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import cluster from 'node:cluster';
|
||||
import os from 'node:os';
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -22,6 +24,9 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@Injectable()
|
||||
export class ServerService implements OnApplicationShutdown {
|
||||
@@ -43,6 +48,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apiServerService: ApiServerService,
|
||||
private openApiServerService: OpenApiServerService,
|
||||
private streamingApiServerService: StreamingApiServerService,
|
||||
private activityPubServerService: ActivityPubServerService,
|
||||
private wellKnownServerService: WellKnownServerService,
|
||||
@@ -72,13 +78,15 @@ export class ServerService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
|
||||
const hostname = os.hostname();
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('x-worker-host', hostname);
|
||||
done();
|
||||
// Register non-serving static server so that the child services can use reply.sendFile.
|
||||
// `root` here is just a placeholder and each call must use its own `rootPath`.
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
||||
fastify.register(this.openApiServerService.createServer);
|
||||
fastify.register(this.fileServerService.createServer);
|
||||
fastify.register(this.activityPubServerService.createServer);
|
||||
fastify.register(this.nodeinfoServerService.createServer);
|
||||
@@ -142,13 +150,12 @@ export class ServerService implements OnApplicationShutdown {
|
||||
host: (host == null) || (host === this.config.host) ? IsNull() : host,
|
||||
isSuspended: false,
|
||||
},
|
||||
relations: ['avatar'],
|
||||
});
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
if (user) {
|
||||
reply.redirect(this.userEntityService.getAvatarUrlSync(user));
|
||||
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
|
||||
} else {
|
||||
reply.redirect('/static-assets/user-unknown.png');
|
||||
}
|
||||
|
@@ -75,7 +75,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : 500, err);
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
@@ -129,7 +129,7 @@ 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 : 500, err);
|
||||
this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err);
|
||||
});
|
||||
|
||||
if (user) {
|
||||
@@ -321,7 +321,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
|
||||
// API invoking
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
|
||||
if (err instanceof ApiError) {
|
||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
} else {
|
||||
const errId = uuid();
|
||||
|
@@ -167,7 +167,7 @@ export class ApiServerService {
|
||||
// Make sure any unknown path under /api returns HTTP 404 Not Found,
|
||||
// because otherwise ClientServerService will return the base client HTML
|
||||
// page with HTTP 200.
|
||||
fastify.get('*', (request, reply) => {
|
||||
fastify.get('/*', (request, reply) => {
|
||||
reply.code(404);
|
||||
// Mock ApiCallService.send's error handling
|
||||
reply.send({
|
||||
|
@@ -3,9 +3,9 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import type { App } from '@/models/entities/App.js';
|
||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import isNativeToken from '@/misc/is-native-token.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@@ -18,7 +18,7 @@ export class AuthenticationError extends Error {
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticateService {
|
||||
private appCache: Cache<App>;
|
||||
private appCache: MemoryKVCache<App>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@@ -30,9 +30,9 @@ export class AuthenticateService {
|
||||
@Inject(DI.appsRepository)
|
||||
private appsRepository: AppsRepository,
|
||||
|
||||
private userCacheService: UserCacheService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
this.appCache = new Cache<App>(Infinity);
|
||||
this.appCache = new MemoryKVCache<App>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -42,7 +42,7 @@ export class AuthenticateService {
|
||||
}
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token,
|
||||
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
||||
|
||||
if (user == null) {
|
||||
@@ -67,7 +67,7 @@ export class AuthenticateService {
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId,
|
||||
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
|
||||
() => this.usersRepository.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<LocalUser>);
|
||||
|
@@ -42,6 +42,7 @@ 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';
|
||||
import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
|
||||
import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js';
|
||||
import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js';
|
||||
import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
|
||||
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
|
||||
@@ -94,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
||||
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
||||
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
||||
import * as ep___channels_update from './endpoints/channels/update.js';
|
||||
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
@@ -114,6 +118,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
|
||||
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
||||
import * as ep___clips_show from './endpoints/clips/show.js';
|
||||
import * as ep___clips_update from './endpoints/clips/update.js';
|
||||
import * as ep___clips_favorite from './endpoints/clips/favorite.js';
|
||||
import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
|
||||
import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
|
||||
import * as ep___drive from './endpoints/drive.js';
|
||||
import * as ep___drive_files from './endpoints/drive/files.js';
|
||||
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
||||
@@ -213,6 +220,8 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
|
||||
import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||
import * as ep___i_update from './endpoints/i/update.js';
|
||||
import * as ep___i_move from './endpoints/i/move.js';
|
||||
import * as ep___i_knownAs from './endpoints/i/known-as.js';
|
||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
@@ -220,10 +229,14 @@ 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___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||
import * as ep___mute_list from './endpoints/mute/list.js';
|
||||
import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
|
||||
import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
|
||||
import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
|
||||
import * as ep___my_apps from './endpoints/my/apps.js';
|
||||
import * as ep___notes from './endpoints/notes.js';
|
||||
import * as ep___notes_children from './endpoints/notes/children.js';
|
||||
@@ -257,7 +270,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
||||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||
import * as ep___notifications_read from './endpoints/notifications/read.js';
|
||||
import * as ep___pagePush from './endpoints/page-push.js';
|
||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||
@@ -363,6 +375,7 @@ const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useCla
|
||||
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 };
|
||||
const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default };
|
||||
const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default };
|
||||
const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default };
|
||||
const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default };
|
||||
const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default };
|
||||
@@ -415,6 +428,9 @@ const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___c
|
||||
const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
|
||||
const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
|
||||
const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
|
||||
const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
|
||||
const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
|
||||
const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
|
||||
const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
|
||||
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
||||
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
||||
@@ -435,6 +451,9 @@ const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_l
|
||||
const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
|
||||
const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
|
||||
const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
|
||||
const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default };
|
||||
const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default };
|
||||
const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default };
|
||||
const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
|
||||
const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
|
||||
const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
|
||||
@@ -534,6 +553,8 @@ const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: e
|
||||
const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default };
|
||||
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
|
||||
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
|
||||
const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
|
||||
const $i_knownAs: Provider = { provide: 'ep:i/known-as', useClass: ep___i_knownAs.default };
|
||||
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
|
||||
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
|
||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||
@@ -541,10 +562,14 @@ const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass
|
||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.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 };
|
||||
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
|
||||
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
|
||||
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
|
||||
const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default };
|
||||
const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default };
|
||||
const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default };
|
||||
const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default };
|
||||
const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default };
|
||||
const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
|
||||
const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default };
|
||||
@@ -578,7 +603,6 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
|
||||
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
|
||||
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
|
||||
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
|
||||
const $notifications_read: Provider = { provide: 'ep:notifications/read', useClass: ep___notifications_read.default };
|
||||
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
|
||||
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
||||
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
|
||||
@@ -688,6 +712,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
$admin_queue_inboxDelayed,
|
||||
$admin_queue_promote,
|
||||
$admin_queue_stats,
|
||||
$admin_relays_add,
|
||||
$admin_relays_list,
|
||||
@@ -740,6 +765,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$channels_timeline,
|
||||
$channels_unfollow,
|
||||
$channels_update,
|
||||
$channels_favorite,
|
||||
$channels_unfavorite,
|
||||
$channels_myFavorites,
|
||||
$charts_activeUsers,
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
@@ -760,6 +788,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$clips_notes,
|
||||
$clips_show,
|
||||
$clips_update,
|
||||
$clips_favorite,
|
||||
$clips_unfavorite,
|
||||
$clips_myFavorites,
|
||||
$drive,
|
||||
$drive_files,
|
||||
$drive_files_attachedNotes,
|
||||
@@ -859,6 +890,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_unpin,
|
||||
$i_updateEmail,
|
||||
$i_update,
|
||||
$i_move,
|
||||
$i_knownAs,
|
||||
$i_webhooks_create,
|
||||
$i_webhooks_list,
|
||||
$i_webhooks_show,
|
||||
@@ -866,10 +899,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_delete,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
$miauth_genToken,
|
||||
$mute_create,
|
||||
$mute_delete,
|
||||
$mute_list,
|
||||
$renoteMute_create,
|
||||
$renoteMute_delete,
|
||||
$renoteMute_list,
|
||||
$my_apps,
|
||||
$notes,
|
||||
$notes_children,
|
||||
@@ -903,7 +940,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$notes_userListTimeline,
|
||||
$notifications_create,
|
||||
$notifications_markAllAsRead,
|
||||
$notifications_read,
|
||||
$pagePush,
|
||||
$pages_create,
|
||||
$pages_delete,
|
||||
@@ -1007,6 +1043,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
$admin_queue_inboxDelayed,
|
||||
$admin_queue_promote,
|
||||
$admin_queue_stats,
|
||||
$admin_relays_add,
|
||||
$admin_relays_list,
|
||||
@@ -1059,6 +1096,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$channels_timeline,
|
||||
$channels_unfollow,
|
||||
$channels_update,
|
||||
$channels_favorite,
|
||||
$channels_unfavorite,
|
||||
$channels_myFavorites,
|
||||
$charts_activeUsers,
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
@@ -1079,6 +1119,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$clips_notes,
|
||||
$clips_show,
|
||||
$clips_update,
|
||||
$clips_favorite,
|
||||
$clips_unfavorite,
|
||||
$clips_myFavorites,
|
||||
$drive,
|
||||
$drive_files,
|
||||
$drive_files_attachedNotes,
|
||||
@@ -1178,6 +1221,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_unpin,
|
||||
$i_updateEmail,
|
||||
$i_update,
|
||||
$i_move,
|
||||
$i_knownAs,
|
||||
$i_webhooks_create,
|
||||
$i_webhooks_list,
|
||||
$i_webhooks_show,
|
||||
@@ -1185,10 +1230,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_delete,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
$miauth_genToken,
|
||||
$mute_create,
|
||||
$mute_delete,
|
||||
$mute_list,
|
||||
$renoteMute_create,
|
||||
$renoteMute_delete,
|
||||
$renoteMute_list,
|
||||
$my_apps,
|
||||
$notes,
|
||||
$notes_children,
|
||||
@@ -1222,7 +1271,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$notes_userListTimeline,
|
||||
$notifications_create,
|
||||
$notifications_markAllAsRead,
|
||||
$notifications_read,
|
||||
$pagePush,
|
||||
$pages_create,
|
||||
$pages_delete,
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||
@@ -15,6 +15,7 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class SignupApiService {
|
||||
@@ -31,6 +32,9 @@ export class SignupApiService {
|
||||
@Inject(DI.userPendingsRepository)
|
||||
private userPendingsRepository: UserPendingsRepository,
|
||||
|
||||
@Inject(DI.usedUsernamesRepository)
|
||||
private usedUsernamesRepository: UsedUsernamesRepository,
|
||||
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
@@ -124,12 +128,21 @@ export class SignupApiService {
|
||||
}
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
||||
}
|
||||
|
||||
const code = rndstr('a-z0-9', 16);
|
||||
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
|
||||
await this.userPendingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
@@ -138,13 +151,13 @@ export class SignupApiService {
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
||||
|
||||
this.emailService.sendEmail(emailAddress!, 'Signup',
|
||||
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
|
@@ -3,17 +3,18 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import * as websocket from 'websocket';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { AuthenticateService } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
import type { ParsedUrlQuery } from 'querystring';
|
||||
import type * as http from 'node:http';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class StreamingApiServerService {
|
||||
@@ -21,8 +22,8 @@ export class StreamingApiServerService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@@ -33,6 +34,9 @@ export class StreamingApiServerService {
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@@ -42,7 +46,7 @@ export class StreamingApiServerService {
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private cacheService: CacheService,
|
||||
private noteReadService: NoteReadService,
|
||||
private authenticateService: AuthenticateService,
|
||||
private channelsService: ChannelsService,
|
||||
@@ -70,8 +74,6 @@ export class StreamingApiServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = request.accept();
|
||||
|
||||
const ev = new EventEmitter();
|
||||
|
||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||
@@ -79,21 +81,22 @@ export class StreamingApiServerService {
|
||||
ev.emit(parsed.channel, parsed.message);
|
||||
}
|
||||
|
||||
this.redisSubscriber.on('message', onRedisMessage);
|
||||
this.redisForPubsub.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.followingsRepository,
|
||||
this.mutingsRepository,
|
||||
this.blockingsRepository,
|
||||
this.channelFollowingsRepository,
|
||||
this.userProfilesRepository,
|
||||
this.channelsService,
|
||||
this.globalEventService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
connection, ev, user, miapp,
|
||||
this.cacheService,
|
||||
ev, user, miapp,
|
||||
);
|
||||
|
||||
await main.init();
|
||||
|
||||
const connection = request.accept();
|
||||
|
||||
main.init2(connection);
|
||||
|
||||
const intervalId = user ? setInterval(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
@@ -108,7 +111,7 @@ export class StreamingApiServerService {
|
||||
connection.once('close', () => {
|
||||
ev.removeAllListeners();
|
||||
main.dispose();
|
||||
this.redisSubscriber.off('message', onRedisMessage);
|
||||
this.redisForPubsub.off('message', onRedisMessage);
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'node:fs';
|
||||
import Ajv from 'ajv';
|
||||
import type { Schema, SchemaType } from '@/misc/schema.js';
|
||||
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';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { Schema } from '@/misc/schema.js';
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { RolePolicies } from '@/core/RoleService.js';
|
||||
|
||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||
@@ -42,6 +42,7 @@ 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';
|
||||
import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
|
||||
import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js';
|
||||
import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js';
|
||||
import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
|
||||
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
|
||||
@@ -94,6 +95,9 @@ import * as ep___channels_show from './endpoints/channels/show.js';
|
||||
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
|
||||
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
|
||||
import * as ep___channels_update from './endpoints/channels/update.js';
|
||||
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
|
||||
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
|
||||
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
|
||||
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
@@ -114,6 +118,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
|
||||
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
||||
import * as ep___clips_show from './endpoints/clips/show.js';
|
||||
import * as ep___clips_update from './endpoints/clips/update.js';
|
||||
import * as ep___clips_favorite from './endpoints/clips/favorite.js';
|
||||
import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
|
||||
import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
|
||||
import * as ep___drive from './endpoints/drive.js';
|
||||
import * as ep___drive_files from './endpoints/drive/files.js';
|
||||
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
||||
@@ -213,6 +220,8 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
|
||||
import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||
import * as ep___i_update from './endpoints/i/update.js';
|
||||
import * as ep___i_move from './endpoints/i/move.js';
|
||||
import * as ep___i_knownAs from './endpoints/i/known-as.js';
|
||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
@@ -220,10 +229,14 @@ 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___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||
import * as ep___mute_list from './endpoints/mute/list.js';
|
||||
import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
|
||||
import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
|
||||
import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
|
||||
import * as ep___my_apps from './endpoints/my/apps.js';
|
||||
import * as ep___notes from './endpoints/notes.js';
|
||||
import * as ep___notes_children from './endpoints/notes/children.js';
|
||||
@@ -257,7 +270,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
|
||||
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||
import * as ep___notifications_create from './endpoints/notifications/create.js';
|
||||
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
|
||||
import * as ep___notifications_read from './endpoints/notifications/read.js';
|
||||
import * as ep___pagePush from './endpoints/page-push.js';
|
||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||
@@ -361,6 +373,7 @@ const eps = [
|
||||
['admin/queue/clear', ep___admin_queue_clear],
|
||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||
['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed],
|
||||
['admin/queue/promote', ep___admin_queue_promote],
|
||||
['admin/queue/stats', ep___admin_queue_stats],
|
||||
['admin/relays/add', ep___admin_relays_add],
|
||||
['admin/relays/list', ep___admin_relays_list],
|
||||
@@ -413,6 +426,9 @@ const eps = [
|
||||
['channels/timeline', ep___channels_timeline],
|
||||
['channels/unfollow', ep___channels_unfollow],
|
||||
['channels/update', ep___channels_update],
|
||||
['channels/favorite', ep___channels_favorite],
|
||||
['channels/unfavorite', ep___channels_unfavorite],
|
||||
['channels/my-favorites', ep___channels_myFavorites],
|
||||
['charts/active-users', ep___charts_activeUsers],
|
||||
['charts/ap-request', ep___charts_apRequest],
|
||||
['charts/drive', ep___charts_drive],
|
||||
@@ -433,6 +449,9 @@ const eps = [
|
||||
['clips/notes', ep___clips_notes],
|
||||
['clips/show', ep___clips_show],
|
||||
['clips/update', ep___clips_update],
|
||||
['clips/favorite', ep___clips_favorite],
|
||||
['clips/unfavorite', ep___clips_unfavorite],
|
||||
['clips/my-favorites', ep___clips_myFavorites],
|
||||
['drive', ep___drive],
|
||||
['drive/files', ep___drive_files],
|
||||
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
||||
@@ -532,6 +551,8 @@ const eps = [
|
||||
['i/unpin', ep___i_unpin],
|
||||
['i/update-email', ep___i_updateEmail],
|
||||
['i/update', ep___i_update],
|
||||
//['i/move', ep___i_move],
|
||||
//['i/known-as', ep___i_knownAs],
|
||||
['i/webhooks/create', ep___i_webhooks_create],
|
||||
['i/webhooks/list', ep___i_webhooks_list],
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
@@ -539,10 +560,14 @@ const eps = [
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['meta', ep___meta],
|
||||
['emojis', ep___emojis],
|
||||
['emoji', ep___emoji],
|
||||
['miauth/gen-token', ep___miauth_genToken],
|
||||
['mute/create', ep___mute_create],
|
||||
['mute/delete', ep___mute_delete],
|
||||
['mute/list', ep___mute_list],
|
||||
['renote-mute/create', ep___renoteMute_create],
|
||||
['renote-mute/delete', ep___renoteMute_delete],
|
||||
['renote-mute/list', ep___renoteMute_list],
|
||||
['my/apps', ep___my_apps],
|
||||
['notes', ep___notes],
|
||||
['notes/children', ep___notes_children],
|
||||
@@ -576,7 +601,6 @@ const eps = [
|
||||
['notes/user-list-timeline', ep___notes_userListTimeline],
|
||||
['notifications/create', ep___notifications_create],
|
||||
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
||||
['notifications/read', ep___notifications_read],
|
||||
['page-push', ep___pagePush],
|
||||
['pages/create', ep___pages_create],
|
||||
['pages/delete', ep___pages_delete],
|
||||
|
@@ -61,11 +61,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// Terminate streaming
|
||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -26,38 +22,14 @@ export const paramDef = {
|
||||
required: ['ids', 'aliases'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
||||
});
|
||||
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -56,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
category: null,
|
||||
aliases: [],
|
||||
host: null,
|
||||
license: null,
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||
|
@@ -87,10 +87,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
license: emoji.license,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
||||
});
|
||||
|
@@ -1,11 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -24,38 +19,14 @@ export const paramDef = {
|
||||
required: ['ids'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||
emoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(emojis),
|
||||
});
|
||||
await this.customEmojiService.deleteBulk(ps.ids);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -31,38 +25,14 @@ export const paramDef = {
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||
emoji: emoji,
|
||||
});
|
||||
await this.customEmojiService.delete(ps.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -26,38 +22,14 @@ export const paramDef = {
|
||||
required: ['ids', 'aliases'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
||||
});
|
||||
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -26,34 +22,14 @@ export const paramDef = {
|
||||
required: ['ids', 'aliases'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ps.ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
aliases: ps.aliases,
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
||||
});
|
||||
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -28,34 +24,14 @@ export const paramDef = {
|
||||
required: ['ids'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ps.ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
category: ps.category,
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
||||
});
|
||||
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -19,6 +15,11 @@ export const meta = {
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||
},
|
||||
sameNameEmojiExists: {
|
||||
message: 'Emoji that have same name already exists.',
|
||||
code: 'SAME_NAME_EMOJI_EXISTS',
|
||||
id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -26,7 +27,7 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
@@ -35,54 +36,24 @@ export const paramDef = {
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['id', 'name', 'aliases'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
name: ps.name,
|
||||
category: ps.category,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases,
|
||||
license: ps.license ?? null,
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
const updated = await this.emojiEntityService.packDetailed(emoji.id);
|
||||
|
||||
if (emoji.name === ps.name) {
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: [updated],
|
||||
});
|
||||
} else {
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||
});
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: updated,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -110,6 +110,14 @@ export const meta = {
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
sensitiveWords: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
hcaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
@@ -231,6 +239,14 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
enableChartsForRemoteUser: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableChartsForFederatedInstances: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
policies: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
@@ -266,7 +282,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
uri: this.config.url,
|
||||
description: instance.description,
|
||||
langs: instance.langs,
|
||||
tosUrl: instance.ToSUrl,
|
||||
tosUrl: instance.termsOfServiceUrl,
|
||||
repositoryUrl: instance.repositoryUrl,
|
||||
feedbackUrl: instance.feedbackUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
@@ -290,13 +306,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
enableEmail: instance.enableEmail,
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
useStarForReactionFallback: instance.useStarForReactionFallback,
|
||||
pinnedUsers: instance.pinnedUsers,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
sensitiveWords: instance.sensitiveWords,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
turnstileSecretKey: instance.turnstileSecretKey,
|
||||
@@ -330,6 +344,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
deeplIsPro: instance.deeplIsPro,
|
||||
enableIpLogging: instance.enableIpLogging,
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
};
|
||||
});
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', enum: ['deliver', 'inbox'] },
|
||||
},
|
||||
required: ['type'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let delayedQueues;
|
||||
|
||||
switch (ps.type) {
|
||||
case 'deliver':
|
||||
delayedQueues = await this.queueService.deliverQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
await queue.promote();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'inbox':
|
||||
delayedQueues = await this.queueService.inboxQueue.getDelayed();
|
||||
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
|
||||
const queue = delayedQueues[queueIndex];
|
||||
await queue.promote();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'promoteQueue');
|
||||
});
|
||||
}
|
||||
}
|
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox);
|
||||
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
|
@@ -27,6 +27,7 @@ export const paramDef = {
|
||||
isAdministrator: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
type: 'object',
|
||||
},
|
||||
@@ -43,6 +44,7 @@ export const paramDef = {
|
||||
'isAdministrator',
|
||||
'asBadge',
|
||||
'canEditMembersByModerator',
|
||||
'displayOrder',
|
||||
'policies',
|
||||
],
|
||||
} as const;
|
||||
@@ -76,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
isModerator: ps.isModerator,
|
||||
asBadge: ps.asBadge,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
displayOrder: ps.displayOrder,
|
||||
policies: ps.policies,
|
||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
|
@@ -35,6 +35,7 @@ export const paramDef = {
|
||||
isAdministrator: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
type: 'object',
|
||||
},
|
||||
@@ -52,6 +53,7 @@ export const paramDef = {
|
||||
'isAdministrator',
|
||||
'asBadge',
|
||||
'canEditMembersByModerator',
|
||||
'displayOrder',
|
||||
'policies',
|
||||
],
|
||||
} as const;
|
||||
@@ -85,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
isAdministrator: ps.isAdministrator,
|
||||
asBadge: ps.asBadge,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
displayOrder: ps.displayOrder,
|
||||
policies: ps.policies,
|
||||
});
|
||||
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
@@ -36,9 +36,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.notificationsRepository)
|
||||
private notificationsRepository: NotificationsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private userSuspendService: UserSuspendService,
|
||||
@@ -65,15 +62,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
targetId: user.id,
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
await this.readAllNotify(user).catch(e => {});
|
||||
})();
|
||||
});
|
||||
}
|
||||
@@ -96,14 +87,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
await this.userFollowingService.unfollow(follower, followee, true);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async readAllNotify(notifier: User) {
|
||||
await this.notificationsRepository.update({
|
||||
notifierId: notifier.id,
|
||||
isRead: false,
|
||||
}, {
|
||||
isRead: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
disableRegistration: { type: 'boolean', nullable: true },
|
||||
useStarForReactionFallback: { type: 'boolean', nullable: true },
|
||||
pinnedUsers: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -27,6 +26,9 @@ export const paramDef = {
|
||||
blockedHosts: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
sensitiveWords: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
mascotImageUrl: { type: 'string', nullable: true },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
@@ -56,10 +58,6 @@ export const paramDef = {
|
||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
maintainerName: { type: 'string', nullable: true },
|
||||
maintainerEmail: { type: 'string', nullable: true },
|
||||
pinnedPages: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
pinnedClipId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
langs: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -94,6 +92,8 @@ export const paramDef = {
|
||||
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
||||
enableIpLogging: { type: 'boolean' },
|
||||
enableActiveEmailValidation: { type: 'boolean' },
|
||||
enableChartsForRemoteUser: { type: 'boolean' },
|
||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -115,10 +115,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.disableRegistration = ps.disableRegistration;
|
||||
}
|
||||
|
||||
if (typeof ps.useStarForReactionFallback === 'boolean') {
|
||||
set.useStarForReactionFallback = ps.useStarForReactionFallback;
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.pinnedUsers)) {
|
||||
set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
|
||||
}
|
||||
@@ -131,6 +127,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.sensitiveWords)) {
|
||||
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
||||
}
|
||||
|
||||
if (ps.themeColor !== undefined) {
|
||||
set.themeColor = ps.themeColor;
|
||||
}
|
||||
@@ -247,14 +247,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.langs = ps.langs.filter(Boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.pinnedPages)) {
|
||||
set.pinnedPages = ps.pinnedPages.filter(Boolean);
|
||||
}
|
||||
|
||||
if (ps.pinnedClipId !== undefined) {
|
||||
set.pinnedClipId = ps.pinnedClipId;
|
||||
}
|
||||
|
||||
if (ps.summalyProxy !== undefined) {
|
||||
set.summalyProxy = ps.summalyProxy;
|
||||
}
|
||||
@@ -304,7 +296,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
if (ps.tosUrl !== undefined) {
|
||||
set.ToSUrl = ps.tosUrl;
|
||||
set.termsOfServiceUrl = ps.tosUrl;
|
||||
}
|
||||
|
||||
if (ps.repositoryUrl !== undefined) {
|
||||
@@ -387,6 +379,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
||||
}
|
||||
|
||||
if (ps.enableChartsForRemoteUser !== undefined) {
|
||||
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
||||
}
|
||||
|
||||
if (ps.enableChartsForFederatedInstances !== undefined) {
|
||||
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
|
||||
}
|
||||
|
||||
await this.metaService.update(set);
|
||||
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
||||
});
|
||||
|
@@ -79,6 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) {
|
||||
throw new Error('invalid param');
|
||||
}
|
||||
|
||||
const currentAntennasCount = await this.antennasRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
@@ -99,9 +103,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const antenna = await this.antennasRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
createdAt: now,
|
||||
lastUsedAt: now,
|
||||
userId: me.id,
|
||||
name: ps.name,
|
||||
src: ps.src,
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
|
||||
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -50,15 +52,16 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.antennaNotesRepository)
|
||||
private antennaNotesRepository: AntennaNotesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private noteReadService: NoteReadService,
|
||||
@@ -73,34 +76,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||
'-',
|
||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
||||
|
||||
if (noteIdsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
|
||||
const notes = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
const notes = await query.getMany();
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
if (notes.length > 0) {
|
||||
this.noteReadService.read(me.id, notes);
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import type { UsersRepository, NotesRepository } from '@/models/index.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
|
||||
import type { SchemaType } from '@/misc/schema.js';
|
||||
import type { SchemaType } from '@/misc/json-schema.js';
|
||||
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
@@ -0,0 +1,61 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '4938f5f3-6167-4c04-9149-6607b7542861',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['channelId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
@Inject(DI.channelFavoritesRepository)
|
||||
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await this.channelFavoritesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: me.id,
|
||||
channelId: channel.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -41,7 +41,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
@@ -58,8 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
followerId: me.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(me.id, 'followChannel', channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelFavoritesRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:channels',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Channel',
|
||||
},
|
||||
},
|
||||
} 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.channelFavoritesRepository)
|
||||
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||
|
||||
private channelEntityService: ChannelEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.channelFavoritesRepository.createQueryBuilder('favorite')
|
||||
.andWhere('favorite.userId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('favorite.channel', 'channel');
|
||||
|
||||
const favorites = await query
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(favorites.map(x => this.channelEntityService.pack(x.channel!, me)));
|
||||
});
|
||||
}
|
||||
}
|
@@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
return await this.channelEntityService.pack(channel, me);
|
||||
return await this.channelEntityService.pack(channel, me, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
||||
import type { ChannelsRepository, Note, NotesRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -48,12 +50,16 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
@@ -67,30 +73,60 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
let timeline: Note[] = [];
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
`channelTimeline:${channel.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||
'-',
|
||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
||||
|
||||
if (noteIdsRes.length === 0) {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
timeline = await query.take(ps.limit).getMany();
|
||||
} else {
|
||||
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
timeline = await query.getMany();
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
if (me) this.activeUsersChart.read(me);
|
||||
|
||||
|
@@ -0,0 +1,56 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '353c68dd-131a-476c-aa99-88a345e83668',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['channelId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
@Inject(DI.channelFavoritesRepository)
|
||||
private channelFavoritesRepository: ChannelFavoritesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await this.channelFavoritesRepository.delete({
|
||||
userId: me.id,
|
||||
channelId: channel.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -38,8 +38,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
@@ -54,8 +52,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
followerId: me.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -46,6 +47,12 @@ export const paramDef = {
|
||||
name: { type: 'string', minLength: 1, maxLength: 128 },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
pinnedNoteIds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['channelId'],
|
||||
} as const;
|
||||
@@ -61,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private channelEntityService: ChannelEntityService,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
@@ -71,7 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
if (channel.userId !== me.id) {
|
||||
const iAmModerator = await this.roleService.isModerator(me);
|
||||
if (channel.userId !== me.id && !iAmModerator) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
@@ -93,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
await this.channelsRepository.update(channel.id, {
|
||||
...(ps.name !== undefined ? { name: ps.name } : {}),
|
||||
...(ps.description !== undefined ? { description: ps.description } : {}),
|
||||
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
|
||||
...(banner ? { bannerId: banner.id } : {}),
|
||||
});
|
||||
|
||||
|
@@ -106,6 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
});
|
||||
|
||||
await this.clipsRepository.update(clip.id, {
|
||||
lastClippedAt: new Date(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
description: ps.description,
|
||||
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await this.clipEntityService.pack(clip);
|
||||
return await this.clipEntityService.pack(clip, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
76
packages/backend/src/server/api/endpoints/clips/favorite.ts
Normal file
76
packages/backend/src/server/api/endpoints/clips/favorite.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clip'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:clip-favorite',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
|
||||
},
|
||||
|
||||
alreadyFavorited: {
|
||||
message: 'The clip has already been favorited.',
|
||||
code: 'ALREADY_FAVORITED',
|
||||
id: '92658936-c625-4273-8326-2d790129256e',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clipId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['clipId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipFavoritesRepository)
|
||||
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if ((clip.userId !== me.id) && !clip.isPublic) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
await this.clipFavoritesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
|
||||
return await this.clipEntityService.packMany(clips, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ClipFavoritesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'clip'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:clip-favorite',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Clip',
|
||||
},
|
||||
},
|
||||
} 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.clipFavoritesRepository)
|
||||
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||
|
||||
private clipEntityService: ClipEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.clipFavoritesRepository.createQueryBuilder('favorite')
|
||||
.andWhere('favorite.userId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('favorite.clip', 'clip');
|
||||
|
||||
const favorites = await query
|
||||
.getMany();
|
||||
|
||||
return this.clipEntityService.packMany(favorites.map(x => x.clip!), me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -75,16 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
|
||||
|
||||
if (me) {
|
||||
|
@@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
return await this.clipEntityService.pack(clip);
|
||||
return await this.clipEntityService.pack(clip, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,65 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clip'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:clip-favorite',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: '2603966e-b865-426c-94a7-af4a01241dc1',
|
||||
},
|
||||
|
||||
notFavorited: {
|
||||
message: 'You have not favorited the clip.',
|
||||
code: 'NOT_FAVORITED',
|
||||
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clipId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['clipId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipFavoritesRepository)
|
||||
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notFavorited);
|
||||
}
|
||||
|
||||
await this.clipFavoritesRepository.delete(exist.id);
|
||||
});
|
||||
}
|
||||
}
|
@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
isPublic: ps.isPublic,
|
||||
});
|
||||
|
||||
return await this.clipEntityService.pack(clip.id);
|
||||
return await this.clipEntityService.pack(clip.id, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ export const paramDef = {
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
|
||||
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -63,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break;
|
||||
case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break;
|
||||
case '+name': query.orderBy('file.name', 'DESC'); break;
|
||||
case '-name': query.orderBy('file.name', 'ASC'); break;
|
||||
case '+size': query.orderBy('file.size', 'DESC'); break;
|
||||
case '-size': query.orderBy('file.size', 'ASC'); break;
|
||||
}
|
||||
|
||||
const files = await query.take(ps.limit).getMany();
|
||||
|
||||
return await this.driveFileEntityService.packMany(files, { detail: false, self: true });
|
||||
|
56
packages/backend/src/server/api/endpoints/emoji.ts
Normal file
56
packages/backend/src/server/api/endpoints/emoji.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 3600,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'EmojiDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emoji = await this.emojisRepository.findOneOrFail({
|
||||
where: {
|
||||
name: ps.name,
|
||||
host: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
return this.emojiEntityService.packDetailed(emoji);
|
||||
});
|
||||
}
|
||||
}
|
@@ -23,24 +23,7 @@ export const meta = {
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
aliases: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
ref: 'EmojiSimple',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,10 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
category: 'ASC',
|
||||
name: 'ASC',
|
||||
},
|
||||
cache: {
|
||||
id: 'meta_emojis',
|
||||
milliseconds: 3600000, // 1 hour
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -76,9 +76,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (typeof ps.blocked === 'boolean') {
|
||||
const meta = await this.metaService.fetch(true);
|
||||
if (ps.blocked) {
|
||||
query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
|
||||
query.andWhere(meta.blockedHosts.length === 0 ? '1=0' : 'instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
|
||||
} else {
|
||||
query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
|
||||
query.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
@@ -14,6 +15,15 @@ export const meta = {
|
||||
optional: false, nullable: false,
|
||||
ref: 'MeDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
userIsDeleted: {
|
||||
message: 'User is deleted.',
|
||||
code: 'USER_IS_DELETED',
|
||||
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
|
||||
kind: 'permission',
|
||||
},
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
@@ -41,13 +51,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
||||
|
||||
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
|
||||
const userProfile = await this.userProfilesRepository.findOneOrFail({
|
||||
const userProfile = await this.userProfilesRepository.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (userProfile == null) {
|
||||
throw new ApiError(meta.errors.userIsDeleted);
|
||||
}
|
||||
|
||||
if (!userProfile.loggedInDates.includes(today)) {
|
||||
this.userProfilesRepository.update({ userId: user.id }, {
|
||||
loggedInDates: [...userProfile.loggedInDates, today],
|
||||
|
92
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
92
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
|
||||
import { User } from '@/models/entities/User.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 30,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||
},
|
||||
notRemote: {
|
||||
message: 'User is not remote. You can only migrate from other instances.',
|
||||
code: 'NOT_REMOTE',
|
||||
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
||||
},
|
||||
uriNull: {
|
||||
message: 'User ActivityPup URI is null.',
|
||||
code: 'URI_NULL',
|
||||
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
alsoKnownAs: { type: 'string' },
|
||||
},
|
||||
required: ['alsoKnownAs'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Check parameter
|
||||
if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
|
||||
|
||||
let unfiltered = ps.alsoKnownAs;
|
||||
const updates = {} as Partial<User>;
|
||||
|
||||
if (!unfiltered) {
|
||||
updates.alsoKnownAs = null;
|
||||
} else {
|
||||
// Parse user's input into the old account
|
||||
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
|
||||
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
||||
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
||||
|
||||
const userAddress = unfiltered.split('@');
|
||||
// Retrieve the old account
|
||||
const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
|
||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
});
|
||||
|
||||
const toUrl: string | null = knownAs.uri;
|
||||
if (!toUrl) throw new ApiError(meta.errors.uriNull);
|
||||
// Only allow moving from a remote account
|
||||
if (this.userEntityService.isLocalUser(knownAs)) throw new ApiError(meta.errors.notRemote);
|
||||
|
||||
updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
|
||||
}
|
||||
|
||||
return await this.accountMoveService.createAlias(me, updates);
|
||||
});
|
||||
}
|
||||
}
|
140
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
140
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 5,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMoveTarget: {
|
||||
message: 'No such move target.',
|
||||
code: 'NO_SUCH_MOVE_TARGET',
|
||||
id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
|
||||
},
|
||||
remoteAccountForbids: {
|
||||
message:
|
||||
'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
|
||||
code: 'REMOTE_ACCOUNT_FORBIDS',
|
||||
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
|
||||
},
|
||||
notRemote: {
|
||||
message: 'User is not remote. You can only migrate to other instances.',
|
||||
code: 'NOT_REMOTE',
|
||||
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
||||
},
|
||||
rootForbidden: {
|
||||
message: 'The root can\'t migrate.',
|
||||
code: 'NOT_ROOT_FORBIDDEN',
|
||||
id: '4362e8dc-731f-4ad8-a694-be2a88922a24',
|
||||
},
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||
},
|
||||
uriNull: {
|
||||
message: 'User ActivityPup URI is null.',
|
||||
code: 'URI_NULL',
|
||||
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
||||
},
|
||||
localUriNull: {
|
||||
message: 'Local User ActivityPup URI is null.',
|
||||
code: 'URI_NULL',
|
||||
id: '95ba11b9-90e8-43a5-ba16-7acc1ab32e71',
|
||||
},
|
||||
alreadyMoved: {
|
||||
message: 'Account was already moved to another account.',
|
||||
code: 'ALREADY_MOVED',
|
||||
id: 'b234a14e-9ebe-4581-8000-074b3c215962',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moveToAccount: { type: 'string' },
|
||||
},
|
||||
required: ['moveToAccount'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private getterService: GetterService,
|
||||
private apPersonService: ApPersonService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// check parameter
|
||||
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||
// abort if user is the root
|
||||
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
|
||||
// abort if user has already moved
|
||||
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
|
||||
|
||||
let unfiltered = ps.moveToAccount;
|
||||
if (!unfiltered) throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||
|
||||
// parse user's input into the destination account
|
||||
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
|
||||
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
||||
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
||||
|
||||
const userAddress = unfiltered.split('@');
|
||||
// retrieve the destination account
|
||||
let moveTo = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
|
||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
||||
throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||
});
|
||||
const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id);
|
||||
if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull);
|
||||
|
||||
// update local db
|
||||
await this.apPersonService.updatePerson(remoteMoveTo.uri);
|
||||
// retrieve updated user
|
||||
moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri);
|
||||
// only allow moving to a remote account
|
||||
if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote);
|
||||
|
||||
let allowed = false;
|
||||
|
||||
const fromUrl = `${this.config.url}/users/${me.id}`;
|
||||
// make sure that the user has indicated the old account as an alias
|
||||
moveTo.alsoKnownAs?.forEach((elem) => {
|
||||
if (fromUrl.includes(elem)) allowed = true;
|
||||
});
|
||||
|
||||
// abort if unintended
|
||||
if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids);
|
||||
|
||||
return await this.accountMoveService.moveToRemote(me, moveTo);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Brackets, In } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js';
|
||||
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
@@ -8,6 +9,8 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Notification } from '@/models/entities/Notification.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'notifications'],
|
||||
@@ -38,8 +41,6 @@ export const paramDef = {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
following: { type: 'boolean', default: false },
|
||||
unreadOnly: { type: 'boolean', default: false },
|
||||
markAsRead: { type: 'boolean', default: true },
|
||||
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||
includeTypes: { type: 'array', items: {
|
||||
@@ -56,21 +57,22 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.notificationsRepository)
|
||||
private notificationsRepository: NotificationsRepository,
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private notificationEntityService: NotificationEntityService,
|
||||
private notificationService: NotificationService,
|
||||
private queryService: QueryService,
|
||||
@@ -89,85 +91,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
const notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||
'-',
|
||||
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
||||
|
||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||
.select('user_profile.mutedInstances')
|
||||
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||
|
||||
const suspendedQuery = this.usersRepository.createQueryBuilder('users')
|
||||
.select('users.id')
|
||||
.where('users.isSuspended = TRUE');
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||
.andWhere('notification.notifieeId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('notification.notifier', 'notifier')
|
||||
.leftJoinAndSelect('notification.note', 'note')
|
||||
.leftJoinAndSelect('notifier.avatar', 'notifierAvatar')
|
||||
.leftJoinAndSelect('notifier.banner', 'notifierBanner')
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
// muted users
|
||||
query.andWhere(new Brackets(qb => { qb
|
||||
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
|
||||
.orWhere('notification.notifierId IS NULL');
|
||||
}));
|
||||
query.setParameters(mutingQuery.getParameters());
|
||||
|
||||
// muted instances
|
||||
query.andWhere(new Brackets(qb => { qb
|
||||
.andWhere('notifier.host IS NULL')
|
||||
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
|
||||
}));
|
||||
query.setParameters(mutingInstanceQuery.getParameters());
|
||||
|
||||
// suspended users
|
||||
query.andWhere(new Brackets(qb => { qb
|
||||
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
|
||||
.orWhere('notification.notifierId IS NULL');
|
||||
}));
|
||||
|
||||
if (ps.following) {
|
||||
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id });
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
|
||||
if (ps.unreadOnly) {
|
||||
query.andWhere('notification.isRead = false');
|
||||
if (notifications.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const notifications = await query.take(ps.limit).getMany();
|
||||
|
||||
// Mark all as read
|
||||
if (notifications.length > 0 && ps.markAsRead) {
|
||||
this.notificationService.readNotification(me.id, notifications.map(x => x.id));
|
||||
if (ps.markAsRead) {
|
||||
this.notificationService.readAllNotification(me.id);
|
||||
}
|
||||
|
||||
const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
|
||||
const noteIds = notifications
|
||||
.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||
.map(notification => notification.noteId!);
|
||||
|
||||
if (notes.length > 0) {
|
||||
if (noteIds.length > 0) {
|
||||
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
|
||||
this.noteReadService.read(me.id, notes);
|
||||
}
|
||||
|
||||
|
@@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
const oldToken = freshUser.token;
|
||||
const oldToken = freshUser.token!;
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
@@ -54,11 +54,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
// Publish event
|
||||
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
||||
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
|
||||
|
||||
// Terminate streaming
|
||||
setTimeout(() => {
|
||||
this.globalEventService.publishUserEvent(me.id, 'terminate', {});
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -35,9 +35,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
id: ps.tokenId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
this.globalEventService.publishUserEvent(me.id, 'terminate');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||
import { HashtagService } from '@/core/HashtagService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -147,11 +149,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private accountUpdateService: AccountUpdateService,
|
||||
private hashtagService: HashtagService,
|
||||
private roleService: RoleService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _user, token) => {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
|
||||
@@ -168,8 +172,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
||||
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
||||
if (ps.mutedWords !== undefined) {
|
||||
// TODO: ちゃんと数える
|
||||
const length = JSON.stringify(ps.mutedWords).length;
|
||||
@@ -215,6 +217,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
|
||||
if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
|
||||
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
|
||||
if (ps.bannerId) {
|
||||
@@ -222,6 +228,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
|
||||
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
|
||||
|
||||
updates.bannerId = banner.id;
|
||||
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
|
||||
if (ps.pinnedPageId) {
|
||||
@@ -276,9 +286,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
includeSecrets: isSecure,
|
||||
});
|
||||
|
||||
const updatedProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
this.cacheService.userProfileCache.set(user.id, updatedProfile);
|
||||
|
||||
// Publish meUpdated event
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
|
||||
this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneByOrFail({ userId: user.id }));
|
||||
|
||||
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
||||
if (user.isLocked && ps.isLocked === false) {
|
||||
|
@@ -276,7 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
uri: this.config.url,
|
||||
description: instance.description,
|
||||
langs: instance.langs,
|
||||
tosUrl: instance.ToSUrl,
|
||||
tosUrl: instance.termsOfServiceUrl,
|
||||
repositoryUrl: instance.repositoryUrl,
|
||||
feedbackUrl: instance.feedbackUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
@@ -315,8 +315,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
mediaProxy: this.config.mediaProxy,
|
||||
|
||||
...(ps.detail ? {
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
requireSetup: (await this.usersRepository.countBy({
|
||||
host: IsNull(),
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MutingsRepository } from '@/models/index.js';
|
||||
import type { Muting } from '@/models/entities/Muting.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -62,9 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private getterService: GetterService,
|
||||
private idService: IdService,
|
||||
private userMutingService: UserMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
@@ -94,16 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await this.mutingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as Muting);
|
||||
|
||||
this.globalEventService.publishUserEvent(me.id, 'mute', mutee);
|
||||
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { MutingsRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private userMutingService: UserMutingService,
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -76,12 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await this.mutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(me.id, 'unmute', mutee);
|
||||
await this.userMutingService.unmute([exist]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -49,16 +49,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (ps.local) {
|
||||
query.andWhere('note.userHost IS NULL');
|
||||
|
@@ -57,16 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}));
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
|
@@ -4,8 +4,8 @@ import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.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: ['clips', 'notes'],
|
||||
@@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
|
||||
return await this.clipEntityService.packMany(clips, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -97,6 +97,7 @@ export const paramDef = {
|
||||
} },
|
||||
cw: { type: 'string', nullable: true, maxLength: 100 },
|
||||
localOnly: { type: 'boolean', default: false },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null },
|
||||
noExtractMentions: { type: 'boolean', default: false },
|
||||
noExtractHashtags: { type: 'boolean', default: false },
|
||||
noExtractEmojis: { type: 'boolean', default: false },
|
||||
@@ -110,7 +111,7 @@ export const paramDef = {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
nullable: false
|
||||
nullable: false,
|
||||
},
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
@@ -280,6 +281,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
renote,
|
||||
cw: ps.cw,
|
||||
localOnly: ps.localOnly,
|
||||
reactionAcceptance: ps.reactionAcceptance,
|
||||
visibility: ps.visibility,
|
||||
visibleUsers,
|
||||
channel,
|
||||
|
@@ -53,16 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
|
||||
|
||||
@@ -71,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
let notes = await query
|
||||
.orderBy('note.score', 'DESC')
|
||||
.take(50)
|
||||
.take(100)
|
||||
.getMany();
|
||||
|
||||
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
@@ -73,22 +73,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
@@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -69,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
@@ -83,22 +85,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
|
||||
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
@@ -107,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
@@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -65,6 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
@@ -75,19 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
@@ -95,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
@@ -60,16 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
|
@@ -8,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
import { PollService } from '@/core/PollService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
@@ -89,7 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private pollService: PollService,
|
||||
private apRendererService: ApRendererService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -161,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (note.userHost != null) {
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser;
|
||||
|
||||
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox);
|
||||
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false);
|
||||
}
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
|
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
order: {
|
||||
id: -1,
|
||||
},
|
||||
relations: ['user', 'user.avatar', 'user.banner', 'note'],
|
||||
relations: ['user', 'note'],
|
||||
});
|
||||
|
||||
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
|
||||
|
@@ -4,8 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.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: ['notes'],
|
||||
@@ -62,16 +62,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.renoteId = :renoteId', { renoteId: note.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
|
@@ -46,16 +46,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
|
@@ -71,16 +71,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
|
@@ -6,6 +6,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -23,6 +25,11 @@ export const meta = {
|
||||
},
|
||||
|
||||
errors: {
|
||||
unavailable: {
|
||||
message: 'Search of notes unavailable.',
|
||||
code: 'UNAVAILABLE',
|
||||
id: '0b44998d-77aa-4427-80d0-d2c9b8523011',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -59,8 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
if (!policies.canSearchNotes) {
|
||||
throw new ApiError(meta.errors.unavailable);
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
|
||||
|
||||
if (ps.userId) {
|
||||
@@ -72,16 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
query
|
||||
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
|
@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.noteThreadMutingsRepository.count({
|
||||
where: {
|
||||
userId: me.id,
|
||||
threadId: note.threadId || note.id,
|
||||
threadId: note.threadId ?? note.id,
|
||||
},
|
||||
take: 1,
|
||||
}),
|
||||
|
@@ -6,7 +6,7 @@ import { QueryService } from '@/core/QueryService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getTimeId } from '@/misc/id/aid.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -57,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const followees = await this.followingsRepository.createQueryBuilder('following')
|
||||
@@ -68,18 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.id > :minId', { minId })
|
||||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
@@ -95,6 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
@@ -84,16 +84,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notifications'],
|
||||
@@ -27,10 +27,10 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, user, token) => {
|
||||
this.createNotificationService.createNotification(user.id, 'app', {
|
||||
this.notificationService.createNotification(user.id, 'app', {
|
||||
appAccessTokenId: token ? token.id : null,
|
||||
customBody: ps.body,
|
||||
customHeader: ps.header,
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotificationsRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notifications', 'account'],
|
||||
@@ -23,24 +21,10 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.notificationsRepository)
|
||||
private notificationsRepository: NotificationsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Update documents
|
||||
await this.notificationsRepository.update({
|
||||
notifieeId: me.id,
|
||||
isRead: false,
|
||||
}, {
|
||||
isRead: true,
|
||||
});
|
||||
|
||||
// 全ての通知を読みましたよというイベントを発行
|
||||
this.globalEventService.publishMainStream(me.id, 'readAllNotifications');
|
||||
this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined);
|
||||
this.notificationService.readAllNotification(me.id, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,57 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notifications', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:notifications',
|
||||
|
||||
description: 'Mark a notification as read.',
|
||||
|
||||
errors: {
|
||||
noSuchNotification: {
|
||||
message: 'No such notification.',
|
||||
code: 'NO_SUCH_NOTIFICATION',
|
||||
id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
notificationId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['notificationId'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
notificationIds: {
|
||||
type: 'array',
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
maxItems: 100,
|
||||
},
|
||||
},
|
||||
required: ['notificationIds'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if ('notificationId' in ps) return this.notificationService.readNotification(me.id, [ps.notificationId]);
|
||||
return this.notificationService.readNotification(me.id, ps.notificationIds);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,97 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/index.js';
|
||||
import type { RenoteMuting } from '@/models/entities/RenoteMuting.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:mutes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 20,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '5e0a5dff-1e94-4202-87ae-4d9c89eb2271',
|
||||
},
|
||||
|
||||
muteeIsYourself: {
|
||||
message: 'Mutee is yourself.',
|
||||
code: 'MUTEE_IS_YOURSELF',
|
||||
id: '37285718-52f7-4aef-b7de-c38b8e8a8420',
|
||||
},
|
||||
|
||||
alreadyMuting: {
|
||||
message: 'You are already muting that user.',
|
||||
code: 'ALREADY_MUTING',
|
||||
id: 'ccfecbe4-1f1c-4fc2-8a3d-c3ffee61cb7b',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private getterService: GetterService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
||||
// 自分自身
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.muteeIsYourself);
|
||||
}
|
||||
|
||||
// Get mutee
|
||||
const mutee = await getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await this.renoteMutingsRepository.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await this.renoteMutingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as RenoteMuting);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:mutes',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '9b6728cf-638c-4aa1-bedb-e07d8101474d',
|
||||
},
|
||||
|
||||
muteeIsYourself: {
|
||||
message: 'Mutee is yourself.',
|
||||
code: 'MUTEE_IS_YOURSELF',
|
||||
id: '619b1314-0850-4597-a242-e245f3da42af',
|
||||
},
|
||||
|
||||
notMuting: {
|
||||
message: 'You are not muting that user.',
|
||||
code: 'NOT_MUTING',
|
||||
id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
||||
// Check if the mutee is yourself
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.muteeIsYourself);
|
||||
}
|
||||
|
||||
// Get mutee
|
||||
const mutee = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Check not muting
|
||||
const exist = await this.renoteMutingsRepository.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notMuting);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await this.renoteMutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RenoteMutingsRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:mutes',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'RenoteMuting',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
private renoteMutingEntityService: RenoteMutingEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
|
||||
.andWhere('muting.muterId = :meId', { meId: me.id });
|
||||
|
||||
const mutings = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.renoteMutingEntityService.packMany(mutings, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.clipEntityService.packMany(clips);
|
||||
return await this.clipEntityService.packMany(clips, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -74,16 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) {
|
||||
|
@@ -50,6 +50,10 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isRenoteMuted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -91,6 +95,10 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isRenoteMuted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -48,6 +48,7 @@ export const meta = {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '4362f8dc-731f-4ad8-a694-be5a88922a24',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number };
|
||||
type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: number };
|
||||
|
||||
export class ApiError extends Error {
|
||||
public message: string;
|
||||
|
@@ -0,0 +1,31 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { genOpenapiSpec } from './gen-spec.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
|
||||
|
||||
@Injectable()
|
||||
export class OpenApiServerService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/api-doc', async (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
return await reply.sendFile('/redoc.html', staticAssets);
|
||||
});
|
||||
fastify.get('/api.json', (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
reply.send(genOpenapiSpec(this.config));
|
||||
});
|
||||
done();
|
||||
}
|
||||
}
|
193
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
193
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import type { Config } from '@/config.js';
|
||||
import endpoints from '../endpoints.js';
|
||||
import { errors as basicErrors } from './errors.js';
|
||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||
|
||||
export function genOpenapiSpec(config: Config) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
|
||||
info: {
|
||||
version: config.version,
|
||||
title: 'Misskey API',
|
||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
||||
},
|
||||
|
||||
externalDocs: {
|
||||
description: 'Repository',
|
||||
url: 'https://github.com/misskey-dev/misskey',
|
||||
},
|
||||
|
||||
servers: [{
|
||||
url: config.apiUrl,
|
||||
}],
|
||||
|
||||
paths: {} as any,
|
||||
|
||||
components: {
|
||||
schemas: schemas,
|
||||
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'body',
|
||||
name: 'i',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
||||
const errors = {} as any;
|
||||
|
||||
if (endpoint.meta.errors) {
|
||||
for (const e of Object.values(endpoint.meta.errors)) {
|
||||
errors[e.code] = {
|
||||
value: {
|
||||
error: e,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
|
||||
|
||||
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
|
||||
desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
|
||||
if (endpoint.meta.kind) {
|
||||
const kind = endpoint.meta.kind;
|
||||
desc += ` / **Permission**: *${kind}*`;
|
||||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
const schema = { ...endpoint.params };
|
||||
|
||||
if (endpoint.meta.requireFile) {
|
||||
schema.properties = {
|
||||
...schema.properties,
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'The file contents.',
|
||||
},
|
||||
};
|
||||
schema.required = [...schema.required ?? [], 'file'];
|
||||
}
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
summary: endpoint.name,
|
||||
description: desc,
|
||||
externalDocs: {
|
||||
description: 'Source code',
|
||||
url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
|
||||
},
|
||||
...(endpoint.meta.tags ? {
|
||||
tags: [endpoint.meta.tags[0]],
|
||||
} : {}),
|
||||
...(endpoint.meta.requireCredential ? {
|
||||
security: [{
|
||||
ApiKeyAuth: [],
|
||||
}],
|
||||
} : {}),
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
[requestType]: {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...(endpoint.meta.res ? {
|
||||
'200': {
|
||||
description: 'OK (with results)',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: resSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {
|
||||
'204': {
|
||||
description: 'OK (without any results)',
|
||||
},
|
||||
}),
|
||||
'400': {
|
||||
description: 'Client error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: { ...errors, ...basicErrors['400'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': {
|
||||
description: 'Authentication error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['401'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'403': {
|
||||
description: 'Forbidden error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['403'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'418': {
|
||||
description: 'I\'m Ai',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['418'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...(endpoint.meta.limit ? {
|
||||
'429': {
|
||||
description: 'To many requests',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['429'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {}),
|
||||
'500': {
|
||||
description: 'Internal server error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['500'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
post: info,
|
||||
};
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import type { Schema } from '@/misc/schema.js';
|
||||
import { refs } from '@/misc/schema.js';
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { refs } from '@/misc/json-schema.js';
|
||||
|
||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||
const res: any = schema;
|
||||
|
@@ -23,12 +23,16 @@ export default abstract class Channel {
|
||||
return this.connection.following;
|
||||
}
|
||||
|
||||
protected get muting() {
|
||||
return this.connection.muting;
|
||||
protected get userIdsWhoMeMuting() {
|
||||
return this.connection.userIdsWhoMeMuting;
|
||||
}
|
||||
|
||||
protected get blocking() {
|
||||
return this.connection.blocking;
|
||||
protected get userIdsWhoMeMutingRenotes() {
|
||||
return this.connection.userIdsWhoMeMutingRenotes;
|
||||
}
|
||||
|
||||
protected get userIdsWhoBlockingMe() {
|
||||
return this.connection.userIdsWhoBlockingMe;
|
||||
}
|
||||
|
||||
protected get followingChannels() {
|
||||
|
@@ -35,9 +35,11 @@ class AntennaChannel extends Channel {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Channel from '../channel.js';
|
||||
@@ -47,9 +47,11 @@ class ChannelChannel extends Channel {
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -64,9 +64,11 @@ class GlobalTimelineChannel extends Channel {
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||
|
||||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||
|
||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user