> :
+ any
+
+type ObjectSchemaType = NullOrUndefined
>;
export type SchemaTypeDef
=
p['type'] extends 'null' ? null :
@@ -149,13 +165,7 @@ export type SchemaTypeDef
=
string
) :
p['type'] extends 'boolean' ? boolean :
- p['type'] extends 'object' ? (
- p['ref'] extends keyof typeof refs ? Packed
:
- p['properties'] extends NonNullable ? ObjType[number]> :
- p['anyOf'] extends ReadonlyArray ? UnionSchemaType & Partial>> :
- p['allOf'] extends ReadonlyArray ? UnionToIntersection> :
- any
- ) :
+ p['type'] extends 'object' ? ObjectSchemaTypeDef :
p['type'] extends 'array' ? (
p['items'] extends OfSchema ? (
p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] :
@@ -166,6 +176,7 @@ export type SchemaTypeDef =
p['items'] extends NonNullable ? SchemaTypeDef[] :
any[]
) :
+ p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> :
p['oneOf'] extends ReadonlyArray ? UnionSchemaType :
any;
diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts
index d9a6ac987..07039d4fa 100644
--- a/packages/backend/src/models/entities/Flash.ts
+++ b/packages/backend/src/models/entities/Flash.ts
@@ -44,7 +44,7 @@ export class Flash {
public user: User | null;
@Column('varchar', {
- length: 16384,
+ length: 32768,
})
public script: string;
diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts
index 6679cdb80..66f131d1c 100644
--- a/packages/backend/src/models/entities/Notification.ts
+++ b/packages/backend/src/models/entities/Notification.ts
@@ -64,6 +64,7 @@ export class Notification {
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* groupInvited - グループに招待された
+ * achievementEarned - 実績を獲得
* app - アプリ通知
*/
@Index()
@@ -129,6 +130,11 @@ export class Notification {
})
public choice: number | null;
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public achievement: string | null;
+
/**
* アプリ通知のbody
*/
diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts
index c561da87c..86df8d5d9 100644
--- a/packages/backend/src/models/entities/UserProfile.ts
+++ b/packages/backend/src/models/entities/UserProfile.ts
@@ -213,6 +213,19 @@ export class UserProfile {
})
public mutingNotificationTypes: typeof notificationTypes[number][];
+ @Column('varchar', {
+ length: 32, array: true, default: '{}',
+ })
+ public loggedInDates: string[];
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public achievements: {
+ name: string;
+ unlockedAt: number;
+ }[];
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts
index d897a0fc0..143f25373 100644
--- a/packages/backend/src/models/schema/emoji.ts
+++ b/packages/backend/src/models/schema/emoji.ts
@@ -29,5 +29,9 @@ export const packedEmojiSchema = {
optional: true, nullable: true,
description: 'The local host is represented with `null`.',
},
+ url: {
+ type: 'string',
+ optional: true, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 034e9cc5a..6a8f35cdd 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
+import { GlobalModule } from '@/GlobalModule.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
@@ -34,6 +35,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
@Module({
imports: [
+ GlobalModule,
CoreModule,
],
providers: [
diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
index 4650da76b..da4ae8855 100644
--- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
+++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts
@@ -57,8 +57,15 @@ export class AggregateRetentionProcessorService {
usersCount: targetUserIds.length,
});
+ // 今日活動したユーザーを全て取得
+ const activeUsers = await this.usersRepository.findBy({
+ host: IsNull(),
+ lastActiveDate: MoreThan(new Date(Date.now() - (1000 * 60 * 60 * 24))),
+ });
+ const activeUsersIds = activeUsers.map(u => u.id);
+
for (const record of pastRecords) {
- const retention = record.userIds.filter(id => targetUserIds.includes(id)).length;
+ const retention = record.userIds.filter(id => activeUsersIds.includes(id)).length;
const data = deepClone(record.data);
data[dateKey] = retention;
diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
index f0543a5ed..57210b25d 100644
--- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts
@@ -6,10 +6,10 @@ import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { StatusError } from '@/misc/status-error.js';
+import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import type { WebhookDeliverJobData } from '../types.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class WebhookDeliverProcessorService {
@@ -33,26 +33,23 @@ export class WebhookDeliverProcessorService {
try {
this.logger.debug(`delivering ${job.data.webhookId}`);
- const res = await this.httpRequestService.fetch(
- job.data.to,
- {
- method: 'POST',
- headers: {
- 'User-Agent': 'Misskey-Hooks',
- 'X-Misskey-Host': this.config.host,
- 'X-Misskey-Hook-Id': job.data.webhookId,
- 'X-Misskey-Hook-Secret': job.data.secret,
- },
- body: JSON.stringify({
- hookId: job.data.webhookId,
- userId: job.data.userId,
- eventId: job.data.eventId,
- createdAt: job.data.createdAt,
- type: job.data.type,
- body: job.data.content,
- }),
- }
- );
+ const res = await this.httpRequestService.send(job.data.to, {
+ method: 'POST',
+ headers: {
+ 'User-Agent': 'Misskey-Hooks',
+ 'X-Misskey-Host': this.config.host,
+ 'X-Misskey-Hook-Id': job.data.webhookId,
+ 'X-Misskey-Hook-Secret': job.data.secret,
+ },
+ body: JSON.stringify({
+ hookId: job.data.webhookId,
+ userId: job.data.userId,
+ eventId: job.data.eventId,
+ createdAt: job.data.createdAt,
+ type: job.data.type,
+ body: job.data.content,
+ }),
+ });
this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(),
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 134b3df32..40024270a 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -5,14 +5,14 @@ import { Inject, Injectable } from '@nestjs/common';
import fastifyStatic from '@fastify/static';
import rename from 'rename';
import type { Config } from '@/config.js';
-import type { DriveFilesRepository } from '@/models/index.js';
+import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js';
-import { ImageProcessingService } from '@/core/ImageProcessingService.js';
+import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js';
import { contentDisposition } from '@/misc/content-disposition.js';
@@ -20,6 +20,8 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
+import { isMimeImage } from '@/misc/is-mime-image.js';
+import sharp from 'sharp';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -57,7 +59,7 @@ export class FileServerService {
reply.header('Cache-Control', 'max-age=300');
};
}
-
+
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
@@ -70,23 +72,309 @@ export class FileServerService {
serve: false,
});
- fastify.get('/app-default.jpg', (request, reply) => {
+ fastify.get('/files/app-default.jpg', (request, reply) => {
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file);
});
- fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply));
- fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(request, reply));
+ fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
+ return await this.sendDriveFile(request, reply)
+ .catch(err => this.errorHandler(request, reply, err));
+ });
+ fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
+ return await this.sendDriveFile(request, reply)
+ .catch(err => this.errorHandler(request, reply, err));
+ });
+
+ fastify.get<{
+ Params: { url: string; };
+ Querystring: { url?: string; };
+ }>('/proxy/:url*', async (request, reply) => {
+ return await this.proxyHandler(request, reply)
+ .catch(err => this.errorHandler(request, reply, err));
+ });
done();
}
+ @bindThis
+ private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) {
+ this.logger.error(`${err}`);
+
+ reply.header('Cache-Control', 'max-age=300');
+
+ if (request.query && 'fallback' in request.query) {
+ return reply.sendFile('/dummy.png', assets);
+ }
+
+ if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
+ reply.code(err.statusCode);
+ return;
+ }
+
+ reply.code(500);
+ return;
+ }
+
@bindThis
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
const key = request.params.key;
+ const file = await this.getFileFromKey(key).then();
+ if (file === '404') {
+ reply.code(404);
+ reply.header('Cache-Control', 'max-age=86400');
+ return reply.sendFile('/dummy.png', assets);
+ }
+
+ if (file === '204') {
+ reply.code(204);
+ reply.header('Cache-Control', 'max-age=86400');
+ return;
+ }
+
+ try {
+ if (file.state === 'remote') {
+ const convertFile = async () => {
+ if (file.fileRole === 'thumbnail') {
+ if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
+ return this.imageProcessingService.convertToWebpStream(
+ file.path,
+ 498,
+ 280
+ );
+ } else if (file.mime.startsWith('video/')) {
+ return await this.videoProcessingService.generateVideoThumbnail(file.path);
+ }
+ }
+
+ if (file.fileRole === 'webpublic') {
+ if (['image/svg+xml'].includes(file.mime)) {
+ return this.imageProcessingService.convertToWebpStream(
+ file.path,
+ 2048,
+ 2048,
+ { ...webpDefault, lossless: true }
+ )
+ }
+ }
+
+ return {
+ data: fs.createReadStream(file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+ };
+
+ const image = await convertFile();
+
+ if ('pipe' in image.data && typeof image.data.pipe === 'function') {
+ // image.dataがstreamなら、stream終了後にcleanup
+ image.data.on('end', file.cleanup);
+ image.data.on('close', file.cleanup);
+ } else {
+ // image.dataがstreamでないなら直ちにcleanup
+ file.cleanup();
+ }
+
+ reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+ return image.data;
+ }
+
+ if (file.fileRole !== 'original') {
+ const filename = rename(file.file.name, {
+ suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
+ extname: file.ext ? `.${file.ext}` : undefined,
+ }).toString();
+
+ reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+ 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;
+ }
+ } catch (e) {
+ if ('cleanup' in file) file.cleanup();
+ throw e;
+ }
+ }
+
+ @bindThis
+ private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
+ const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
+
+ if (typeof url !== 'string') {
+ reply.code(400);
+ return;
+ }
+
+ // Create temp file
+ const file = await this.getStreamAndTypeFromUrl(url);
+ if (file === '404') {
+ reply.code(404);
+ reply.header('Cache-Control', 'max-age=86400');
+ return reply.sendFile('/dummy.png', assets);
+ }
+
+ if (file === '204') {
+ reply.code(204);
+ reply.header('Cache-Control', 'max-age=86400');
+ return;
+ }
+
+ try {
+ const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
+ const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
+
+ let image: IImageStreamable | null = null;
+ if ('emoji' in request.query && isConvertibleImage) {
+ if (!isAnimationConvertibleImage && !('static' in request.query)) {
+ image = {
+ data: fs.createReadStream(file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+ } else {
+ const data = sharp(file.path, { animated: !('static' in request.query) })
+ .resize({
+ height: 128,
+ withoutEnlargement: true,
+ })
+ .webp(webpDefault);
+
+ image = {
+ data,
+ ext: 'webp',
+ type: 'image/webp',
+ };
+ }
+ } else if ('static' in request.query && isConvertibleImage) {
+ image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
+ } else if ('preview' in request.query && isConvertibleImage) {
+ image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
+ } else if ('badge' in request.query) {
+ if (!isConvertibleImage) {
+ // 画像でないなら404でお茶を濁す
+ throw new StatusError('Unexpected mime', 404);
+ }
+
+ const mask = sharp(file.path)
+ .resize(96, 96, {
+ fit: 'inside',
+ withoutEnlargement: false,
+ })
+ .greyscale()
+ .normalise()
+ .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',
+ type: 'image/png',
+ };
+ } else if (file.mime === 'image/svg+xml') {
+ image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
+ } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
+ throw new StatusError('Rejected type', 403, 'Rejected type');
+ }
+
+ if (!image) {
+ image = {
+ data: fs.createReadStream(file.path),
+ ext: file.ext,
+ type: file.mime,
+ };
+ }
+
+ if ('cleanup' in file) {
+ if ('pipe' in image.data && typeof image.data.pipe === 'function') {
+ // image.dataがstreamなら、stream終了後にcleanup
+ image.data.on('end', file.cleanup);
+ image.data.on('close', file.cleanup);
+ } else {
+ // image.dataがstreamでないなら直ちにcleanup
+ file.cleanup();
+ }
+ }
+
+ reply.header('Content-Type', image.type);
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
+ return image.data;
+ } catch (e) {
+ if ('cleanup' in file) file.cleanup();
+ throw e;
+ }
+ }
+
+ @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; }
+ | '404'
+ | '204'
+ > {
+ if (url.startsWith(`${this.config.url}/files/`)) {
+ const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
+ if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
+
+ return await this.getFileFromKey(key);
+ }
+
+ return await this.downloadAndDetectTypeFromUrl(url);
+ }
+
+ @bindThis
+ private async downloadAndDetectTypeFromUrl(url: string): Promise<
+ { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
+ > {
+ const [path, cleanup] = await createTemp();
+ try {
+ await this.downloadService.downloadUrl(url, path);
+
+ const { mime, ext } = await this.fileInfoService.detectType(path);
+
+ return {
+ state: 'remote',
+ mime, ext,
+ path, cleanup,
+ }
+ } catch (e) {
+ cleanup();
+ throw e;
+ }
+ }
+
+ @bindThis
+ private async getFileFromKey(key: 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; }
+ | '404'
+ | '204'
+ > {
// Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
@@ -94,89 +382,41 @@ export class FileServerService {
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
- if (file == null) {
- reply.code(404);
- reply.header('Cache-Control', 'max-age=86400');
- return reply.sendFile('/dummy.png', assets);
- }
+ if (file == null) return '404';
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) {
- if (file.isLink && file.uri) { // 期限切れリモートファイル
- const [path, cleanup] = await createTemp();
-
- try {
- await this.downloadService.downloadUrl(file.uri, path);
-
- const { mime, ext } = await this.fileInfoService.detectType(path);
-
- const convertFile = async () => {
- if (isThumbnail) {
- if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(mime)) {
- return await this.imageProcessingService.convertToWebp(path, 498, 280);
- } else if (mime.startsWith('video/')) {
- return await this.videoProcessingService.generateVideoThumbnail(path);
- }
- }
-
- if (isWebpublic) {
- if (['image/svg+xml'].includes(mime)) {
- return await this.imageProcessingService.convertToPng(path, 2048, 2048);
- }
- }
-
- return {
- data: fs.readFileSync(path),
- ext,
- type: mime,
- };
- };
-
- const image = await convertFile();
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- return image.data;
- } catch (err) {
- this.logger.error(`${err}`);
-
- if (err instanceof StatusError && err.isClientError) {
- reply.code(err.statusCode);
- reply.header('Cache-Control', 'max-age=86400');
- } else {
- reply.code(500);
- reply.header('Cache-Control', 'max-age=300');
- }
- } finally {
- cleanup();
- }
- return;
+ if (!(file.isLink && file.uri)) return '204';
+ const result = await this.downloadAndDetectTypeFromUrl(file.uri);
+ return {
+ ...result,
+ fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
+ file,
}
-
- reply.code(204);
- reply.header('Cache-Control', 'max-age=86400');
- return;
}
- if (isThumbnail || isWebpublic) {
- const { mime, ext } = await this.fileInfoService.detectType(this.internalStorageService.resolvePath(key));
- const filename = rename(file.name, {
- suffix: isThumbnail ? '-thumb' : '-web',
- extname: ext ? `.${ext}` : undefined,
- }).toString();
+ const path = this.internalStorageService.resolvePath(key);
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', filename));
- return this.internalStorageService.read(key);
- } else {
- const readable = this.internalStorageService.read(file.accessKey!);
- readable.on('error', this.commonReadableHandlerGenerator(reply));
- reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- reply.header('Content-Disposition', contentDisposition('inline', file.name));
- return readable;
+ if (isThumbnail || isWebpublic) {
+ const { mime, ext } = await this.fileInfoService.detectType(path);
+ return {
+ state: 'stored_internal',
+ fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
+ file,
+ mime, ext,
+ path,
+ };
+ }
+
+ return {
+ state: 'stored_internal',
+ fileRole: 'original',
+ file,
+ mime: file.type,
+ ext: null,
+ path,
}
}
}
diff --git a/packages/backend/src/server/MediaProxyServerService.ts b/packages/backend/src/server/MediaProxyServerService.ts
deleted file mode 100644
index 5b76f1502..000000000
--- a/packages/backend/src/server/MediaProxyServerService.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
-import { Inject, Injectable } from '@nestjs/common';
-import sharp from 'sharp';
-import fastifyStatic from '@fastify/static';
-import { DI } from '@/di-symbols.js';
-import type { Config } from '@/config.js';
-import { isMimeImage } from '@/misc/is-mime-image.js';
-import { createTemp } from '@/misc/create-temp.js';
-import { DownloadService } from '@/core/DownloadService.js';
-import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
-import type { IImage } from '@/core/ImageProcessingService.js';
-import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
-import { StatusError } from '@/misc/status-error.js';
-import type Logger from '@/logger.js';
-import { FileInfoService } from '@/core/FileInfoService.js';
-import { LoggerService } from '@/core/LoggerService.js';
-import { bindThis } from '@/decorators.js';
-import type { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-const assets = `${_dirname}/../../src/server/assets/`;
-
-@Injectable()
-export class MediaProxyServerService {
- private logger: Logger;
-
- constructor(
- @Inject(DI.config)
- private config: Config,
-
- private fileInfoService: FileInfoService,
- private downloadService: DownloadService,
- private imageProcessingService: ImageProcessingService,
- private loggerService: LoggerService,
- ) {
- this.logger = this.loggerService.getLogger('server', 'gray', false);
-
- //this.createServer = this.createServer.bind(this);
- }
-
- @bindThis
- public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
- fastify.addHook('onRequest', (request, reply, done) => {
- reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
- done();
- });
-
- fastify.register(fastifyStatic, {
- root: _dirname,
- serve: false,
- });
-
- fastify.get<{
- Params: { url: string; };
- Querystring: { url?: string; };
- }>('/:url*', async (request, reply) => await this.handler(request, reply));
-
- done();
- }
-
- @bindThis
- private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
- const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
-
- if (typeof url !== 'string') {
- reply.code(400);
- return;
- }
-
- // Create temp file
- const [path, cleanup] = await createTemp();
-
- try {
- await this.downloadService.downloadUrl(url, path);
-
- const { mime, ext } = await this.fileInfoService.detectType(path);
- const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
- const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
-
- let image: IImage;
- if ('emoji' in request.query && isConvertibleImage) {
- if (!isAnimationConvertibleImage && !('static' in request.query)) {
- image = {
- data: fs.readFileSync(path),
- ext,
- type: mime,
- };
- } else {
- const data = await sharp(path, { animated: !('static' in request.query) })
- .resize({
- height: 128,
- withoutEnlargement: true,
- })
- .webp(webpDefault)
- .toBuffer();
-
- image = {
- data,
- ext: 'webp',
- type: 'image/webp',
- };
- }
- } else if ('static' in request.query && isConvertibleImage) {
- image = await this.imageProcessingService.convertToWebp(path, 498, 280);
- } else if ('preview' in request.query && isConvertibleImage) {
- image = await this.imageProcessingService.convertToWebp(path, 200, 200);
- } else if ('badge' in request.query) {
- if (!isConvertibleImage) {
- // 画像でないなら404でお茶を濁す
- throw new StatusError('Unexpected mime', 404);
- }
-
- const mask = sharp(path)
- .resize(96, 96, {
- fit: 'inside',
- withoutEnlargement: false,
- })
- .greyscale()
- .normalise()
- .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',
- type: 'image/png',
- };
- } else if (mime === 'image/svg+xml') {
- image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault);
- } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
- throw new StatusError('Rejected type', 403, 'Rejected type');
- } else {
- image = {
- data: fs.readFileSync(path),
- ext,
- type: mime,
- };
- }
-
- reply.header('Content-Type', image.type);
- reply.header('Cache-Control', 'max-age=31536000, immutable');
- return image.data;
- } catch (err) {
- this.logger.error(`${err}`);
-
- if ('fallback' in request.query) {
- return reply.sendFile('/dummy.png', assets);
- }
-
- if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
- reply.code(err.statusCode);
- } else {
- reply.code(500);
- }
- } finally {
- cleanup();
- }
- }
-}
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 474edafe4..9dc152769 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -3,7 +3,6 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
-import { MediaProxyServerService } from './MediaProxyServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
@@ -51,7 +50,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
UrlPreviewService,
ActivityPubServerService,
FileServerService,
- MediaProxyServerService,
NodeinfoServerService,
ServerService,
WellKnownServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index fac8497b5..beb3a34ec 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -14,15 +14,14 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { createTemp } from '@/misc/create-temp.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
+import { bindThis } from '@/decorators.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
-import { MediaProxyServerService } from './MediaProxyServerService.js';
import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class ServerService {
@@ -48,7 +47,6 @@ export class ServerService {
private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService,
- private mediaProxyServerService: MediaProxyServerService,
private clientServerService: ClientServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
@@ -73,8 +71,7 @@ export class ServerService {
}
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
- fastify.register(this.fileServerService.createServer, { prefix: '/files' });
- fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
+ fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer);
@@ -82,13 +79,13 @@ export class ServerService {
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;
+ reply.header('Cache-Control', 'public, max-age=86400');
+
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
reply.code(404);
return;
}
- reply.header('Cache-Control', 'public, max-age=86400');
-
const name = path.split('@')[0].replace('.webp', '');
const host = path.split('@')[1]?.replace('.webp', '');
@@ -101,7 +98,12 @@ export class ServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
if (emoji == null) {
- return await reply.redirect('/static-assets/emoji-unknown.png');
+ if ('fallback' in request.query) {
+ return await reply.redirect('/static-assets/emoji-unknown.png');
+ } else {
+ reply.code(404);
+ return;
+ }
}
const url = new URL('/proxy/emoji.webp', this.config.url);
@@ -127,6 +129,8 @@ export class ServerService {
relations: ['avatar'],
});
+ reply.header('Cache-Control', 'public, max-age=86400');
+
if (user) {
reply.redirect(this.userEntityService.getAvatarUrlSync(user));
} else {
@@ -138,6 +142,7 @@ export class ServerService {
const [temp, cleanup] = await createTemp();
await genIdenticon(request.params.x, fs.createWriteStream(temp));
reply.header('Content-Type', 'image/png');
+ reply.header('Cache-Control', 'public, max-age=86400');
return fs.createReadStream(temp).on('close', () => cleanup());
});
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 14927da7d..466651f37 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
+import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
+import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
@@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
+const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
@@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
+const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
+ $i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
+ $users_achievements,
$fetchRss,
$retention,
],
@@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
+ $i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
+ $users_achievements,
$fetchRss,
$retention,
],
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 54c4206ea..3678fe14e 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
+import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
+import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
@@ -506,6 +508,7 @@ const eps = [
['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps],
['i/authorized-apps', ep___i_authorizedApps],
+ ['i/claim-achievement', ep___i_claimAchievement],
['i/change-password', ep___i_changePassword],
['i/delete-account', ep___i_deleteAccount],
['i/export-blocking', ep___i_exportBlocking],
@@ -660,6 +663,7 @@ const eps = [
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/stats', ep___users_stats],
+ ['users/achievements', ep___users_achievements],
['fetch-rss', ep___fetchRss],
['retention', ep___retention],
];
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
index 9b6c774f0..c683cd24c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -3,6 +3,8 @@ 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';
export const meta = {
tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
@@ -49,6 +54,10 @@ export default class extends Endpoint {
}
await this.db.queryResultCache!.remove(['meta_emojis']);
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packMany(ps.ids),
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index abca1d169..1bb05c15c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common';
import rndstr from 'rndstr';
import { DataSource } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js';
-import { IdService } from '@/core/IdService.js';
+import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -39,43 +37,26 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint {
constructor(
- @Inject(DI.db)
- private db: DataSource,
-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- @Inject(DI.emojisRepository)
- private emojisRepository: EmojisRepository,
+ private customEmojiService: CustomEmojiService,
- private emojiEntityService: EmojiEntityService,
- private idService: IdService,
- private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
+ const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
- if (file == null) throw new ApiError(meta.errors.noSuchFile);
+ if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
- const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
+ const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
- const emoji = await this.emojisRepository.insert({
- id: this.idService.genId(),
- updatedAt: new Date(),
- name: name,
+ const emoji = await this.customEmojiService.add({
+ driveFile,
+ name,
category: null,
- host: null,
aliases: [],
- originalUrl: file.url,
- publicUrl: file.webpublicUrl ?? file.url,
- type: file.webpublicType ?? file.type,
- }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
-
- await this.db.queryResultCache!.remove(['meta_emojis']);
-
- this.globalEventService.publishBroadcastStream('emojiAdded', {
- emoji: await this.emojiEntityService.pack(emoji.id),
+ host: null,
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
index ae45105b2..0c337237d 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
@@ -4,6 +4,8 @@ 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';
export const meta = {
tags: ['admin'],
@@ -35,6 +37,8 @@ export default class extends Endpoint {
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
@@ -43,13 +47,15 @@ export default class extends Endpoint {
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.packMany(emojis),
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
index e237d87d3..c1a60a277 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
@@ -5,6 +5,8 @@ import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -42,6 +44,8 @@ export default class extends Endpoint {
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
@@ -52,6 +56,10 @@ export default class extends Endpoint {
await this.db.queryResultCache!.remove(['meta_emojis']);
+ this.globalEventService.publishBroadcastStream('emojiDeleted', {
+ emojis: [ await this.emojiEntityService.pack(emoji) ],
+ });
+
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index d9ce97194..8e0ea2e11 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -101,7 +101,7 @@ export default class extends Endpoint {
.take(ps.limit)
.getMany();
- return this.emojiEntityService.packMany(emojis);
+ return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 1a6096f36..1b1931f8e 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -98,7 +98,7 @@ export default class extends Endpoint {
emojis = await q.take(ps.limit).getMany();
}
- return this.emojiEntityService.packMany(emojis);
+ return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
index 5fc9e024b..065965f64 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -3,6 +3,8 @@ 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';
export const meta = {
tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
@@ -49,6 +54,10 @@ export default class extends Endpoint {
}
await this.db.queryResultCache!.remove(['meta_emojis']);
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packMany(ps.ids),
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
index 8b5ba8fbf..51c0f329a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -3,6 +3,8 @@ 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';
export const meta = {
tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
@@ -45,6 +50,10 @@ export default class extends Endpoint {
});
await this.db.queryResultCache!.remove(['meta_emojis']);
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packMany(ps.ids),
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
index 827b5ace7..3329cab7b 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -3,6 +3,8 @@ 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';
export const meta = {
tags: ['admin'],
@@ -37,6 +39,9 @@ export default class extends Endpoint {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
@@ -47,6 +52,10 @@ export default class extends Endpoint {
});
await this.db.queryResultCache!.remove(['meta_emojis']);
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packMany(ps.ids),
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index fb0ef1287..22bedc710 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -48,6 +50,9 @@ export default class extends Endpoint {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
+
+ private emojiEntityService: EmojiEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
@@ -62,6 +67,22 @@ export default class extends Endpoint {
});
await this.db.queryResultCache!.remove(['meta_emojis']);
+
+ const updated = await this.emojiEntityService.pack(emoji.id);
+
+ if (emoji.name === ps.name) {
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: [ updated ],
+ });
+ } else {
+ this.globalEventService.publishBroadcastStream('emojiDeleted', {
+ emojis: [ await this.emojiEntityService.pack(emoji) ],
+ });
+
+ this.globalEventService.publishBroadcastStream('emojiAdded', {
+ emoji: updated,
+ });
+ }
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index ee63d291b..ff0a78b92 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -28,8 +28,8 @@ export const meta = {
recursiveNesting: {
message: 'It can not be structured like nesting folders recursively.',
- code: 'NO_SUCH_PARENT_FOLDER',
- id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1',
+ code: 'RECURSIVE_NESTING',
+ id: 'dbeb024837894013aed44279f9199740',
},
},
diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 97dcfde59..77854afb3 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -10,6 +10,8 @@ export const meta = {
tags: ['meta'],
requireCredential: false,
+ allowGet: true,
+ cacheSec: 3600,
res: {
type: 'object',
@@ -83,6 +85,7 @@ export default class extends Endpoint {
emojis: await this.emojiEntityService.packMany(emojis, {
omitId: true,
omitHost: true,
+ withUrl: true,
}),
};
});
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index ae6a87513..5849d3111 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -33,16 +33,13 @@ export default class extends Endpoint {
private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps, me) => {
- const res = await this.httpRequestService.fetch(
- ps.url,
- {
- method: 'GET',
- headers: {
- Accept: 'application/rss+xml, */*',
- },
- // timeout: 5000,
- }
- );
+ const res = await this.httpRequestService.send(ps.url, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/rss+xml, */*',
+ },
+ timeout: 5000,
+ });
const text = await res.text();
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index 3bcd6ff8f..6beef5ab8 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/index.js';
+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';
@@ -29,15 +29,36 @@ export default class extends Endpoint {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null;
- // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
- return await this.userEntityService.pack(user.id, user, {
+ const now = new Date();
+ const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
+
+ // 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
+ const userProfile = await this.userProfilesRepository.findOneOrFail({
+ where: {
+ userId: user.id,
+ },
+ relations: ['user'],
+ });
+
+ if (!userProfile.loggedInDates.includes(today)) {
+ this.userProfilesRepository.update({ userId: user.id }, {
+ loggedInDates: [...userProfile.loggedInDates, today],
+ });
+ userProfile.loggedInDates = [...userProfile.loggedInDates, today];
+ }
+
+ return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
detail: true,
includeSecrets: isSecure,
+ userProfile,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
new file mode 100644
index 000000000..52ae5475b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -0,0 +1,28 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { AchievementService } from '@/core/AchievementService.js';
+
+export const meta = {
+ requireCredential: true,
+} 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 {
+ constructor(
+ private achievementService: AchievementService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.achievementService.create(me.id, ps.name);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 92bc8a759..a709ab2f7 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -90,48 +90,13 @@ export const paramDef = {
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
- text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
- fileIds: {
- type: 'array',
- uniqueItems: true,
- minItems: 1,
- maxItems: 16,
- items: { type: 'string', format: 'misskey:id' },
- },
- mediaIds: {
- deprecated: true,
- description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
- type: 'array',
- uniqueItems: true,
- minItems: 1,
- maxItems: 16,
- items: { type: 'string', format: 'misskey:id' },
- },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
- renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
- poll: {
- type: 'object',
- nullable: true,
- properties: {
- choices: {
- type: 'array',
- uniqueItems: true,
- minItems: 2,
- maxItems: 10,
- items: { type: 'string', minLength: 1, maxLength: 50 },
- },
- multiple: { type: 'boolean', default: false },
- expiresAt: { type: 'integer', nullable: true },
- expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
- },
- required: ['choices'],
- },
},
anyOf: [
{
@@ -143,21 +108,60 @@ export const paramDef = {
},
{
// (re)note with files, text and poll are optional
+ properties: {
+ fileIds: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ },
required: ['fileIds'],
},
{
// (re)note with files, text and poll are optional
+ properties: {
+ mediaIds: {
+ deprecated: true,
+ description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ },
required: ['mediaIds'],
},
{
// (re)note with poll, text and files are optional
properties: {
- poll: { type: 'object', nullable: false },
+ poll: {
+ type: 'object',
+ nullable: true,
+ properties: {
+ choices: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 2,
+ maxItems: 10,
+ items: { type: 'string', minLength: 1, maxLength: 50 },
+ },
+ multiple: { type: 'boolean' },
+ expiresAt: { type: 'integer', nullable: true },
+ expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
+ },
+ required: ['choices'],
+ },
},
required: ['poll'],
},
{
// pure renote
+ properties: {
+ renoteId: { type: 'string', format: 'misskey:id', nullable: true },
+ },
required: ['renoteId'],
},
],
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
index acf22a5ad..e423f0f10 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -6,6 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+import { AchievementService } from '@/core/AchievementService.js';
export const meta = {
tags: ['notes', 'favorites'],
@@ -51,6 +52,7 @@ export default class extends Endpoint {
private idService: IdService,
private getterService: GetterService,
+ private achievementService: AchievementService,
) {
super(meta, paramDef, async (ps, me) => {
// Get favoritee
@@ -76,6 +78,10 @@ export default class extends Endpoint {
noteId: note.id,
userId: me.id,
});
+
+ if (note.userHost == null) {
+ this.achievementService.create(note.userId, 'myNoteFavorited1');
+ }
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index ab1977167..66655234a 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -7,8 +7,8 @@ import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
-import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
@@ -83,20 +83,14 @@ export default class extends Endpoint {
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
- const res = await this.httpRequestService.fetch(
- endpoint,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- Accept: 'application/json, */*',
- },
- body: params.toString(),
+ const res = await this.httpRequestService.send(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Accept: 'application/json, */*',
},
- {
- noOkError: false,
- }
- );
+ body: params.toString(),
+ });
const json = (await res.json()) as {
translations: {
diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts
new file mode 100644
index 000000000..2a095d83e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/achievements.ts
@@ -0,0 +1,31 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserProfilesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+} 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 {
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
+
+ return profile.achievements;
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index 9ec911f32..ac401a60e 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
- if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
+ if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic);
}
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 95491211b..b176e6c65 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -29,14 +29,22 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- username: { type: 'string', nullable: true },
- host: { type: 'string', nullable: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true },
},
anyOf: [
- { required: ['username'] },
- { required: ['host'] },
+ {
+ properties: {
+ username: { type: 'string', nullable: true },
+ },
+ required: ['username']
+ },
+ {
+ properties: {
+ host: { type: 'string', nullable: true },
+ },
+ required: ['host']
+ },
],
} as const;
diff --git a/packages/backend/src/server/api/integration/DiscordServerService.ts b/packages/backend/src/server/api/integration/DiscordServerService.ts
index 0ac273381..cbced901e 100644
--- a/packages/backend/src/server/api/integration/DiscordServerService.ts
+++ b/packages/backend/src/server/api/integration/DiscordServerService.ts
@@ -134,7 +134,7 @@ export class DiscordServerService {
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
- fastify.get('/dc/cb', async (request, reply) => {
+ fastify.get<{ Querystring: { code: string; state: string; } }>('/dc/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOAuth2();
diff --git a/packages/backend/src/server/api/integration/GithubServerService.ts b/packages/backend/src/server/api/integration/GithubServerService.ts
index a8c745d2d..76089c935 100644
--- a/packages/backend/src/server/api/integration/GithubServerService.ts
+++ b/packages/backend/src/server/api/integration/GithubServerService.ts
@@ -132,7 +132,7 @@ export class GithubServerService {
reply.redirect(oauth2!.getAuthorizeUrl(params));
});
- fastify.get('/gh/cb', async (request, reply) => {
+ fastify.get<{ Querystring: { code: string; state: string; } }>('/gh/cb', async (request, reply) => {
const userToken = this.getUserToken(request);
const oauth2 = await getOath2();
diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts
index 9cfadbfa1..f31a788d3 100644
--- a/packages/backend/src/server/api/integration/TwitterServerService.ts
+++ b/packages/backend/src/server/api/integration/TwitterServerService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
-import autwh from 'autwh';
+import * as autwh from 'autwh';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index a442529bb..36bfa7836 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -18,37 +18,42 @@ import { Following, Role, RoleAssignment } from '@/models';
import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events';
-// redis通すとDateのインスタンスはstringに変換されるので
-type Serialized = {
- [K in keyof T]: T[K] extends Date ? string : T[K];
-};
-
//#region Stream type-body definitions
export interface InternalStreamTypes {
- userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
- userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
- remoteUserUpdated: Serialized<{ id: User['id']; }>;
- follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
- unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
- policiesUpdated: Serialized;
- roleCreated: Serialized;
- roleDeleted: Serialized;
- roleUpdated: Serialized;
- userRoleAssigned: Serialized;
- userRoleUnassigned: Serialized;
- webhookCreated: Serialized;
- webhookDeleted: Serialized;
- webhookUpdated: Serialized;
- antennaCreated: Serialized;
- antennaDeleted: Serialized;
- antennaUpdated: Serialized;
- metaUpdated: Serialized;
+ userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; };
+ userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
+ remoteUserUpdated: { id: User['id']; };
+ follow: { followerId: User['id']; followeeId: User['id']; };
+ unfollow: { followerId: User['id']; followeeId: User['id']; };
+ policiesUpdated: Role['policies'];
+ roleCreated: Role;
+ roleDeleted: Role;
+ roleUpdated: Role;
+ userRoleAssigned: RoleAssignment;
+ userRoleUnassigned: RoleAssignment;
+ webhookCreated: Webhook;
+ webhookDeleted: Webhook;
+ webhookUpdated: Webhook;
+ antennaCreated: Antenna;
+ antennaDeleted: Antenna;
+ antennaUpdated: Antenna;
+ metaUpdated: Meta;
}
export interface BroadcastTypes {
emojiAdded: {
emoji: Packed<'Emoji'>;
};
+ emojiUpdated: {
+ emojis: Packed<'Emoji'>[];
+ };
+ emojiDeleted: {
+ emojis: {
+ id?: string;
+ name: string;
+ [other: string]: any;
+ }[];
+ };
}
export interface UserStreamTypes {
@@ -200,63 +205,72 @@ type EventUnionFromDictionary<
U = Events
> = U[keyof U];
+// redis通すとDateのインスタンスはstringに変換されるので
+type Serialized = {
+ [K in keyof T]: T[K] extends Date ? string : T[K] extends Record ? Serialized : T[K];
+};
+
+type SerializedAll = {
+ [K in keyof T]: Serialized;
+};
+
// name/messages(spec) pairs dictionary
export type StreamMessages = {
internal: {
name: 'internal';
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
broadcast: {
name: 'broadcast';
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
user: {
name: `user:${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
main: {
name: `mainStream:${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
drive: {
name: `driveStream:${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
note: {
name: `noteStream:${Note['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
channel: {
name: `channelStream:${Channel['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
userList: {
name: `userListStream:${UserList['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
antenna: {
name: `antennaStream:${Antenna['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
messaging: {
name: `messagingStream:${User['id']}-${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
groupMessaging: {
name: `messagingStream:${UserGroup['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
messagingIndex: {
name: `messagingIndexStream:${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
admin: {
name: `adminStream:${User['id']}`;
- payload: EventUnionFromDictionary;
+ payload: EventUnionFromDictionary>;
};
notes: {
name: 'notesStream';
- payload: Packed<'Note'>;
+ payload: Serialized>;
};
};
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 03547b550..2b03897d8 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -22,13 +22,13 @@
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
- const v = localStorage.getItem('v') || VERSION;
+ let forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
+ }
//#region Detect language & fetch translations
- const localeVersion = localStorage.getItem('localeVersion');
- const localeOutdated = (localeVersion == null || localeVersion !== v);
-
- if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
+ if (!localStorage.hasOwnProperty('locale')) {
const supportedLangs = LANGS;
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
@@ -45,13 +45,31 @@
}
}
- const res = await window.fetch(`/assets/locales/${lang}.${v}.json`);
- if (res.status === 200) {
+ const metaRes = await window.fetch('/api/meta', {
+ method: 'POST',
+ body: JSON.stringify({}),
+ credentials: 'omit',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (metaRes.status !== 200) {
+ renderError('META_FETCH');
+ return;
+ }
+ const meta = await metaRes.json();
+ const v = meta.version;
+ if (v == null) {
+ renderError('META_FETCH_V');
+ return;
+ }
+ const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+ if (localRes.status === 200) {
localStorage.setItem('lang', lang);
- localStorage.setItem('locale', await res.text());
+ localStorage.setItem('locale', await localRes.text());
localStorage.setItem('localeVersion', v);
} else {
- await checkUpdate();
renderError('LOCALE_FETCH');
return;
}
@@ -62,7 +80,6 @@
function importAppScript() {
import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => {
- await checkUpdate();
console.error(e);
renderError('APP_IMPORT', e);
});
@@ -140,7 +157,7 @@
An error has occurred!
-