Merge branch 'develop' into emoji-re
This commit is contained in:
@@ -9,7 +9,17 @@
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
},
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "es2021"
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
export class flashScriptLength1674086433654 {
|
||||
name = 'flashScriptLength1674086433654'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(32768)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "script" TYPE character varying(16384)`);
|
||||
}
|
||||
}
|
33
packages/backend/migration/1674118260469-achievement.js
Normal file
33
packages/backend/migration/1674118260469-achievement.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export class achievement1674118260469 {
|
||||
name = 'achievement1674118260469'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "notification" ADD "achievement" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "achievements" jsonb NOT NULL DEFAULT '[]'`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "achievements"`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "achievement"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1674255666603-loggedInDates.js
Normal file
11
packages/backend/migration/1674255666603-loggedInDates.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export class loggedInDates1674255666603 {
|
||||
name = 'loggedInDates1674255666603'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "loggedInDates" character varying(32) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "loggedInDates"`);
|
||||
}
|
||||
}
|
@@ -7,6 +7,8 @@
|
||||
"start": "node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"build:swc": "swc src -d built -D",
|
||||
"watch:swc": "swc src -d built -D -w",
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "tsc --noEmit && eslint --quiet \"src/**/*.ts\"",
|
||||
@@ -56,9 +58,9 @@
|
||||
"date-fns": "2.29.3",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.11.0",
|
||||
"fastify": "4.12.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.1.0",
|
||||
"file-type": "18.2.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"got": "12.5.3",
|
||||
@@ -87,7 +89,7 @@
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.2.0",
|
||||
"punycode": "2.3.0",
|
||||
"pureimage": "0.3.15",
|
||||
"qrcode": "1.5.1",
|
||||
"random-seed": "0.3.0",
|
||||
@@ -118,7 +120,7 @@
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.4",
|
||||
"ulid": "2.3.0",
|
||||
"undici": "^5.15.0",
|
||||
"undici": "^5.15.1",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
@@ -129,7 +131,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@swc/core": "1.3.26",
|
||||
"@swc/cli": "^0.1.59",
|
||||
"@swc/core": "1.3.27",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
@@ -141,7 +144,7 @@
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
@@ -173,11 +176,11 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||
"@typescript-eslint/parser": "5.48.2",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-plugin-import": "2.27.4",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.3.1",
|
||||
"jest-mock": "^29.3.1",
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServerModule } from '@/server/ServerModule.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
|
||||
import { DaemonModule } from '@/daemons/DaemonModule.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
ServerModule,
|
||||
QueueProcessorModule,
|
||||
DaemonModule,
|
||||
],
|
||||
})
|
||||
export class RootModule {}
|
||||
export class MainModule {}
|
@@ -17,6 +17,9 @@ import { JanitorService } from '@/daemons/JanitorService.js';
|
||||
import { QueueStatsService } from '@/daemons/QueueStatsService.js';
|
||||
import { ServerStatsService } from '@/daemons/ServerStatsService.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
import { MainModule } from '@/MainModule.js';
|
||||
import { envOption } from '../env.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
@@ -70,6 +73,15 @@ export async function masterMain() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = await NestFactory.createApplicationContext(MainModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// start server
|
||||
const serverService = app.get(ServerService);
|
||||
serverService.launch();
|
||||
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
@@ -78,15 +90,10 @@ export async function masterMain() {
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
|
||||
if (!envOption.noDaemons) {
|
||||
const daemons = await NestFactory.createApplicationContext(DaemonModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
daemons.enableShutdownHooks();
|
||||
daemons.get(JanitorService).start();
|
||||
daemons.get(QueueStatsService).start();
|
||||
daemons.get(ServerStatsService).start();
|
||||
}
|
||||
app.get(ChartManagementService).start();
|
||||
app.get(JanitorService).start();
|
||||
app.get(QueueStatsService).start();
|
||||
app.get(ServerStatsService).start();
|
||||
}
|
||||
|
||||
function showEnvironment(): void {
|
||||
|
@@ -1,32 +1,23 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { envOption } from '@/env.js';
|
||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { RootModule } from '../RootModule.js';
|
||||
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
*/
|
||||
export async function workerMain() {
|
||||
const app = await NestFactory.createApplicationContext(RootModule, {
|
||||
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// start server
|
||||
const serverService = app.get(ServerService);
|
||||
serverService.launch();
|
||||
jobQueue.enableShutdownHooks();
|
||||
|
||||
// start job queue
|
||||
if (!envOption.onlyServer) {
|
||||
const queueProcessorService = app.get(QueueProcessorService);
|
||||
queueProcessorService.start();
|
||||
}
|
||||
jobQueue.get(QueueProcessorService).start();
|
||||
|
||||
app.get(ChartManagementService).run();
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
|
||||
if (cluster.isWorker) {
|
||||
// Send a 'ready' message to parent process
|
||||
|
121
packages/backend/src/core/AchievementService.ts
Normal file
121
packages/backend/src/core/AchievementService.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
|
||||
const ACHIEVEMENT_TYPES = [
|
||||
'notes1',
|
||||
'notes10',
|
||||
'notes100',
|
||||
'notes500',
|
||||
'notes1000',
|
||||
'notes5000',
|
||||
'notes10000',
|
||||
'notes20000',
|
||||
'notes30000',
|
||||
'notes40000',
|
||||
'notes50000',
|
||||
'notes60000',
|
||||
'notes70000',
|
||||
'notes80000',
|
||||
'notes90000',
|
||||
'notes100000',
|
||||
'login3',
|
||||
'login7',
|
||||
'login15',
|
||||
'login30',
|
||||
'login60',
|
||||
'login100',
|
||||
'login200',
|
||||
'login300',
|
||||
'login400',
|
||||
'login500',
|
||||
'login600',
|
||||
'login700',
|
||||
'login800',
|
||||
'login900',
|
||||
'login1000',
|
||||
'passedSinceAccountCreated1',
|
||||
'passedSinceAccountCreated2',
|
||||
'passedSinceAccountCreated3',
|
||||
'loggedInOnBirthday',
|
||||
'loggedInOnNewYearsDay',
|
||||
'noteClipped1',
|
||||
'noteFavorited1',
|
||||
'myNoteFavorited1',
|
||||
'profileFilled',
|
||||
'markedAsCat',
|
||||
'following1',
|
||||
'following10',
|
||||
'following50',
|
||||
'following100',
|
||||
'following300',
|
||||
'followers1',
|
||||
'followers10',
|
||||
'followers50',
|
||||
'followers100',
|
||||
'followers300',
|
||||
'followers500',
|
||||
'followers1000',
|
||||
'collectAchievements30',
|
||||
'viewAchievements3min',
|
||||
'iLoveMisskey',
|
||||
'foundTreasure',
|
||||
'client30min',
|
||||
'noteDeletedWithin1min',
|
||||
'postedAtLateNight',
|
||||
'postedAt0min0sec',
|
||||
'selfQuote',
|
||||
'htl20npm',
|
||||
'viewInstanceChart',
|
||||
'outputHelloWorldOnScratchpad',
|
||||
'open3windows',
|
||||
'driveFolderCircularReference',
|
||||
'reactWithoutRead',
|
||||
'clickedClickHere',
|
||||
'justPlainLucky',
|
||||
'setNameToSyuilo',
|
||||
'cookieClicked',
|
||||
'brainDiver',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
export class AchievementService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private createNotificationService: CreateNotificationService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
userId: User['id'],
|
||||
type: typeof ACHIEVEMENT_TYPES[number],
|
||||
): Promise<void> {
|
||||
if (!ACHIEVEMENT_TYPES.includes(type)) return;
|
||||
|
||||
const date = Date.now();
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: userId });
|
||||
|
||||
if (profile.achievements.some(a => a.name === type)) return;
|
||||
|
||||
await this.userProfilesRepository.update(userId, {
|
||||
achievements: [...profile.achievements, {
|
||||
name: type,
|
||||
unlockedAt: date,
|
||||
}],
|
||||
});
|
||||
|
||||
this.createNotificationService.createNotification(userId, 'achievementEarned', {
|
||||
achievement: type,
|
||||
});
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import { AppLockService } from './AppLockService.js';
|
||||
import { AchievementService } from './AchievementService.js';
|
||||
import { CaptchaService } from './CaptchaService.js';
|
||||
import { CreateNotificationService } from './CreateNotificationService.js';
|
||||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||
@@ -128,6 +129,7 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx
|
||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
|
||||
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
|
||||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||
@@ -255,6 +257,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AiService,
|
||||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
CaptchaService,
|
||||
CreateNotificationService,
|
||||
CreateSystemUserService,
|
||||
@@ -376,6 +379,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AiService,
|
||||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$CaptchaService,
|
||||
$CreateNotificationService,
|
||||
$CreateSystemUserService,
|
||||
@@ -498,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AiService,
|
||||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
CaptchaService,
|
||||
CreateNotificationService,
|
||||
CreateSystemUserService,
|
||||
@@ -618,6 +623,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AiService,
|
||||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$CaptchaService,
|
||||
$CreateNotificationService,
|
||||
$CreateSystemUserService,
|
||||
|
@@ -125,7 +125,7 @@ export class UndiciFetcher {
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
}).catch((err) => {
|
||||
this.logger?.error('fetch error', err);
|
||||
this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err);
|
||||
throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable');
|
||||
});
|
||||
if (!res.ok && !privateOptions.noOkError) {
|
||||
|
@@ -57,7 +57,7 @@ export class ApRequestService {
|
||||
method: 'POST',
|
||||
headers: this.objectAssignWithLcKey({
|
||||
'Date': new Date().toUTCString(),
|
||||
'Host': u.hostname,
|
||||
'Host': u.host,
|
||||
'Content-Type': 'application/activity+json',
|
||||
'Digest': digestHeader,
|
||||
}, args.additionalHeaders),
|
||||
@@ -83,7 +83,7 @@ export class ApRequestService {
|
||||
headers: this.objectAssignWithLcKey({
|
||||
'Accept': 'application/activity+json, application/ld+json',
|
||||
'Date': new Date().toUTCString(),
|
||||
'Host': new URL(args.url).hostname,
|
||||
'Host': new URL(args.url).host,
|
||||
}, args.additionalHeaders),
|
||||
};
|
||||
|
||||
@@ -106,6 +106,8 @@ export class ApRequestService {
|
||||
request.headers = this.objectAssignWithLcKey(request.headers, {
|
||||
Signature: signatureHeader,
|
||||
});
|
||||
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
|
||||
delete request.headers['host'];
|
||||
|
||||
return {
|
||||
request,
|
||||
|
@@ -54,7 +54,7 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async run() {
|
||||
public async start() {
|
||||
// 20分おきにメモリ情報をDBに書き込み
|
||||
this.saveIntervalId = setInterval(() => {
|
||||
for (const chart of this.charts) {
|
||||
|
@@ -22,7 +22,7 @@ export class EmojiEntityService {
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: Emoji['id'] | Emoji,
|
||||
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
|
||||
): Promise<Packed<'Emoji'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
||||
|
||||
@@ -32,13 +32,15 @@ export class EmojiEntityService {
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
host: opts.omitHost ? undefined : emoji.host,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: opts.withUrl ? (emoji.publicUrl || emoji.originalUrl) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
emojis: any[],
|
||||
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.pack(x, opts)));
|
||||
}
|
||||
|
@@ -114,6 +114,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
...(notification.type === 'groupInvited' ? {
|
||||
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
|
||||
} : {}),
|
||||
...(notification.type === 'achievementEarned' ? {
|
||||
achievement: notification.achievement,
|
||||
} : {}),
|
||||
...(notification.type === 'app' ? {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader ?? token?.name,
|
||||
|
@@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { ILocalUser, IRemoteUser, 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, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@@ -343,6 +343,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
options?: {
|
||||
detail?: D,
|
||||
includeSecrets?: boolean,
|
||||
userProfile?: UserProfile,
|
||||
},
|
||||
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
|
||||
const opts = Object.assign({
|
||||
@@ -375,7 +376,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
.innerJoinAndSelect('pin.note', 'note')
|
||||
.orderBy('pin.id', 'DESC')
|
||||
.getMany() : [];
|
||||
const profile = opts.detail ? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
|
||||
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
|
||||
|
||||
const followingCount = profile == null ? null :
|
||||
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
|
||||
@@ -493,6 +494,8 @@ export class UserEntityService implements OnModuleInit {
|
||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
||||
achievements: profile!.achievements,
|
||||
loggedInDays: profile!.loggedInDates.length,
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
|
@@ -44,7 +44,7 @@ export class Flash {
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16384,
|
||||
length: 32768,
|
||||
})
|
||||
public script: string;
|
||||
|
||||
|
@@ -64,6 +64,7 @@ export class Notification {
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
* groupInvited - グループに招待された
|
||||
* achievementEarned - 実績を獲得
|
||||
* app - アプリ通知
|
||||
*/
|
||||
@Index()
|
||||
@@ -129,6 +130,11 @@ export class Notification {
|
||||
})
|
||||
public choice: number | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public achievement: string | null;
|
||||
|
||||
/**
|
||||
* アプリ通知のbody
|
||||
*/
|
||||
|
@@ -213,6 +213,19 @@ export class UserProfile {
|
||||
})
|
||||
public mutingNotificationTypes: typeof notificationTypes[number][];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, array: true, default: '{}',
|
||||
})
|
||||
public loggedInDates: string[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public achievements: {
|
||||
name: string;
|
||||
unlockedAt: number;
|
||||
}[];
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
@@ -29,5 +29,9 @@ export const packedEmojiSchema = {
|
||||
optional: true, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
|
||||
@@ -34,6 +35,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
CoreModule,
|
||||
],
|
||||
providers: [
|
||||
|
@@ -14,6 +14,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ApiServerService } from './api/ApiServerService.js';
|
||||
@@ -22,7 +23,6 @@ import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { MediaProxyServerService } from './MediaProxyServerService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ServerService {
|
||||
@@ -82,13 +82,13 @@ export class ServerService {
|
||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||
const path = request.params.path;
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
const name = path.split('@')[0].replace('.webp', '');
|
||||
const host = path.split('@')[1]?.replace('.webp', '');
|
||||
|
||||
@@ -101,7 +101,12 @@ export class ServerService {
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||
|
||||
if (emoji == null) {
|
||||
return await reply.redirect('/static-assets/emoji-unknown.png');
|
||||
if ('fallback' in request.query) {
|
||||
return await reply.redirect('/static-assets/emoji-unknown.png');
|
||||
} else {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL('/proxy/emoji.webp', this.config.url);
|
||||
@@ -127,6 +132,8 @@ export class ServerService {
|
||||
relations: ['avatar'],
|
||||
});
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
|
||||
if (user) {
|
||||
reply.redirect(this.userEntityService.getAvatarUrlSync(user));
|
||||
} else {
|
||||
@@ -138,6 +145,7 @@ export class ServerService {
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(request.params.x, fs.createWriteStream(temp));
|
||||
reply.header('Content-Type', 'image/png');
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
return fs.createReadStream(temp).on('close', () => cleanup());
|
||||
});
|
||||
|
||||
|
@@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
|
||||
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
|
||||
import * as ep___i_apps from './endpoints/i/apps.js';
|
||||
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
|
||||
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
|
||||
import * as ep___i_changePassword from './endpoints/i/change-password.js';
|
||||
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
|
||||
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
@@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import { GetterService } from './GetterService.js';
|
||||
@@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e
|
||||
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
|
||||
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
|
||||
const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
|
||||
const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
|
||||
const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
|
||||
const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
|
||||
const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
|
||||
@@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
|
||||
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
|
||||
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
|
||||
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
|
||||
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
|
||||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
|
||||
|
||||
@@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_2fa_unregister,
|
||||
$i_apps,
|
||||
$i_authorizedApps,
|
||||
$i_claimAchievement,
|
||||
$i_changePassword,
|
||||
$i_deleteAccount,
|
||||
$i_exportBlocking,
|
||||
@@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$fetchRss,
|
||||
$retention,
|
||||
],
|
||||
@@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_2fa_unregister,
|
||||
$i_apps,
|
||||
$i_authorizedApps,
|
||||
$i_claimAchievement,
|
||||
$i_changePassword,
|
||||
$i_deleteAccount,
|
||||
$i_exportBlocking,
|
||||
@@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$fetchRss,
|
||||
$retention,
|
||||
],
|
||||
|
@@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
|
||||
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
|
||||
import * as ep___i_apps from './endpoints/i/apps.js';
|
||||
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
|
||||
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
|
||||
import * as ep___i_changePassword from './endpoints/i/change-password.js';
|
||||
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
|
||||
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
@@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
|
||||
@@ -506,6 +508,7 @@ const eps = [
|
||||
['i/2fa/unregister', ep___i_2fa_unregister],
|
||||
['i/apps', ep___i_apps],
|
||||
['i/authorized-apps', ep___i_authorizedApps],
|
||||
['i/claim-achievement', ep___i_claimAchievement],
|
||||
['i/change-password', ep___i_changePassword],
|
||||
['i/delete-account', ep___i_deleteAccount],
|
||||
['i/export-blocking', ep___i_exportBlocking],
|
||||
@@ -660,6 +663,7 @@ const eps = [
|
||||
['users/search', ep___users_search],
|
||||
['users/show', ep___users_show],
|
||||
['users/stats', ep___users_stats],
|
||||
['users/achievements', ep___users_achievements],
|
||||
['fetch-rss', ep___fetchRss],
|
||||
['retention', ep___retention],
|
||||
];
|
||||
|
@@ -28,8 +28,8 @@ export const meta = {
|
||||
|
||||
recursiveNesting: {
|
||||
message: 'It can not be structured like nesting folders recursively.',
|
||||
code: 'NO_SUCH_PARENT_FOLDER',
|
||||
id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1',
|
||||
code: 'RECURSIVE_NESTING',
|
||||
id: 'dbeb024837894013aed44279f9199740',
|
||||
},
|
||||
},
|
||||
|
||||
|
@@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
emojis: await this.emojiEntityService.packMany(emojis, {
|
||||
omitId: true,
|
||||
omitHost: true,
|
||||
withUrl: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -29,15 +29,36 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, user, token) => {
|
||||
const isSecure = token == null;
|
||||
|
||||
// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
|
||||
return await this.userEntityService.pack<true, true>(user.id, user, {
|
||||
const now = new Date();
|
||||
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
||||
|
||||
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
|
||||
const userProfile = await this.userProfilesRepository.findOneOrFail({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (!userProfile.loggedInDates.includes(today)) {
|
||||
this.userProfilesRepository.update({ userId: user.id }, {
|
||||
loggedInDates: [...userProfile.loggedInDates, today],
|
||||
});
|
||||
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
||||
}
|
||||
|
||||
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
|
||||
detail: true,
|
||||
includeSecrets: isSecure,
|
||||
userProfile,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { AchievementService } from '@/core/AchievementService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private achievementService: AchievementService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.achievementService.create(me.id, ps.name);
|
||||
});
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { AchievementService } from '@/core/AchievementService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'favorites'],
|
||||
@@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
private idService: IdService,
|
||||
private getterService: GetterService,
|
||||
private achievementService: AchievementService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Get favoritee
|
||||
@@ -76,6 +78,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (note.userHost == null) {
|
||||
this.achievementService.create(note.userId, 'myNoteFavorited1');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,31 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserProfilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
|
||||
|
||||
return profile.achievements;
|
||||
});
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import autwh from 'autwh';
|
||||
import * as autwh from 'autwh';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@@ -22,13 +22,13 @@
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||
};
|
||||
|
||||
const v = localStorage.getItem('v') || VERSION;
|
||||
let forceError = localStorage.getItem('forceError');
|
||||
if (forceError != null) {
|
||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
|
||||
}
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
const localeVersion = localStorage.getItem('localeVersion');
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== v);
|
||||
|
||||
if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
|
||||
if (!localStorage.hasOwnProperty('locale')) {
|
||||
const supportedLangs = LANGS;
|
||||
let lang = localStorage.getItem('lang');
|
||||
if (lang == null || !supportedLangs.includes(lang)) {
|
||||
@@ -42,13 +42,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
||||
if (res.status === 200) {
|
||||
const metaRes = await window.fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (metaRes.status !== 200) {
|
||||
renderError('META_FETCH');
|
||||
return;
|
||||
}
|
||||
const meta = await metaRes.json();
|
||||
const v = meta.version;
|
||||
if (v == null) {
|
||||
renderError('META_FETCH_V');
|
||||
return;
|
||||
}
|
||||
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
||||
if (localRes.status === 200) {
|
||||
localStorage.setItem('lang', lang);
|
||||
localStorage.setItem('locale', await res.text());
|
||||
localStorage.setItem('locale', await localRes.text());
|
||||
localStorage.setItem('localeVersion', v);
|
||||
} else {
|
||||
await checkUpdate();
|
||||
renderError('LOCALE_FETCH');
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +77,6 @@
|
||||
function importAppScript() {
|
||||
import(`/vite/${CLIENT_ENTRY}`)
|
||||
.catch(async e => {
|
||||
await checkUpdate();
|
||||
console.error(e);
|
||||
renderError('APP_IMPORT', e);
|
||||
});
|
||||
@@ -286,48 +303,4 @@
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function checkUpdate() {
|
||||
try {
|
||||
const res = await window.fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
body: '{}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const meta = await res.json();
|
||||
|
||||
if (meta.version == null) {
|
||||
throw new Error('failed to fetch instance metadata');
|
||||
}
|
||||
|
||||
if (meta.version != v) {
|
||||
localStorage.setItem('v', meta.version);
|
||||
refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
renderError('UPDATE_CHECK', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function refresh() {
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => registration.unregister());
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
})();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const;
|
||||
|
||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||
|
||||
|
Reference in New Issue
Block a user