Merge branch 'develop' into stories/components-ab
This commit is contained in:
commit
20789bf10c
109 changed files with 2122 additions and 1816 deletions
|
|
@ -1,8 +1,15 @@
|
|||
import Redis from 'ioredis';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { createRedisConnection } from './built/redis.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const redis = createRedisConnection(config);
|
||||
const redis = new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db ?? 0,
|
||||
});
|
||||
|
||||
redis.on('connect', () => redis.disconnect());
|
||||
redis.on('error', (e) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
export class AvatarUrlAndBannerUrl1680775031481 {
|
||||
name = 'AvatarUrlAndBannerUrl1680775031481'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,24 +22,24 @@
|
|||
"test-and-coverage": "pnpm jest-and-coverage"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "^1.3.11",
|
||||
"@swc/core-darwin-arm64": "^1.3.42",
|
||||
"@swc/core-darwin-x64": "^1.3.42",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.3.42",
|
||||
"@swc/core-linux-arm64-gnu": "^1.3.42",
|
||||
"@swc/core-linux-arm64-musl": "^1.3.42",
|
||||
"@swc/core-linux-x64-gnu": "^1.3.42",
|
||||
"@swc/core-linux-x64-musl": "^1.3.42",
|
||||
"@swc/core-win32-arm64-msvc": "^1.3.42",
|
||||
"@swc/core-win32-ia32-msvc": "^1.3.42",
|
||||
"@swc/core-win32-x64-msvc": "^1.3.42",
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.3.46",
|
||||
"@swc/core-darwin-x64": "1.3.46",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.46",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.46",
|
||||
"@swc/core-linux-arm64-musl": "1.3.46",
|
||||
"@swc/core-linux-x64-gnu": "1.3.46",
|
||||
"@swc/core-linux-x64-musl": "1.3.46",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.46",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.46",
|
||||
"@swc/core-win32-x64-msvc": "1.3.46",
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.301.0",
|
||||
"@aws-sdk/lib-storage": "3.301.0",
|
||||
"@aws-sdk/node-http-handler": "3.296.0",
|
||||
"@aws-sdk/client-s3": "3.306.0",
|
||||
"@aws-sdk/lib-storage": "3.306.0",
|
||||
"@aws-sdk/node-http-handler": "3.306.0",
|
||||
"@bull-board/api": "5.0.0",
|
||||
"@bull-board/fastify": "5.0.0",
|
||||
"@bull-board/ui": "5.0.0",
|
||||
|
|
@ -49,15 +49,15 @@
|
|||
"@fastify/cors": "8.2.1",
|
||||
"@fastify/http-proxy": "9.0.0",
|
||||
"@fastify/multipart": "7.5.0",
|
||||
"@fastify/static": "6.9.0",
|
||||
"@fastify/static": "6.10.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.12",
|
||||
"@nestjs/core": "9.3.12",
|
||||
"@nestjs/testing": "9.3.12",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/core": "9.4.0",
|
||||
"@nestjs/testing": "9.4.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.42",
|
||||
"@swc/core": "1.3.46",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
|
|
@ -136,8 +136,8 @@
|
|||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "5.0.2",
|
||||
"typeorm": "0.3.13",
|
||||
"typescript": "5.0.3",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
|
|
@ -190,8 +190,8 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"aws-sdk-client-mock": "^2.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.37.0",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,15 @@ import { setTimeout } from 'node:timers/promises';
|
|||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { createRedisConnection } from '@/redis.js';
|
||||
import { DI } from './di-symbols.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const $config: Provider = {
|
||||
provide: DI.config,
|
||||
useValue: config,
|
||||
useValue: loadConfig(),
|
||||
};
|
||||
|
||||
const $db: Provider = {
|
||||
|
|
@ -28,18 +25,31 @@ const $db: Provider = {
|
|||
const $redis: Provider = {
|
||||
provide: DI.redis,
|
||||
useFactory: (config) => {
|
||||
const redisClient = createRedisConnection(config);
|
||||
return redisClient;
|
||||
return new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db ?? 0,
|
||||
});
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redisSubscriber: Provider = {
|
||||
provide: DI.redisSubscriber,
|
||||
const $redisForPubsub: Provider = {
|
||||
provide: DI.redisForPubsub,
|
||||
useFactory: (config) => {
|
||||
const redisSubscriber = createRedisConnection(config);
|
||||
redisSubscriber.subscribe(config.host);
|
||||
return redisSubscriber;
|
||||
const redis = new Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
password: config.redisForPubsub.pass,
|
||||
keyPrefix: `${config.redisForPubsub.prefix}:`,
|
||||
db: config.redisForPubsub.db ?? 0,
|
||||
});
|
||||
redis.subscribe(config.host);
|
||||
return redis;
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
|
@ -47,14 +57,14 @@ const $redisSubscriber: Provider = {
|
|||
@Global()
|
||||
@Module({
|
||||
imports: [RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisSubscriber],
|
||||
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisForPubsub],
|
||||
exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.db) private db: DataSource,
|
||||
@Inject(DI.redis) private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis,
|
||||
) {}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
|
|
@ -69,7 +79,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
this.redisSubscriber.disconnect(),
|
||||
this.redisForPubsub.disconnect(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,22 @@ export type Source = {
|
|||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
redisForPubsub?: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
redisForJobQueue?: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
elasticsearch: {
|
||||
host: string;
|
||||
port: number;
|
||||
|
|
@ -91,6 +107,8 @@ export type Mixin = {
|
|||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
redisForPubsub: NonNullable<Source['redisForPubsub']>;
|
||||
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
|
||||
};
|
||||
|
||||
export type Config = Source & Mixin;
|
||||
|
|
@ -151,6 +169,8 @@ export function loadConfig() {
|
|||
: null;
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
|
||||
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
|
@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
this.antennasFetched = false;
|
||||
this.antennas = [];
|
||||
|
||||
this.redisSubscriber.on('message', this.onRedisMessage);
|
||||
this.redisForPubsub.on('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onRedisMessage);
|
||||
this.redisForPubsub.off('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export class CacheService implements OnApplicationShutdown {
|
|||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
|
@ -116,7 +116,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -167,6 +167,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onMessage);
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { EmojisRepository, Note } from '@/models/index.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
private cache: MemoryKVCache<Emoji | null>;
|
||||
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
|
|
@ -32,9 +36,16 @@ export class CustomEmojiService {
|
|||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reactionService: ReactionService,
|
||||
) {
|
||||
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
|
||||
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
||||
toRedisConverter: (value) => JSON.stringify(value.values()),
|
||||
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -60,7 +71,7 @@ export class CustomEmojiService {
|
|||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (data.host == null) {
|
||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||
|
|
@ -70,6 +81,146 @@ export class CustomEmojiService {
|
|||
return emoji;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: Emoji['id'], data: {
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
}): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
license: data.license,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
const updated = await this.emojiEntityService.packDetailed(emoji.id);
|
||||
|
||||
if (emoji.name === data.name) {
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: [updated],
|
||||
});
|
||||
} else {
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||
});
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: updated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: [...new Set(emoji.aliases.concat(aliases))],
|
||||
});
|
||||
}
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
aliases: aliases,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
|
||||
});
|
||||
}
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
category: category,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(id: Emoji['id']) {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteBulk(ids: Emoji['id'][]) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
id: In(ids),
|
||||
});
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
}
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(emojis),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||
// クエリに使うホスト
|
||||
|
|
@ -84,7 +235,7 @@ export class CustomEmojiService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
|
|
@ -143,30 +294,6 @@ export class CustomEmojiService {
|
|||
return res;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public aggregateNoteEmojis(notes: Note[]) {
|
||||
let emojis: { name: string | null; host: string | null; }[] = [];
|
||||
for (const note of notes) {
|
||||
emojis = emojis.concat(note.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
|
@ -9,23 +10,40 @@ import { bindThis } from '@/decorators.js';
|
|||
|
||||
@Injectable()
|
||||
export class FederatedInstanceService {
|
||||
private cache: MemoryKVCache<Instance>;
|
||||
public federatedInstanceCache: RedisKVCache<Instance | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
|
||||
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
|
||||
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||
memoryCacheLifetime: 1000 * 60 * 30, // 30m
|
||||
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => {
|
||||
const parsed = JSON.parse(value);
|
||||
return {
|
||||
...parsed,
|
||||
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<Instance> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = this.cache.get(host);
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached) return cached;
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
|
@ -37,10 +55,10 @@ export class FederatedInstanceService {
|
|||
firstRetrievedAt: new Date(),
|
||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
this.cache.set(host, i);
|
||||
this.federatedInstanceCache.set(host, i);
|
||||
return i;
|
||||
} else {
|
||||
this.cache.set(host, index);
|
||||
this.federatedInstanceCache.set(host, index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,10 +67,10 @@ export class FederatedInstanceService {
|
|||
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = this.cache.get(host);
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached == null) return;
|
||||
|
||||
this.cache.set(host, {
|
||||
this.federatedInstanceCache.set(host, {
|
||||
...cached,
|
||||
...data,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { IsNull } from 'typeorm';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { MemoryCache } from '@/misc/cache.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
|
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
|||
|
||||
@Injectable()
|
||||
export class InstanceActorService {
|
||||
private cache: MemoryCache<LocalUser>;
|
||||
private cache: MemorySingleCache<LocalUser>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
|
|
@ -19,7 +19,7 @@ export class InstanceActorService {
|
|||
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
) {
|
||||
this.cache = new MemoryCache<LocalUser>(Infinity);
|
||||
this.cache = new MemorySingleCache<LocalUser>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
|
|||
private intervalId: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
|
@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
}, 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
clearInterval(this.intervalId);
|
||||
this.redisSubscriber.off('message', this.onMessage);
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
|||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import type { Channel } from '@/models/entities/Channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { MemoryCache } from '@/misc/cache.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
|
|
@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async readAllNotification(
|
||||
userId: User['id'],
|
||||
force = false,
|
||||
) {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
|
||||
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
||||
|
||||
if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||
return this.postReadAllNotifications(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
...data,
|
||||
} as Notification;
|
||||
|
||||
this.redisClient.xadd(
|
||||
const redisIdPromise = this.redisClient.xadd(
|
||||
`notificationTimeline:${notifieeId}`,
|
||||
'MAXLEN', '~', '300',
|
||||
`${this.idService.parse(notification.id).date.getTime()}-*`,
|
||||
|
|
@ -109,7 +110,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) return;
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
|
||||
|
||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, End
|
|||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
redis: {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
db: config.redis.db ?? 0,
|
||||
port: config.redisForJobQueue.port,
|
||||
host: config.redisForJobQueue.host,
|
||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||
password: config.redisForJobQueue.pass,
|
||||
db: config.redisForJobQueue.db ?? 0,
|
||||
},
|
||||
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
|
||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
|
||||
limiter: limitPerSec > 0 ? {
|
||||
max: limitPerSec,
|
||||
duration: 1000,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
|
|
@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
|
|
@ -60,9 +60,6 @@ export class ReactionService {
|
|||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
|
|
@ -74,6 +71,7 @@ export class ReactionService {
|
|||
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
|
|
@ -104,7 +102,6 @@ export class ReactionService {
|
|||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else {
|
||||
// TODO: cache
|
||||
reaction = await this.toDbReaction(reaction, user.host);
|
||||
}
|
||||
|
||||
|
|
@ -158,21 +155,22 @@ export class ReactionService {
|
|||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||
const decodedReaction = this.decodeReaction(reaction);
|
||||
|
||||
// TODO: Cache
|
||||
const emoji = await this.emojisRepository.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host ?? IsNull(),
|
||||
},
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
});
|
||||
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
||||
: await this.emojisRepository.findOne(
|
||||
{
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host,
|
||||
},
|
||||
});
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||
reaction: decodedReaction.reaction,
|
||||
emoji: emoji != null ? {
|
||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
||||
emoji: customEmoji != null ? {
|
||||
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
||||
} : null,
|
||||
userId: user.id,
|
||||
});
|
||||
|
|
@ -311,10 +309,12 @@ export class ReactionService {
|
|||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
host: reacterHost ?? IsNull(),
|
||||
name,
|
||||
});
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
|||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { MemoryCache } from '@/misc/cache.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { Relay } from '@/models/entities/Relay.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
|
|
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
|||
|
||||
@Injectable()
|
||||
export class RelayService {
|
||||
private relaysCache: MemoryCache<Relay[]>;
|
||||
private relaysCache: MemorySingleCache<Relay[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
|
|
@ -30,7 +30,7 @@ export class RelayService {
|
|||
private createSystemUserService: CreateSystemUserService,
|
||||
private apRendererService: ApRendererService,
|
||||
) {
|
||||
this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
|
||||
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { MemoryKVCache, MemoryCache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
|
@ -57,15 +57,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown {
|
||||
private rolesCache: MemoryCache<Role[]>;
|
||||
private rolesCache: MemorySingleCache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
||||
|
||||
public static AlreadyAssignedError = class extends Error {};
|
||||
public static NotAssignedError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
|
@ -84,10 +84,10 @@ export class RoleService implements OnApplicationShutdown {
|
|||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.rolesCache = new MemoryCache<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
|
||||
this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -400,6 +400,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onMessage);
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
private webhooks: Webhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onMessage);
|
||||
this.redisForPubsub.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
|
|||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
|
|
@ -50,6 +52,7 @@ export class ApRendererService {
|
|||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private ldSignatureService: LdSignatureService,
|
||||
|
|
@ -272,11 +275,7 @@ export class ApRendererService {
|
|||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
// TODO: cache
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
});
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||
}
|
||||
|
|
@ -701,13 +700,9 @@ export class ApRendererService {
|
|||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||
if (names == null || names.length === 0) return [];
|
||||
|
||||
const emojis = await Promise.all(
|
||||
names.map(name => this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
})),
|
||||
);
|
||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||
|
||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
||||
return emojis;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
|
@ -341,15 +342,17 @@ export class ApNoteService {
|
|||
if (!tags) return [];
|
||||
|
||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
||||
|
||||
const existingEmojis = await this.emojisRepository.findBy({
|
||||
host,
|
||||
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
|
||||
});
|
||||
|
||||
return await Promise.all(eomjiTags.map(async tag => {
|
||||
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
|
||||
const name = tag.name!.replaceAll(':', '');
|
||||
tag.icon = toSingle(tag.icon);
|
||||
|
||||
const exists = await this.emojisRepository.findOneBy({
|
||||
host,
|
||||
name,
|
||||
});
|
||||
const exists = existingEmojis.find(x => x.name === name);
|
||||
|
||||
if (exists) {
|
||||
if ((tag.updated != null && exists.updatedAt == null)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import type { UtilityService } from '@/core/UtilityService.js';
|
|||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
|
|
@ -49,6 +50,7 @@ const summaryLength = 2048;
|
|||
export class ApPersonService implements OnModuleInit {
|
||||
private utilityService: UtilityService;
|
||||
private userEntityService: UserEntityService;
|
||||
private driveFileEntityService: DriveFileEntityService;
|
||||
private idService: IdService;
|
||||
private globalEventService: GlobalEventService;
|
||||
private metaService: MetaService;
|
||||
|
|
@ -113,6 +115,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
onModuleInit() {
|
||||
this.utilityService = this.moduleRef.get('UtilityService');
|
||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
||||
this.metaService = this.moduleRef.get('MetaService');
|
||||
|
|
@ -356,32 +359,44 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const avatarId = avatar ? avatar.id : null;
|
||||
const bannerId = banner ? banner.id : null;
|
||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatarBlurhash,
|
||||
bannerBlurhash,
|
||||
});
|
||||
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
//#endregion
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
user!.avatarUrl = avatarUrl;
|
||||
user!.bannerUrl = bannerUrl;
|
||||
user!.avatarBlurhash = avatarBlurhash;
|
||||
user!.bannerBlurhash = bannerBlurhash;
|
||||
//#endregion
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
emojis: emojiNames,
|
||||
});
|
||||
//#endregion
|
||||
await this.usersRepository.update(user!.id, {
|
||||
emojis: emojiNames,
|
||||
});
|
||||
//#endregion
|
||||
|
||||
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||
|
||||
return user!;
|
||||
return user!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -463,10 +478,14 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
if (avatar) {
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
updates.bannerId = banner.id;
|
||||
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
|
||||
// Update user
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ export class DriveFileEntityService {
|
|||
fileIds: DriveFile['id'][],
|
||||
options?: PackOptions,
|
||||
): Promise<Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>> {
|
||||
if (fileIds.length === 0) return new Map();
|
||||
const files = await this.driveFilesRepository.findBy({ id: In(fileIds) });
|
||||
const packedFiles = await this.packMany(files, options);
|
||||
const map = new Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>(packedFiles.map(f => [f.id, f]));
|
||||
|
|
@ -280,6 +281,7 @@ export class DriveFileEntityService {
|
|||
fileIds: DriveFile['id'][],
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
if (fileIds.length === 0) return [];
|
||||
const filesMap = await this.packManyByIdsMap(fileIds, options);
|
||||
return fileIds.map(id => filesMap.get(id)).filter(isNotNull);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,11 @@ export class NoteEntityService implements OnModuleInit {
|
|||
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
|
||||
}
|
||||
|
||||
// パフォーマンスのためノートが作成されてから1秒以上経っていない場合はリアクションを取得しない
|
||||
if (note.createdAt.getTime() + 1000 > Date.now()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const reaction = await this.noteReactionsRepository.findOneBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
|
|
@ -283,7 +288,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}, options);
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const note = typeof src === 'object' ? src : await this.notesRepository.findOneByOrFail({ id: src });
|
||||
const note = typeof src === 'object' ? src : await this.notesRepository.findOneOrFail({ where: { id: src }, relations: ['user'] });
|
||||
const host = note.userHost;
|
||||
|
||||
let text = note.text;
|
||||
|
|
@ -395,7 +400,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
|
||||
if (meId) {
|
||||
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
|
||||
const targets = [...notes.map(n => n.id), ...renoteIds];
|
||||
// パフォーマンスのためノートが作成されてから1秒以上経っていない場合はリアクションを取得しない
|
||||
const targets = [...notes.filter(n => n.createdAt.getTime() + 1000 < Date.now()).map(n => n.id), ...renoteIds];
|
||||
const myReactions = await this.noteReactionsRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: In(targets),
|
||||
|
|
@ -406,10 +412,10 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
|
||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
||||
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
|
||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
|
|
@ -420,6 +426,30 @@ export class NoteEntityService implements OnModuleInit {
|
|||
})));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public aggregateNoteEmojis(notes: Note[]) {
|
||||
let emojis: { name: string | null; host: string | null; }[] = [];
|
||||
for (const note of notes) {
|
||||
emojis = emojis.concat(note.emojis
|
||||
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis
|
||||
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis
|
||||
.map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis
|
||||
.map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
||||
|
|
|
|||
|
|
@ -108,27 +108,30 @@ export class NotificationEntityService implements OnModuleInit {
|
|||
) {
|
||||
if (notifications.length === 0) return [];
|
||||
|
||||
const noteIds = notifications.map(x => x.noteId).filter(isNotNull);
|
||||
let validNotifications = notifications;
|
||||
|
||||
const noteIds = validNotifications.map(x => x.noteId).filter(isNotNull);
|
||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||
where: { id: In(noteIds) },
|
||||
relations: ['user', 'user.avatar', 'user.banner', 'reply', 'reply.user', 'reply.user.avatar', 'reply.user.banner', 'renote', 'renote.user', 'renote.user.avatar', 'renote.user.banner'],
|
||||
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
||||
}) : [];
|
||||
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
||||
detail: true,
|
||||
});
|
||||
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
||||
|
||||
const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
|
||||
validNotifications = validNotifications.filter(x => x.noteId == null || packedNotes.has(x.noteId));
|
||||
|
||||
const userIds = validNotifications.map(x => x.notifierId).filter(isNotNull);
|
||||
const users = userIds.length > 0 ? await this.usersRepository.find({
|
||||
where: { id: In(userIds) },
|
||||
relations: ['avatar', 'banner'],
|
||||
}) : [];
|
||||
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
|
||||
detail: false,
|
||||
});
|
||||
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
||||
|
||||
return await Promise.all(notifications.map(x => this.pack(x, meId, {}, {
|
||||
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
|
||||
packedNotes,
|
||||
packedUsers,
|
||||
})));
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { AntennaService } from '../AntennaService.js';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
|
|
@ -53,7 +53,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
private customEmojiService: CustomEmojiService;
|
||||
private antennaService: AntennaService;
|
||||
private roleService: RoleService;
|
||||
private userInstanceCache: MemoryKVCache<Instance | null>;
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
|
@ -119,7 +119,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
//private antennaService: AntennaService,
|
||||
//private roleService: RoleService,
|
||||
) {
|
||||
this.userInstanceCache = new MemoryKVCache<Instance | null>(1000 * 60 * 60 * 3);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
|
|
@ -129,6 +128,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||
this.antennaService = this.moduleRef.get('AntennaService');
|
||||
this.roleService = this.moduleRef.get('RoleService');
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
}
|
||||
|
||||
//#region Validators
|
||||
|
|
@ -269,27 +269,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getAvatarUrl(user: User): Promise<string> {
|
||||
if (user.avatar) {
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
|
||||
} else if (user.avatarId) {
|
||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
|
||||
return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getAvatarUrlSync(user: User): string {
|
||||
if (user.avatar) {
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getIdenticonUrl(user: User): string {
|
||||
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
|
||||
|
|
@ -309,19 +288,23 @@ export class UserEntityService implements OnModuleInit {
|
|||
includeSecrets: false,
|
||||
}, options);
|
||||
|
||||
let user: User;
|
||||
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
|
||||
|
||||
if (typeof src === 'object') {
|
||||
user = src;
|
||||
if (src.avatar === undefined && src.avatarId) src.avatar = await this.driveFilesRepository.findOneBy({ id: src.avatarId }) ?? null;
|
||||
if (src.banner === undefined && src.bannerId) src.banner = await this.driveFilesRepository.findOneBy({ id: src.bannerId }) ?? null;
|
||||
} else {
|
||||
user = await this.usersRepository.findOneOrFail({
|
||||
where: { id: src },
|
||||
relations: {
|
||||
avatar: true,
|
||||
banner: true,
|
||||
},
|
||||
// migration
|
||||
if (user.avatarId != null && user.avatarUrl === null) {
|
||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
|
||||
user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
this.usersRepository.update(user.id, {
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: avatar.blurhash,
|
||||
});
|
||||
}
|
||||
if (user.bannerId != null && user.bannerUrl === null) {
|
||||
const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
|
||||
user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
this.usersRepository.update(user.id, {
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: banner.blurhash,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -356,14 +339,11 @@ export class UserEntityService implements OnModuleInit {
|
|||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: this.getAvatarUrlSync(user),
|
||||
avatarBlurhash: user.avatar?.blurhash ?? null,
|
||||
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
isBot: user.isBot ?? falsy,
|
||||
isCat: user.isCat ?? falsy,
|
||||
instance: user.host ? this.userInstanceCache.fetch(user.host,
|
||||
() => this.instancesRepository.findOneBy({ host: user.host! }),
|
||||
v => v != null,
|
||||
).then(instance => instance ? {
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
|
|
@ -386,8 +366,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null,
|
||||
bannerBlurhash: user.banner?.blurhash ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||
isSuspended: user.isSuspended ?? falsy,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const DI = {
|
|||
config: Symbol('config'),
|
||||
db: Symbol('db'),
|
||||
redis: Symbol('redis'),
|
||||
redisSubscriber: Symbol('redisSubscriber'),
|
||||
redisForPubsub: Symbol('redisForPubsub'),
|
||||
|
||||
//#region Repositories
|
||||
usersRepository: Symbol('usersRepository'),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,90 @@ export class RedisKVCache<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export class RedisSingleCache<T> {
|
||||
private redisClient: Redis.Redis;
|
||||
private name: string;
|
||||
private lifetime: number;
|
||||
private memoryCache: MemorySingleCache<T>;
|
||||
private fetcher: () => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T;
|
||||
|
||||
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
||||
lifetime: RedisSingleCache<T>['lifetime'];
|
||||
memoryCacheLifetime: number;
|
||||
fetcher: RedisSingleCache<T>['fetcher'];
|
||||
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
||||
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
||||
}) {
|
||||
this.redisClient = redisClient;
|
||||
this.name = name;
|
||||
this.lifetime = opts.lifetime;
|
||||
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
|
||||
this.fetcher = opts.fetcher;
|
||||
this.toRedisConverter = opts.toRedisConverter;
|
||||
this.fromRedisConverter = opts.fromRedisConverter;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async set(value: T): Promise<void> {
|
||||
this.memoryCache.set(value);
|
||||
if (this.lifetime === Infinity) {
|
||||
await this.redisClient.set(
|
||||
`singlecache:${this.name}`,
|
||||
this.toRedisConverter(value),
|
||||
);
|
||||
} else {
|
||||
await this.redisClient.set(
|
||||
`singlecache:${this.name}`,
|
||||
this.toRedisConverter(value),
|
||||
'ex', Math.round(this.lifetime / 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async get(): Promise<T | undefined> {
|
||||
const memoryCached = this.memoryCache.get();
|
||||
if (memoryCached !== undefined) return memoryCached;
|
||||
|
||||
const cached = await this.redisClient.get(`singlecache:${this.name}`);
|
||||
if (cached == null) return undefined;
|
||||
return this.fromRedisConverter(cached);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(): Promise<void> {
|
||||
this.memoryCache.delete();
|
||||
await this.redisClient.del(`singlecache:${this.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(): Promise<T> {
|
||||
const cachedValue = await this.get();
|
||||
if (cachedValue !== undefined) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await this.fetcher();
|
||||
this.set(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async refresh() {
|
||||
const value = await this.fetcher();
|
||||
this.set(value);
|
||||
|
||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
export class MemoryKVCache<T> {
|
||||
|
|
@ -173,12 +257,12 @@ export class MemoryKVCache<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export class MemoryCache<T> {
|
||||
export class MemorySingleCache<T> {
|
||||
private cachedAt: number | null = null;
|
||||
private value: T | undefined;
|
||||
private lifetime: number;
|
||||
|
||||
constructor(lifetime: MemoryCache<never>['lifetime']) {
|
||||
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
|
||||
this.lifetime = lifetime;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,26 @@ export class User {
|
|||
@JoinColumn()
|
||||
public banner: DriveFile | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public avatarUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public bannerUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public avatarBlurhash: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public bannerBlurhash: string | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||
import { MemoryCache } from '@/misc/cache.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
|
|
@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
|
|||
@Injectable()
|
||||
export class DeliverProcessorService {
|
||||
private logger: Logger;
|
||||
private suspendedHostsCache: MemoryCache<Instance[]>;
|
||||
private suspendedHostsCache: MemorySingleCache<Instance[]>;
|
||||
private latest: string | null;
|
||||
|
||||
constructor(
|
||||
|
|
@ -46,7 +46,7 @@ export class DeliverProcessorService {
|
|||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
||||
this.suspendedHostsCache = new MemoryCache<Instance[]>(1000 * 60 * 60);
|
||||
this.suspendedHostsCache = new MemorySingleCache<Instance[]>(1000 * 60 * 60);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ export class ImportCustomEmojisProcessorService {
|
|||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(emojiInfo.name)) {
|
||||
this.logger.error(`invalid emojiname: ${emojiInfo.name}`);
|
||||
continue;
|
||||
}
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await this.emojisRepository.delete({
|
||||
name: emojiInfo.name,
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import Redis from 'ioredis';
|
||||
import { Config } from '@/config.js';
|
||||
|
||||
export function createRedisConnection(config: Config): Redis.Redis {
|
||||
return new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db ?? 0,
|
||||
});
|
||||
}
|
||||
|
|
@ -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 { MemoryCache } 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';
|
||||
|
|
@ -118,7 +118,7 @@ export class NodeinfoServerService {
|
|||
};
|
||||
};
|
||||
|
||||
const cache = new MemoryCache<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(() => nodeinfo2());
|
||||
|
|
|
|||
|
|
@ -149,13 +149,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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,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,
|
||||
|
|
@ -81,7 +81,7 @@ export class StreamingApiServerService {
|
|||
ev.emit(parsed.channel, parsed.message);
|
||||
}
|
||||
|
||||
this.redisSubscriber.on('message', onRedisMessage);
|
||||
this.redisForPubsub.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
|
|
@ -111,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,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
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, IsNull } 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 = {
|
||||
|
|
@ -45,51 +41,19 @@ export const paramDef = {
|
|||
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 });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: ps.name, host: IsNull() });
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== ps.id) throw new ApiError(meta.errors.sameNameEmojiExists);
|
||||
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,
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,16 +95,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
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');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
|
|
|
|||
|
|
@ -93,16 +93,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
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')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (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,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 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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 = {
|
||||
|
|
@ -148,6 +149,7 @@ 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,
|
||||
|
|
@ -170,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;
|
||||
|
|
@ -217,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) {
|
||||
|
|
@ -224,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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -73,16 +73,10 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -91,16 +91,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
.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);
|
||||
|
|
|
|||
|
|
@ -80,16 +80,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
.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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -85,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);
|
||||
|
|
|
|||
|
|
@ -70,16 +70,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.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)];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.notificationService.readAllNotification(me.id);
|
||||
this.notificationService.readAllNotification(me.id, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ export class ClientServerService {
|
|||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await reply.view('user', {
|
||||
user, profile, me,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(user),
|
||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
sub: request.params.sub,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
|
|
@ -464,7 +464,7 @@ export class ClientServerService {
|
|||
return await reply.view('note', {
|
||||
note: _note,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: note.userId })),
|
||||
avatarUrl: _note.user.avatarUrl,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
|
|
@ -503,7 +503,7 @@ export class ClientServerService {
|
|||
return await reply.view('page', {
|
||||
page: _page,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: page.userId })),
|
||||
avatarUrl: _page.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
|
|
@ -527,7 +527,7 @@ export class ClientServerService {
|
|||
return await reply.view('flash', {
|
||||
flash: _flash,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: flash.userId })),
|
||||
avatarUrl: _flash.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
|
|
@ -551,7 +551,7 @@ export class ClientServerService {
|
|||
return await reply.view('clip', {
|
||||
clip: _clip,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: clip.userId })),
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
|
|
@ -573,7 +573,7 @@ export class ClientServerService {
|
|||
return await reply.view('gallery-post', {
|
||||
post: _post,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: post.userId })),
|
||||
avatarUrl: _post.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class FeedService {
|
|||
generator: 'Misskey',
|
||||
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
|
||||
link: author.link,
|
||||
image: await this.userEntityService.getAvatarUrl(user),
|
||||
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
feedLinks: {
|
||||
json: `${author.link}.json`,
|
||||
atom: `${author.link}.atom`,
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
/* TODO
|
||||
test('リモートユーザーの投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'localTimeline', // ayano:Local
|
||||
|
|
@ -191,6 +192,7 @@ describe('Streaming', () => {
|
|||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
*/
|
||||
|
||||
test('ホーム指定の投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
|
|
@ -244,6 +246,7 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
/* TODO
|
||||
test('フォローしているリモートユーザーの投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
|
|
@ -263,6 +266,7 @@ describe('Streaming', () => {
|
|||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
*/
|
||||
|
||||
test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
|
|
@ -316,6 +320,7 @@ describe('Streaming', () => {
|
|||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
/* TODO
|
||||
test('フォローしていないリモートユーザーの投稿が流れる', async () => {
|
||||
const fired = await waitFire(
|
||||
ayano, 'globalTimeline', // ayano:Global
|
||||
|
|
@ -325,6 +330,7 @@ describe('Streaming', () => {
|
|||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
*/
|
||||
|
||||
test('ホーム投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
|
|
|
|||
6
packages/frontend/.storybook/.gitignore
vendored
6
packages/frontend/.storybook/.gitignore
vendored
|
|
@ -1,9 +1,7 @@
|
|||
# (cd path/to/frontend; pnpm tsc -p .storybook)
|
||||
# (cd path/to/frontend; node .storybook/generate.js)
|
||||
/changes.js
|
||||
/generate.js
|
||||
# (cd path/to/frontend; node .storybook/preload-locale.js)
|
||||
/preload-locale.js
|
||||
/locale.ts
|
||||
# (cd path/to/frontend; node .storybook/preload-theme.js)
|
||||
/main.js
|
||||
/preload-theme.js
|
||||
/themes.ts
|
||||
|
|
|
|||
80
packages/frontend/.storybook/changes.ts
Normal file
80
packages/frontend/.storybook/changes.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import micromatch from 'micromatch';
|
||||
import main from './main';
|
||||
|
||||
interface Stats {
|
||||
readonly modules: readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly reasons: readonly {
|
||||
readonly moduleName: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
fs.readFile(
|
||||
path.resolve(__dirname, '../storybook-static/preview-stats.json')
|
||||
).then((buffer) => {
|
||||
const stats: Stats = JSON.parse(buffer.toString());
|
||||
const keys = new Set(stats.modules.map((stat) => stat.id));
|
||||
const map = new Map(
|
||||
Array.from(keys, (key) => [
|
||||
key,
|
||||
new Set(
|
||||
stats.modules
|
||||
.filter((stat) => stat.id === key)
|
||||
.flatMap((stat) => stat.reasons)
|
||||
.map((reason) => reason.moduleName)
|
||||
),
|
||||
])
|
||||
);
|
||||
const modules = new Set(
|
||||
process.argv
|
||||
.slice(2)
|
||||
.map((arg) =>
|
||||
path.relative(
|
||||
path.resolve(__dirname, '..'),
|
||||
path.resolve(__dirname, '../../..', arg)
|
||||
)
|
||||
)
|
||||
.map((path) => (path.startsWith('.') ? path : `./${path}`))
|
||||
);
|
||||
if (
|
||||
micromatch(Array.from(modules), [
|
||||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/**',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
]).length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
const oldSize = modules.size;
|
||||
for (const module of Array.from(modules)) {
|
||||
if (map.has(module)) {
|
||||
for (const dependency of Array.from(map.get(module)!)) {
|
||||
modules.add(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modules.size === oldSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const stories = micromatch(
|
||||
Array.from(modules),
|
||||
main.stories.map((story) => `./${path.relative('..', story)}`)
|
||||
);
|
||||
if (stories.length) {
|
||||
for (const story of stories) {
|
||||
process.stdout.write(` --only-story-files ${story}`);
|
||||
}
|
||||
} else {
|
||||
process.stdout.write(` --skip`);
|
||||
}
|
||||
});
|
||||
|
|
@ -17,6 +17,49 @@ export function abuseUserReport() {
|
|||
};
|
||||
}
|
||||
|
||||
export function galleryPost(isSensitive = false) {
|
||||
return {
|
||||
id: 'somepostid',
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
updatedAt: '2016-12-28T22:49:51.000Z',
|
||||
userid: 'someuserid',
|
||||
user: userDetailed(),
|
||||
title: 'Some post title',
|
||||
description: 'Some post description',
|
||||
fileIds: ['somefileid'],
|
||||
files: [
|
||||
file(isSensitive),
|
||||
],
|
||||
isSensitive,
|
||||
likedCount: 0,
|
||||
isLiked: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function file(isSensitive = false) {
|
||||
return {
|
||||
id: 'somefileid',
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
name: 'somefile.jpg',
|
||||
type: 'image/jpeg',
|
||||
md5: 'f6fc51c73dc21b1fb85ead2cdf57530a',
|
||||
size: 77752,
|
||||
isSensitive,
|
||||
blurhash: 'eQAmoa^-MH8w9ZIvNLSvo^$*MwRPbwtSxutRozjEiwR.RjWBoeozog',
|
||||
properties: {
|
||||
width: 1024,
|
||||
height: 270
|
||||
},
|
||||
url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
|
||||
thumbnailUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
|
||||
comment: null,
|
||||
folderId: null,
|
||||
folder: null,
|
||||
userId: null,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed {
|
||||
return {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ function toStories(component: string): string {
|
|||
Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
])
|
||||
.then((globs) => globs.flat())
|
||||
.then((components) => Promise.all(components.map((component) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { resolve } from 'node:path';
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
import turbosnap from 'vite-plugin-turbosnap';
|
||||
const config = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
|
|
@ -20,8 +21,13 @@ const config = {
|
|||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
async viteFinal(config, options) {
|
||||
async viteFinal(config) {
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
turbosnap({
|
||||
rootDir: config.root ?? process.cwd(),
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
target: [
|
||||
'chrome108',
|
||||
|
|
|
|||
|
|
@ -18,5 +18,10 @@
|
|||
"jsx": "react",
|
||||
"jsxFactory": "h"
|
||||
},
|
||||
"files": ["./generate.tsx", "./preload-locale.ts", "./preload-theme.ts"]
|
||||
"files": [
|
||||
"./changes.ts",
|
||||
"./generate.tsx",
|
||||
"./preload-locale.ts",
|
||||
"./preload-theme.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@
|
|||
"strict-event-emitter-types": "2.0.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.150.1",
|
||||
"three": "0.151.3",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.0.2",
|
||||
"typescript": "5.0.3",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.2.1",
|
||||
|
|
@ -74,31 +74,32 @@
|
|||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^7.0.2",
|
||||
"@storybook/addon-essentials": "^7.0.2",
|
||||
"@storybook/addon-interactions": "^7.0.2",
|
||||
"@storybook/addon-links": "^7.0.2",
|
||||
"@storybook/addon-storysource": "^7.0.2",
|
||||
"@storybook/addons": "^7.0.2",
|
||||
"@storybook/blocks": "^7.0.2",
|
||||
"@storybook/core-events": "^7.0.2",
|
||||
"@storybook/jest": "~0.1.0",
|
||||
"@storybook/manager-api": "^7.0.2",
|
||||
"@storybook/preview-api": "^7.0.2",
|
||||
"@storybook/react": "^7.0.2",
|
||||
"@storybook/react-vite": "^7.0.2",
|
||||
"@storybook/testing-library": "~0.0.14-next.1",
|
||||
"@storybook/theming": "^7.0.2",
|
||||
"@storybook/types": "^7.0.2",
|
||||
"@storybook/vue3": "^7.0.2",
|
||||
"@storybook/vue3-vite": "^7.0.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@storybook/addon-actions": "7.0.2",
|
||||
"@storybook/addon-essentials": "7.0.2",
|
||||
"@storybook/addon-interactions": "7.0.2",
|
||||
"@storybook/addon-links": "7.0.2",
|
||||
"@storybook/addon-storysource": "7.0.2",
|
||||
"@storybook/addons": "7.0.2",
|
||||
"@storybook/blocks": "7.0.2",
|
||||
"@storybook/core-events": "7.0.2",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.2",
|
||||
"@storybook/preview-api": "7.0.2",
|
||||
"@storybook/react": "7.0.2",
|
||||
"@storybook/react-vite": "7.0.2",
|
||||
"@storybook/testing-library": "0.0.14-next.1",
|
||||
"@storybook/theming": "7.0.2",
|
||||
"@storybook/types": "7.0.2",
|
||||
"@storybook/vue3": "7.0.2",
|
||||
"@storybook/vue3-vite": "7.0.2",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree": "1.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/micromatch": "3.1.1",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
|
|
@ -109,32 +110,34 @@
|
|||
"@types/uuid": "9.0.1",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@vitest/coverage-c8": "^0.29.8",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"astring": "^1.8.4",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"chromatic": "^6.17.2",
|
||||
"astring": "1.8.4",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"chromatic": "6.17.3",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.10.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "8.9.0",
|
||||
"msw": "^1.1.0",
|
||||
"msw-storybook-addon": "^1.8.0",
|
||||
"prettier": "^2.8.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
"prettier": "2.8.7",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "^7.0.2",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vitest": "^0.29.8",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vite-plugin-turbosnap": "^1.0.1",
|
||||
"vitest": "0.29.8",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.1.1",
|
||||
"vue-tsc": "1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { galleryPost } from '../../.storybook/fakes';
|
||||
import MkGalleryPostPreview from './MkGalleryPostPreview.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkGalleryPostPreview,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkGalleryPostPreview v-bind="props" />',
|
||||
};
|
||||
},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const links = canvas.getAllByRole('link');
|
||||
await expect(links).toHaveLength(2);
|
||||
await expect(links[0]).toHaveAttribute('href', `/gallery/${galleryPost().id}`);
|
||||
await expect(links[1]).toHaveAttribute('href', `/@${galleryPost().user.username}@${galleryPost().user.host}`);
|
||||
},
|
||||
args: {
|
||||
post: galleryPost(),
|
||||
},
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="width:260px"><story /></div>',
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const Hover = {
|
||||
...Default,
|
||||
async play(context) {
|
||||
await Default.play(context);
|
||||
const canvas = within(context.canvasElement);
|
||||
const links = canvas.getAllByRole('link');
|
||||
await waitFor(() => userEvent.hover(links[0]));
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const HoverThenUnhover = {
|
||||
...Default,
|
||||
async play(context) {
|
||||
await Hover.play(context);
|
||||
const canvas = within(context.canvasElement);
|
||||
const links = canvas.getAllByRole('link');
|
||||
await waitFor(() => userEvent.unhover(links[0]));
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const Sensitive = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
post: galleryPost(true),
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const SensitiveHover = {
|
||||
...Hover,
|
||||
args: {
|
||||
...Hover.args,
|
||||
post: galleryPost(true),
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
export const SensitiveHoverThenUnhover = {
|
||||
...HoverThenUnhover,
|
||||
args: {
|
||||
...HoverThenUnhover.args,
|
||||
post: galleryPost(true),
|
||||
},
|
||||
} satisfies StoryObj<typeof MkGalleryPostPreview>;
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
|
||||
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1" @pointerenter="enterHover" @pointerleave="leaveHover">
|
||||
<div class="thumbnail">
|
||||
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
|
||||
<ImgWithBlurhash class="img" :hash="post.files[0].blurhash"/>
|
||||
<Transition>
|
||||
<ImgWithBlurhash v-if="show" class="img layered" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
|
||||
</Transition>
|
||||
</div>
|
||||
<article>
|
||||
<header>
|
||||
|
|
@ -15,12 +18,25 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
post: any;
|
||||
post: misskey.entities.GalleryPost;
|
||||
}>();
|
||||
|
||||
const hover = ref(false);
|
||||
const show = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive || hover.value);
|
||||
|
||||
function enterHover(): void {
|
||||
hover.value = true;
|
||||
}
|
||||
|
||||
function leaveHover(): void {
|
||||
hover.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -56,6 +72,21 @@ const props = defineProps<{
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
||||
&.layered {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&.v-enter-active,
|
||||
&.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
&.v-enter-from,
|
||||
&.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -964,7 +964,7 @@ defineExpose({
|
|||
padding: 0 12px;
|
||||
line-height: 34px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
min-width: 90px;
|
||||
box-sizing: border-box;
|
||||
color: var(--fgOnAccent);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const Default = {
|
|||
};
|
||||
},
|
||||
},
|
||||
template: '<MkA v-bind="props">Text</MkA>',
|
||||
template: '<MkA v-bind="props">Misskey</MkA>',
|
||||
};
|
||||
},
|
||||
async play({ canvasElement }) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { waitFor } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkPageHeader from './MkPageHeader.vue';
|
||||
export const Empty = {
|
||||
|
|
@ -22,16 +23,16 @@ export const Empty = {
|
|||
template: '<MkPageHeader v-bind="props" />',
|
||||
};
|
||||
},
|
||||
async play() {
|
||||
const wait = new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await waitFor(async () => await wait);
|
||||
},
|
||||
args: {
|
||||
static: true,
|
||||
tabs: [],
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
chromatic: {
|
||||
/* This component has animations that are implemented with JavaScript. So it's unstable to take a snapshot. */
|
||||
disableSnapshot: true,
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkPageHeader>;
|
||||
export const OneTab = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { commonHandlers } from '../../../.storybook/mocks';
|
||||
|
|
@ -30,7 +30,7 @@ export const Default = {
|
|||
const canvas = within(canvasElement);
|
||||
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
||||
await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/');
|
||||
await userEvent.hover(a);
|
||||
await waitFor(() => userEvent.hover(a));
|
||||
/*
|
||||
await tick(); // FIXME: wait for network request
|
||||
const anchors = canvas.getAllByRole<HTMLAnchorElement>('link');
|
||||
|
|
@ -44,7 +44,7 @@ export const Default = {
|
|||
await expect(icon).toBeInTheDocument();
|
||||
await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico');
|
||||
*/
|
||||
await userEvent.unhover(a);
|
||||
await waitFor(() => userEvent.unhover(a));
|
||||
},
|
||||
args: {
|
||||
url: 'https://misskey-hub.net/',
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ const patrons = [
|
|||
'氷月氷華里',
|
||||
'Ebise Lutica',
|
||||
'巣黒るい@リスケモ男の娘VTuber!',
|
||||
'ふぇいぽむ',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ hr {
|
|||
}
|
||||
|
||||
.ti {
|
||||
width: 1.28em;
|
||||
vertical-align: -12%;
|
||||
line-height: 1em;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
|
||||
</button>
|
||||
<div class="post" @click="post">
|
||||
<div class="post" @click="os.post()">
|
||||
<MkButton class="button" gradate full rounded>
|
||||
<i class="ti ti-pencil ti-fw"></i>
|
||||
</MkButton>
|
||||
|
|
@ -41,93 +41,50 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { openInstanceMenu } from './_common_/common';
|
||||
import { host } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { navbarItemDef } from '@/navbar';
|
||||
import { openAccountMenu, $i } from '@/account';
|
||||
import { openAccountMenu as openAccountMenu_, $i } from '@/account';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { mainRouter } from '@/router';
|
||||
import { defaultStore } from '@/store';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
const WINDOW_THRESHOLD = 1400;
|
||||
|
||||
data() {
|
||||
return {
|
||||
host: host,
|
||||
accounts: [],
|
||||
connection: null,
|
||||
navbarItemDef: navbarItemDef,
|
||||
settingsWindowed: false,
|
||||
defaultStore,
|
||||
instance,
|
||||
$i,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
menu(): string[] {
|
||||
return defaultStore.state.menu;
|
||||
},
|
||||
|
||||
otherNavItemIndicated(): boolean {
|
||||
for (const def in this.navbarItemDef) {
|
||||
if (this.menu.includes(def)) continue;
|
||||
if (this.navbarItemDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'defaultStore.reactiveState.menuDisplay.value'() {
|
||||
this.calcViewState();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('resize', this.calcViewState);
|
||||
this.calcViewState();
|
||||
},
|
||||
|
||||
methods: {
|
||||
openInstanceMenu,
|
||||
|
||||
calcViewState() {
|
||||
this.settingsWindowed = (window.innerWidth > 1400);
|
||||
},
|
||||
|
||||
post() {
|
||||
os.post();
|
||||
},
|
||||
|
||||
search() {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
|
||||
more(ev) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
anchor: { x: 'center', y: 'bottom' },
|
||||
}, {
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
openAccountMenu: (ev) => {
|
||||
openAccountMenu({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
},
|
||||
},
|
||||
let settingsWindowed = $ref(window.innerWidth > WINDOW_THRESHOLD);
|
||||
let menu = $ref(defaultStore.state.menu);
|
||||
// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
|
||||
let otherNavItemIndicated = computed<boolean>(() => {
|
||||
for (const def in navbarItemDef) {
|
||||
if (menu.includes(def)) continue;
|
||||
if (navbarItemDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
function more(ev: MouseEvent) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
anchor: { x: 'center', y: 'bottom' },
|
||||
}, {
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function openAccountMenu(ev: MouseEvent) {
|
||||
openAccountMenu_({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
settingsWindowed = (window.innerWidth >= WINDOW_THRESHOLD);
|
||||
}, { passive: true });
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||
</button>
|
||||
<div class="post" data-cy-open-post-form @click="post">
|
||||
<div class="post" data-cy-open-post-form @click="os.post">
|
||||
<MkButton class="button" gradate full rounded>
|
||||
<i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span>
|
||||
</MkButton>
|
||||
|
|
@ -40,109 +40,59 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, computed, watch, nextTick } from 'vue';
|
||||
import { openInstanceMenu } from './_common_/common';
|
||||
import { host } from '@/config';
|
||||
// import { host } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { navbarItemDef } from '@/navbar';
|
||||
import { openAccountMenu, $i } from '@/account';
|
||||
import { openAccountMenu as openAccountMenu_, $i } from '@/account';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
import { mainRouter } from '@/router';
|
||||
// import { StickySidebar } from '@/scripts/sticky-sidebar';
|
||||
// import { mainRouter } from '@/router';
|
||||
//import MisskeyLogo from '@assets/client/misskey.svg';
|
||||
import { defaultStore } from '@/store';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
//MisskeyLogo,
|
||||
},
|
||||
const WINDOW_THRESHOLD = 1400;
|
||||
|
||||
data() {
|
||||
return {
|
||||
host: host,
|
||||
accounts: [],
|
||||
connection: null,
|
||||
navbarItemDef: navbarItemDef,
|
||||
iconOnly: false,
|
||||
settingsWindowed: false,
|
||||
defaultStore,
|
||||
instance,
|
||||
$i,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
menu(): string[] {
|
||||
return this.defaultStore.state.menu;
|
||||
},
|
||||
|
||||
otherNavItemIndicated(): boolean {
|
||||
for (const def in this.navbarItemDef) {
|
||||
if (this.menu.includes(def)) continue;
|
||||
if (this.navbarItemDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'defaultStore.reactiveState.menuDisplay.value'() {
|
||||
this.calcViewState();
|
||||
},
|
||||
|
||||
iconOnly() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('change-view-mode');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('resize', this.calcViewState);
|
||||
this.calcViewState();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const sticky = new StickySidebar(this.$el.parentElement, 16);
|
||||
window.addEventListener('scroll', () => {
|
||||
sticky.calc(window.scrollY);
|
||||
}, { passive: true });
|
||||
},
|
||||
|
||||
methods: {
|
||||
openInstanceMenu,
|
||||
|
||||
calcViewState() {
|
||||
this.iconOnly = (window.innerWidth <= 1400) || (this.defaultStore.state.menuDisplay === 'sideIcon');
|
||||
this.settingsWindowed = (window.innerWidth > 1400);
|
||||
},
|
||||
|
||||
post() {
|
||||
os.post();
|
||||
},
|
||||
|
||||
search() {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
|
||||
more(ev) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
openAccountMenu: (ev) => {
|
||||
openAccountMenu({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
},
|
||||
},
|
||||
const menu = $ref(defaultStore.state.menu);
|
||||
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
|
||||
const otherNavItemIndicated = computed<boolean>(() => {
|
||||
for (const def in navbarItemDef) {
|
||||
if (menu.includes(def)) continue;
|
||||
if (navbarItemDef[def].indicated) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
let el = $shallowRef<HTMLElement>();
|
||||
// let accounts = $ref([]);
|
||||
// let connection = $ref(null);
|
||||
let iconOnly = $ref(false);
|
||||
let settingsWindowed = $ref(false);
|
||||
|
||||
function calcViewState() {
|
||||
iconOnly = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon');
|
||||
settingsWindowed = (window.innerWidth > WINDOW_THRESHOLD);
|
||||
}
|
||||
|
||||
function more(ev: MouseEvent) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||
src: ev.currentTarget ?? ev.target,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
function openAccountMenu(ev: MouseEvent) {
|
||||
openAccountMenu_({
|
||||
withExtraOperation: true,
|
||||
}, ev);
|
||||
}
|
||||
|
||||
watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||
calcViewState();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -166,8 +166,6 @@ export type Channels = {
|
|||
readAllAntennas: () => void;
|
||||
unreadAntenna: (payload: Antenna) => void;
|
||||
readAllAnnouncements: () => void;
|
||||
readAllChannels: () => void;
|
||||
unreadChannel: (payload: Note['id']) => void;
|
||||
myTokenRegenerated: () => void;
|
||||
reversiNoInvites: () => void;
|
||||
reversiInvited: (payload: FIXME) => void;
|
||||
|
|
@ -1857,12 +1855,6 @@ export type Endpoints = {
|
|||
req: NoParams;
|
||||
res: null;
|
||||
};
|
||||
'notifications/read': {
|
||||
req: {
|
||||
notificationId: Notification_2['id'];
|
||||
};
|
||||
res: null;
|
||||
};
|
||||
'page-push': {
|
||||
req: {
|
||||
pageId: Page['id'];
|
||||
|
|
@ -2361,7 +2353,6 @@ type MeDetailed = UserDetailed & {
|
|||
hasPendingReceivedFollowRequest: boolean;
|
||||
hasUnreadAnnouncement: boolean;
|
||||
hasUnreadAntenna: boolean;
|
||||
hasUnreadChannel: boolean;
|
||||
hasUnreadMentions: boolean;
|
||||
hasUnreadMessagingMessage: boolean;
|
||||
hasUnreadNotification: boolean;
|
||||
|
|
@ -2618,7 +2609,11 @@ export class Stream extends EventEmitter<StreamEvents> {
|
|||
// (undocumented)
|
||||
removeSharedConnectionPool(pool: Pool): void;
|
||||
// (undocumented)
|
||||
send(typeOrPayload: any, payload?: any): void;
|
||||
send(typeOrPayload: string): void;
|
||||
// (undocumented)
|
||||
send(typeOrPayload: string, payload: any): void;
|
||||
// (undocumented)
|
||||
send(typeOrPayload: Record<string, any> | any[]): void;
|
||||
// (undocumented)
|
||||
state: 'initializing' | 'reconnecting' | 'connected';
|
||||
// (undocumented)
|
||||
|
|
@ -2714,8 +2709,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
|||
//
|
||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:595:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:35:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||
// src/api.types.ts:594:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,23 +24,23 @@
|
|||
"@swc/jest": "0.2.24",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"eslint": "8.37.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest": "29.5.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-websocket-mock": "2.4.0",
|
||||
"mock-socket": "9.2.1",
|
||||
"tsd": "0.28.1",
|
||||
"typescript": "5.0.2"
|
||||
"typescript": "5.0.3"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.42",
|
||||
"@swc/core": "1.3.46",
|
||||
"eventemitter3": "5.0.0",
|
||||
"reconnecting-websocket": "^4.4.0"
|
||||
"reconnecting-websocket": "4.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,14 +169,21 @@ export default class Stream extends EventEmitter<StreamEvents> {
|
|||
|
||||
/**
|
||||
* Send a message to connection
|
||||
* ! ストリーム上のやり取りはすべてJSONで行われます !
|
||||
*/
|
||||
public send(typeOrPayload: any, payload?: any): void {
|
||||
const data = payload === undefined ? typeOrPayload : {
|
||||
type: typeOrPayload,
|
||||
body: payload,
|
||||
};
|
||||
public send(typeOrPayload: string): void
|
||||
public send(typeOrPayload: string, payload: any): void
|
||||
public send(typeOrPayload: Record<string, any> | any[]): void
|
||||
public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
|
||||
if (typeof typeOrPayload === 'string') {
|
||||
this.stream.send(JSON.stringify({
|
||||
type: typeOrPayload,
|
||||
...(payload === undefined ? {} : { body: payload }),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.stream.send(JSON.stringify(data));
|
||||
this.stream.send(JSON.stringify(typeOrPayload));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"typescript": "5.0.2"
|
||||
"typescript": "5.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export async function findClient() {
|
|||
type: 'window',
|
||||
});
|
||||
for (const c of clients) {
|
||||
if (c.url.indexOf('?zen') < 0) return c;
|
||||
if (!new URL(c.url).searchParams.has('zen')) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue