summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-01-21 18:45:50 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-01-21 18:45:50 +0900
commit38fde26d603951b389d23ade204f22b0674b2cde (patch)
tree6883103e35b7e836c6fe94558cbb26bce2e374ce /packages/backend/src
parentMerge branch 'develop' (diff)
parent13.1.0 (diff)
downloadmisskey-38fde26d603951b389d23ade204f22b0674b2cde.tar.gz
misskey-38fde26d603951b389d23ade204f22b0674b2cde.tar.bz2
misskey-38fde26d603951b389d23ade204f22b0674b2cde.zip
Merge branch 'develop'
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/MainModule.ts (renamed from packages/backend/src/RootModule.ts)6
-rw-r--r--packages/backend/src/boot/master.ts25
-rw-r--r--packages/backend/src/boot/worker.ts19
-rw-r--r--packages/backend/src/core/AchievementService.ts118
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/HttpRequestService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts6
-rw-r--r--packages/backend/src/core/chart/ChartManagementService.ts2
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts3
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts7
-rw-r--r--packages/backend/src/models/entities/Flash.ts2
-rw-r--r--packages/backend/src/models/entities/Notification.ts6
-rw-r--r--packages/backend/src/models/entities/UserProfile.ts13
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts16
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/users/achievements.ts31
-rw-r--r--packages/backend/src/server/api/integration/TwitterServerService.ts2
-rw-r--r--packages/backend/src/server/web/boot.js5
-rw-r--r--packages/backend/src/types.ts2
24 files changed, 300 insertions, 44 deletions
diff --git a/packages/backend/src/RootModule.ts b/packages/backend/src/MainModule.ts
index 3fc3927768..fc568e883e 100644
--- a/packages/backend/src/RootModule.ts
+++ b/packages/backend/src/MainModule.ts
@@ -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 {}
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index 4630217c4c..93cb3131ba 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -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 {
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
index f29e37de78..e0574643b7 100644
--- a/packages/backend/src/boot/worker.ts
+++ b/packages/backend/src/boot/worker.ts
@@ -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
diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts
new file mode 100644
index 0000000000..26dd356d36
--- /dev/null
+++ b/packages/backend/src/core/AchievementService.ts
@@ -0,0 +1,118 @@
+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',
+ 'profileFilled',
+ 'markedAsCat',
+ 'following1',
+ 'following10',
+ 'following50',
+ 'following100',
+ 'following300',
+ 'followers1',
+ 'followers10',
+ 'followers50',
+ 'followers100',
+ 'followers300',
+ 'followers500',
+ 'followers1000',
+ 'collectAchievements30',
+ 'viewAchievements3min',
+ 'iLoveMisskey',
+ 'client30min',
+ 'noteDeletedWithin1min',
+ 'postedAtLateNight',
+ 'postedAt0min0sec',
+ 'selfQuote',
+ 'htl20npm',
+ '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: string,
+ ): 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,
+ });
+ }
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 0ae1ee32b2..eddf407940 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -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,
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts
index 8639b5713d..2864ad4405 100644
--- a/packages/backend/src/core/HttpRequestService.ts
+++ b/packages/backend/src/core/HttpRequestService.ts
@@ -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) {
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index d44d06a442..ab22a0c411 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -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,
diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts
index 37de30b71c..4fba1b57d0 100644
--- a/packages/backend/src/core/chart/ChartManagementService.ts
+++ b/packages/backend/src/core/chart/ChartManagementService.ts
@@ -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) {
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index a1c2c9cffb..a8210eea02 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -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,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index bf6f6f4553..34b523e143 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -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 ? {
diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/entities/Flash.ts
index d9a6ac987c..07039d4fa1 100644
--- a/packages/backend/src/models/entities/Flash.ts
+++ b/packages/backend/src/models/entities/Flash.ts
@@ -44,7 +44,7 @@ export class Flash {
public user: User | null;
@Column('varchar', {
- length: 16384,
+ length: 32768,
})
public script: string;
diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts
index 6679cdb809..66f131d1c0 100644
--- a/packages/backend/src/models/entities/Notification.ts
+++ b/packages/backend/src/models/entities/Notification.ts
@@ -64,6 +64,7 @@ export class Notification {
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* groupInvited - グループに招待された
+ * achievementEarned - 実績を獲得
* app - アプリ通知
*/
@Index()
@@ -129,6 +130,11 @@ export class Notification {
})
public choice: number | null;
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public achievement: string | null;
+
/**
* アプリ通知のbody
*/
diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/entities/UserProfile.ts
index c561da87ce..86df8d5d98 100644
--- a/packages/backend/src/models/entities/UserProfile.ts
+++ b/packages/backend/src/models/entities/UserProfile.ts
@@ -213,6 +213,19 @@ export class UserProfile {
})
public mutingNotificationTypes: typeof notificationTypes[number][];
+ @Column('varchar', {
+ length: 32, array: true, default: '{}',
+ })
+ public loggedInDates: string[];
+
+ @Column('jsonb', {
+ default: [],
+ })
+ public achievements: {
+ name: string;
+ unlockedAt: number;
+ }[];
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 034e9cc5a5..6a8f35cdda 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
+import { GlobalModule } from '@/GlobalModule.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DbQueueProcessorsService } from './DbQueueProcessorsService.js';
@@ -34,6 +35,7 @@ import { ExportFavoritesProcessorService } from './processors/ExportFavoritesPro
@Module({
imports: [
+ GlobalModule,
CoreModule,
],
providers: [
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index fac8497b5e..eb6a3795eb 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -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());
});
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 14927da7d6..466651f379 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
+import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
+import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
@@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
+const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
@@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
+const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
+ $i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
+ $users_achievements,
$fetchRss,
$retention,
],
@@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_unregister,
$i_apps,
$i_authorizedApps,
+ $i_claimAchievement,
$i_changePassword,
$i_deleteAccount,
$i_exportBlocking,
@@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
+ $users_achievements,
$fetchRss,
$retention,
],
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 54c4206ea4..3678fe14e8 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
+import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
@@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
+import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
@@ -506,6 +508,7 @@ const eps = [
['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps],
['i/authorized-apps', ep___i_authorizedApps],
+ ['i/claim-achievement', ep___i_claimAchievement],
['i/change-password', ep___i_changePassword],
['i/delete-account', ep___i_deleteAccount],
['i/export-blocking', ep___i_exportBlocking],
@@ -660,6 +663,7 @@ const eps = [
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/stats', ep___users_stats],
+ ['users/achievements', ep___users_achievements],
['fetch-rss', ep___fetchRss],
['retention', ep___retention],
];
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index ee63d291b2..ff0a78b929 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -28,8 +28,8 @@ export const meta = {
recursiveNesting: {
message: 'It can not be structured like nesting folders recursively.',
- code: 'NO_SUCH_PARENT_FOLDER',
- id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1',
+ code: 'RECURSIVE_NESTING',
+ id: 'dbeb024837894013aed44279f9199740',
},
},
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index 3bcd6ff8fb..6beef5ab85 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/index.js';
+import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -29,15 +29,36 @@ export default class extends Endpoint<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,
});
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
new file mode 100644
index 0000000000..52ae5475b6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -0,0 +1,28 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { AchievementService } from '@/core/AchievementService.js';
+
+export const meta = {
+ requireCredential: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ },
+ required: ['name'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ private achievementService: AchievementService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.achievementService.create(me.id, ps.name);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts
new file mode 100644
index 0000000000..2a095d83ea
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/achievements.ts
@@ -0,0 +1,31 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserProfilesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ requireCredential: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<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;
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts
index 9cfadbfa1a..f31a788d31 100644
--- a/packages/backend/src/server/api/integration/TwitterServerService.ts
+++ b/packages/backend/src/server/api/integration/TwitterServerService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
-import autwh from 'autwh';
+import * as autwh from 'autwh';
import type { Config } from '@/config.js';
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index e2fc27fecd..a4513696a1 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -24,6 +24,11 @@
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);
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 573e2faf87..7e9e193362 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -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;