summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-11-22 12:29:04 +0000
committerdakkar <dakkar@thenautilus.net>2024-11-22 12:29:04 +0000
commitbc816cb166d907d38eb096a02c5f4f57d9a2f1a0 (patch)
treea0e6b1dfeed8aa2b0543846eb65b8aae0ce631ad /packages/backend/src
parentbetter wording for moderator inactivity messages (diff)
parentRelease: 2024.11.0 (diff)
downloadsharkey-bc816cb166d907d38eb096a02c5f4f57d9a2f1a0.tar.gz
sharkey-bc816cb166d907d38eb096a02c5f4f57d9a2f1a0.tar.bz2
sharkey-bc816cb166d907d38eb096a02c5f4f57d9a2f1a0.zip
Merge tag '2024.11.0' into feature/2024.10
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts6
-rw-r--r--packages/backend/src/core/AnnouncementService.ts2
-rw-r--r--packages/backend/src/core/NoteCreateService.ts17
-rw-r--r--packages/backend/src/core/QueueService.ts18
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts33
-rw-r--r--packages/backend/src/core/UserWebhookService.ts14
-rw-r--r--packages/backend/src/core/UtilityService.ts1
-rw-r--r--packages/backend/src/core/WebAuthnService.ts4
-rw-r--r--packages/backend/src/core/WebhookTestService.ts68
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts1
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts3
-rw-r--r--packages/backend/src/core/activitypub/ApRequestService.ts8
-rw-r--r--packages/backend/src/core/activitypub/misc/contexts.ts3
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts15
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts15
-rw-r--r--packages/backend/src/core/activitypub/models/ApQuestionService.ts2
-rw-r--r--packages/backend/src/core/activitypub/type.ts3
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts95
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts3
-rw-r--r--packages/backend/src/misc/sql-like-escape.ts2
-rw-r--r--packages/backend/src/models/AbuseUserReport.ts4
-rw-r--r--packages/backend/src/models/User.ts17
-rw-r--r--packages/backend/src/models/json-schema/user.ts12
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts23
-rw-r--r--packages/backend/src/server/api/GetterService.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts57
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-account.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts6
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts65
-rw-r--r--packages/backend/src/server/web/views/announcement.pug21
-rw-r--r--packages/backend/src/server/web/views/base.pug6
37 files changed, 450 insertions, 129 deletions
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index 25e265f2b1..742e2621fd 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -154,9 +154,9 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
const convertedReports = abuseReports.map(it => {
return {
...it,
- reporter: usersMap.get(it.reporterId),
- targetUser: usersMap.get(it.targetUserId),
- assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
+ reporter: usersMap.get(it.reporterId) ?? null,
+ targetUser: usersMap.get(it.targetUserId) ?? null,
+ assignee: it.assigneeId ? (usersMap.get(it.assigneeId) ?? null) : null,
};
});
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index d4fcf19439..a9f6731977 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -72,7 +72,7 @@ export class AnnouncementService {
updatedAt: null,
title: values.title,
text: values.text,
- imageUrl: values.imageUrl,
+ imageUrl: values.imageUrl || null,
icon: values.icon,
display: values.display,
forExistingUsers: values.forExistingUsers,
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 13bafb7883..ae98c2fc79 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -58,6 +58,7 @@ import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { LatestNoteService } from '@/core/LatestNoteService.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
+import { CacheService } from '@/core/CacheService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -608,13 +609,21 @@ export class NoteCreateService implements OnApplicationShutdown {
this.followingsRepository.findBy({
followeeId: user.id,
notify: 'normal',
- }).then(followings => {
+ }).then(async followings => {
if (note.visibility !== 'specified') {
+ const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
for (const following of followings) {
// TODO: ワードミュート考慮
- this.notificationService.createNotification(following.followerId, 'note', {
- noteId: note.id,
- }, user.id);
+ let isRenoteMuted = false;
+ if (isPureRenote) {
+ const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId);
+ isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id);
+ }
+ if (!isRenoteMuted) {
+ this.notificationService.createNotification(following.followerId, 'note', {
+ noteId: note.id,
+ }, user.id);
+ }
}
}
});
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 341eb26c99..010ba9b7d6 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -7,13 +7,15 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
+import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js';
+import { type UserWebhookPayload } from './UserWebhookService.js';
import type {
DbJobData,
DeliverJobData,
@@ -30,8 +32,8 @@ import type {
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
- UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
+ UserWebhookDeliverQueue,
} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
@@ -527,10 +529,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService
*/
@bindThis
- public userWebhookDeliver(
+ public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook,
- type: typeof webhookEventTypes[number],
- content: unknown,
+ type: T,
+ content: UserWebhookPayload<T>,
opts?: { attempts?: number },
) {
const data: UserWebhookDeliverJobData = {
@@ -559,10 +561,10 @@ export class QueueService {
* @see SystemWebhookDeliverProcessorService
*/
@bindThis
- public systemWebhookDeliver(
+ public systemWebhookDeliver<T extends SystemWebhookEventType>(
webhook: MiSystemWebhook,
- type: SystemWebhookEventType,
- content: unknown,
+ type: T,
+ content: SystemWebhookPayload<T>,
opts?: { attempts?: number },
) {
const data: SystemWebhookDeliverJobData = {
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index db6407dcb3..de00169612 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -15,8 +15,39 @@ import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
+import { Packed } from '@/misc/json-schema.js';
+import { AbuseReportResolveType } from '@/models/AbuseUserReport.js';
+import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
+export type AbuseReportPayload = {
+ id: string;
+ targetUserId: string;
+ targetUser: Packed<'UserLite'> | null;
+ targetUserHost: string | null;
+ reporterId: string;
+ reporter: Packed<'UserLite'> | null;
+ reporterHost: string | null;
+ assigneeId: string | null;
+ assignee: Packed<'UserLite'> | null;
+ resolved: boolean;
+ forwarded: boolean;
+ comment: string;
+ moderationNote: string;
+ resolvedAs: AbuseReportResolveType | null;
+};
+
+export type InactiveModeratorsWarningPayload = {
+ remainingTime: ModeratorInactivityRemainingTime;
+};
+
+export type SystemWebhookPayload<T extends SystemWebhookEventType> =
+ T extends 'abuseReport' | 'abuseReportResolved' ? AbuseReportPayload :
+ T extends 'userCreated' ? Packed<'UserLite'> :
+ T extends 'inactiveModeratorsWarning' ? InactiveModeratorsWarningPayload :
+ T extends 'inactiveModeratorsInvitationOnlyChanged' ? Record<string, never> :
+ never;
+
@Injectable()
export class SystemWebhookService implements OnApplicationShutdown {
private logger: Logger;
@@ -168,7 +199,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
webhook: MiSystemWebhook | MiSystemWebhook['id'],
type: T,
- content: unknown,
+ content: SystemWebhookPayload<T>,
) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index 8a40a53688..7117a3d7fa 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js';
-import { MiWebhook } from '@/models/Webhook.js';
+import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
+import type { Packed } from '@/misc/json-schema.js';
+
+export type UserWebhookPayload<T extends WebhookEventTypes> =
+ T extends 'note' | 'reply' | 'renote' |'mention' ? {
+ note: Packed<'Note'>,
+ } :
+ T extends 'follow' | 'unfollow' ? {
+ user: Packed<'UserDetailedNotMe'>,
+ } :
+ T extends 'followed' ? {
+ user: Packed<'UserLite'>,
+ } : never;
@Injectable()
export class UserWebhookService implements OnApplicationShutdown {
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 4c6d539e16..906adcfd27 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -122,6 +122,7 @@ export class UtilityService {
return host;
}
+ @bindThis
public isFederationAllowedHost(host: string): boolean {
if (this.meta.federation === 'none') return false;
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index 75ab0a207c..ad53192f18 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -246,14 +246,12 @@ export class WebAuthnService {
@bindThis
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
- const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
+ const challenge = await this.redisClient.getdel(`webauthn:challenge:${userId}`);
if (!challenge) {
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found');
}
- await this.redisClient.del(`webauthn:challenge:${userId}`);
-
const key = await this.userSecurityKeysRepository.findOneBy({
id: response.id,
userId: userId,
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 0304cae355..746f289311 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -7,22 +7,16 @@ import { Injectable } from '@nestjs/common';
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
-import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
-import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
const oneDayMillis = 24 * 60 * 60 * 1000;
-type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
- targetUser: Packed<'UserLite'> | null,
- reporter: Packed<'UserLite'> | null,
- assignee: Packed<'UserLite'> | null,
-};
-
-function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
+function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
@@ -89,6 +83,9 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isExplorable: true,
isHibernated: false,
isDeleted: false,
+ requireSigninToViewContents: false,
+ makeNotesFollowersOnlyBefore: null,
+ makeNotesHiddenBefore: null,
emojis: [],
score: 0,
host: null,
@@ -322,10 +319,10 @@ export class WebhookTestService {
* - 送信対象イベント(on)に関する設定
*/
@bindThis
- public async testUserWebhook(
+ public async testUserWebhook<T extends WebhookEventTypes>(
params: {
webhookId: MiWebhook['id'],
- type: WebhookEventTypes,
+ type: T,
override?: Partial<Omit<MiWebhook, 'id'>>,
},
sender: MiUser | null,
@@ -337,7 +334,7 @@ export class WebhookTestService {
}
const webhook = webhooks[0];
- const send = (contents: unknown) => {
+ const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = {
...webhook,
...params.override,
@@ -345,7 +342,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
// また、Jobの試行回数も1回だけ.
- this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
};
const dummyNote1 = generateDummyNote({
@@ -377,33 +374,41 @@ export class WebhookTestService {
switch (params.type) {
case 'note': {
- send(toPackedNote(dummyNote1));
+ send('note', { note: toPackedNote(dummyNote1) });
break;
}
case 'reply': {
- send(toPackedNote(dummyReply1));
+ send('reply', { note: toPackedNote(dummyReply1) });
break;
}
case 'renote': {
- send(toPackedNote(dummyRenote1));
+ send('renote', { note: toPackedNote(dummyRenote1) });
break;
}
case 'mention': {
- send(toPackedNote(dummyMention1));
+ send('mention', { note: toPackedNote(dummyMention1) });
break;
}
case 'follow': {
- send(toPackedUserDetailedNotMe(dummyUser1));
+ send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break;
}
case 'followed': {
- send(toPackedUserLite(dummyUser2));
+ send('followed', { user: toPackedUserLite(dummyUser2) });
break;
}
case 'unfollow': {
- send(toPackedUserDetailedNotMe(dummyUser3));
+ send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break;
}
+ // まだ実装されていない (#9485)
+ case 'reaction':
+ return;
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _exhaustiveAssertion: never = params.type;
+ return;
+ }
}
}
@@ -416,10 +421,10 @@ export class WebhookTestService {
* - 送信対象イベント(on)に関する設定
*/
@bindThis
- public async testSystemWebhook(
+ public async testSystemWebhook<T extends SystemWebhookEventType>(
params: {
webhookId: MiSystemWebhook['id'],
- type: SystemWebhookEventType,
+ type: T,
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
},
) {
@@ -429,7 +434,7 @@ export class WebhookTestService {
}
const webhook = webhooks[0];
- const send = (contents: unknown) => {
+ const send = <U extends SystemWebhookEventType>(type: U, contents: SystemWebhookPayload<U>) => {
const merged = {
...webhook,
...params.override,
@@ -437,12 +442,12 @@ export class WebhookTestService {
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
// また、Jobの試行回数も1回だけ.
- this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ this.queueService.systemWebhookDeliver(merged, type, contents, { attempts: 1 });
};
switch (params.type) {
case 'abuseReport': {
- send(generateAbuseReport({
+ send('abuseReport', generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@@ -451,7 +456,7 @@ export class WebhookTestService {
break;
}
case 'abuseReportResolved': {
- send(generateAbuseReport({
+ send('abuseReportResolved', generateAbuseReport({
targetUserId: dummyUser1.id,
targetUser: dummyUser1,
reporterId: dummyUser2.id,
@@ -463,7 +468,7 @@ export class WebhookTestService {
break;
}
case 'userCreated': {
- send(toPackedUserLite(dummyUser1));
+ send('userCreated', toPackedUserLite(dummyUser1));
break;
}
case 'inactiveModeratorsWarning': {
@@ -473,15 +478,20 @@ export class WebhookTestService {
asHours: 24,
};
- send({
+ send('inactiveModeratorsWarning', {
remainingTime: dummyTime,
});
break;
}
case 'inactiveModeratorsInvitationOnlyChanged': {
- send({});
+ send('inactiveModeratorsInvitationOnlyChanged', {});
break;
}
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _exhaustiveAssertion: never = params.type;
+ return;
+ }
}
}
}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 64b64013af..d1ae13d706 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -135,6 +135,7 @@ export class ApInboxService {
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
+ // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
this.apPersonService.updatePerson(actor.uri);
});
}
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 42ee5bc58a..6242141947 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -517,6 +517,9 @@ export class ApRendererService {
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
_misskey_summary: profile.description,
_misskey_followedMessage: profile.followedMessage,
+ _misskey_requireSigninToViewContents: user.requireSigninToViewContents,
+ _misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
+ _misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
icon: avatar ? this.renderImage(avatar) : null,
image: banner ? this.renderImage(banner) : null,
backgroundUrl: background ? this.renderImage(background) : null,
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index eeff73385b..ca4aabbf49 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
@@ -19,6 +20,7 @@ import type { IObject } from './type.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import { UtilityService } from "@/core/UtilityService.js";
+import type { IObject } from './type.js';
type Request = {
url: string;
@@ -242,10 +244,8 @@ export class ApRequestService {
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
const href = alternate.getAttribute('href');
- if (href) {
- if (this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
- return await this.signedGet(href, user, false);
- }
+ if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
+ return await this.signedGet(href, user, false);
}
}
} catch (e) {
diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts
index da75fc1d42..45ca495145 100644
--- a/packages/backend/src/core/activitypub/misc/contexts.ts
+++ b/packages/backend/src/core/activitypub/misc/contexts.ts
@@ -558,6 +558,9 @@ const extension_context_definition = {
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary',
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
+ '_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
+ '_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
+ '_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
'isCat': 'misskey:isCat',
// Firefish
firefish: 'https://joinfirefish.org/ns#',
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index a0ddc2075b..7ca588aae2 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -108,6 +108,10 @@ export class ApNoteService {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
+ if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
+ return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
+ }
+
if (actor) {
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
if (attribution !== actor.uri) {
@@ -118,10 +122,6 @@ export class ApNoteService {
}
}
- if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
- return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
- }
-
if (note) {
const url = (object.url) ? getOneApId(object.url) : note.url;
if (url && url !== note.url) {
@@ -183,7 +183,7 @@ export class ApNoteService {
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
- throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id}`);
+ throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
}
}
@@ -421,9 +421,6 @@ export class ApNoteService {
const url = getOneApHrefNullable(note.url);
- if (url && !checkHttps(url)) {
- throw new Error('unexpected schema of note url: ' + url);
- }
if (url != null) {
if (!checkHttps(url)) {
@@ -431,7 +428,7 @@ export class ApNoteService {
}
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
- throw new Error(`note url <> id host mismatch: ${url} <> ${note.id}`);
+ throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 5cc1862167..1fa4a11d47 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -253,6 +253,12 @@ export class ApPersonService implements OnModuleInit {
if (user == null) throw new Error('failed to create user: user is null');
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
+ // icon and image may be arrays
+ // see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
+ if (Array.isArray(img)) {
+ img = img.find(item => item && item.url) ?? null;
+ }
+
// if we have an explicitly missing image, return an
// explicitly-null set of values
if ((img == null) || (typeof img === 'object' && img.url == null)) {
@@ -393,7 +399,7 @@ export class ApPersonService implements OnModuleInit {
usernameLower: person.preferredUsername?.toLowerCase(),
host,
inbox: person.inbox,
- sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
+ sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
notesCount: outboxcollection?.totalItems ?? 0,
followersCount: followerscollection?.totalItems ?? 0,
followingCount: followingcollection?.totalItems ?? 0,
@@ -404,6 +410,9 @@ export class ApPersonService implements OnModuleInit {
isBot,
isCat: (person as any).isCat === true,
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
+ requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
+ makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null,
+ makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null,
emojis,
})) as MiRemoteUser;
@@ -571,7 +580,7 @@ export class ApPersonService implements OnModuleInit {
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
- sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
+ sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured,
emojis: emojiNames,
@@ -647,7 +656,7 @@ export class ApPersonService implements OnModuleInit {
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await this.followingsRepository.update(
{ followerId: exist.id },
- { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
+ { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null },
);
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
index 83a98d17f9..990e186e88 100644
--- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts
+++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts
@@ -11,8 +11,8 @@ import type { IPoll } from '@/models/Poll.js';
import type { MiRemoteUser } from '@/models/User.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
-import { UtilityService } from '@/core/UtilityService.js';
import { getOneApId, isQuestion } from '../type.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js';
import type { Resolver } from '../ApResolverService.js';
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index af5aba9c16..c5c0bb8c94 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -16,6 +16,9 @@ export interface IObject {
summary?: string | null;
_misskey_summary?: string;
_misskey_followedMessage?: string | null;
+ _misskey_requireSigninToViewContents?: boolean;
+ _misskey_makeNotesFollowersOnlyBefore?: number | null;
+ _misskey_makeNotesHiddenBefore?: number | null;
published?: string;
cc?: ApObject;
to?: ApObject;
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index ab9c72cfbe..3ad4577706 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -109,57 +109,81 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
- private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
+ private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
+ // FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
+ if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
+ const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
+ if ((followersOnlyBefore != null)
+ && (
+ (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
+ || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
+ )
+ ) {
+ packedNote.visibility = 'followers';
+ }
+ }
+
+ if (meId === packedNote.userId) return;
+
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
- // visibility が specified かつ自分が指定されていなかったら非表示
- if (packedNote.visibility === 'specified') {
- if (meId == null) {
+ if (packedNote.user.requireSigninToViewContents && meId == null) {
+ hide = true;
+ }
+
+ if (!hide) {
+ const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
+ if ((hiddenBefore != null)
+ && (
+ (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
+ || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
+ )
+ ) {
hide = true;
- } else if (meId === packedNote.userId) {
- hide = false;
- } else {
- // 指定されているかどうか
- const specified = packedNote.visibleUserIds!.some(id => meId === id);
+ }
+ }
- if (specified) {
+ // visibility が specified かつ自分が指定されていなかったら非表示
+ if (!hide) {
+ if (packedNote.visibility === 'specified') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
hide = false;
} else {
- hide = true;
+ // 指定されているかどうか
+ const specified = packedNote.visibleUserIds!.some(id => meId === id);
+
+ if (!specified) {
+ hide = true;
+ }
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
- if (packedNote.visibility === 'followers') {
- if (meId == null) {
- hide = true;
- } else if (meId === packedNote.userId) {
- hide = false;
- } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
- // 自分の投稿に対するリプライ
- hide = false;
- } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
- // 自分へのメンション
- hide = false;
- } else if (packedNote.renote && (meId === packedNote.renote.userId)) {
- hide = false;
- } else {
- if (packedNote.renote) {
- const isFollowing = await this.followingsRepository.exists({
- where: {
- followeeId: packedNote.renote.userId,
- followerId: meId,
- },
- });
-
- hide = !isFollowing;
+ if (!hide) {
+ if (packedNote.visibility === 'followers') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId === packedNote.userId) {
+ hide = false;
+ } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+ // 自分の投稿に対するリプライ
+ hide = false;
+ } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
+ // 自分へのメンション
+ hide = false;
+ } else if (packedNote.renote && (meId === packedNote.renote.userId)) {
+ hide = false;
} else {
// フォロワーかどうか
+ // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
+ const appearNote = packedNote.renote ?? packedNote;
const isFollowing = await this.followingsRepository.exists({
where: {
- followeeId: packedNote.userId,
+ followeeId: appearNote.userId,
followerId: meId,
},
});
@@ -189,6 +213,7 @@ export class NoteEntityService implements OnModuleInit {
packedNote.reactionEmojis = undefined;
packedNote.reactions = undefined;
packedNote.isHidden = true;
+ // TODO: hiddenReason みたいなのを提供しても良さそう
}
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index a421e23195..4f6b412609 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -542,6 +542,9 @@ export class UserEntityService implements OnModuleInit {
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
speakAsCat: user.speakAsCat ?? false,
approved: user.approved,
+ requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
+ makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
+ makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts
index ffe61670ee..6b4f51b00e 100644
--- a/packages/backend/src/misc/sql-like-escape.ts
+++ b/packages/backend/src/misc/sql-like-escape.ts
@@ -4,5 +4,5 @@
*/
export function sqlLikeEscape(s: string) {
- return s.replace(/([%_\\])/g, '\\$1');
+ return s.replace(/([\\%_])/g, '\\$1');
}
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index cb5672e4ac..d43ebf9342 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -7,6 +7,8 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
+export type AbuseReportResolveType = 'accept' | 'reject';
+
@Entity('abuse_user_report')
export class MiAbuseUserReport {
@PrimaryColumn(id())
@@ -76,7 +78,7 @@ export class MiAbuseUserReport {
@Column('varchar', {
length: 128, nullable: true,
})
- public resolvedAs: 'accept' | 'reject' | null;
+ public resolvedAs: AbuseReportResolveType | null;
//#region Denormalized fields
@Index()
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index c7ecccf1cf..a4481a1f81 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -244,6 +244,23 @@ export class MiUser {
})
public isHibernated: boolean;
+ @Column('boolean', {
+ default: false,
+ })
+ public requireSigninToViewContents: boolean;
+
+ // in sec, マイナスで相対時間
+ @Column('integer', {
+ nullable: true,
+ })
+ public makeNotesFollowersOnlyBefore: number | null;
+
+ // in sec, マイナスで相対時間
+ @Column('integer', {
+ nullable: true,
+ })
+ public makeNotesHiddenBefore: number | null;
+
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
@Column('boolean', {
default: false,
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index a6517bfb98..46aa06c392 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -146,6 +146,18 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ requireSigninToViewContents: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
+ makeNotesFollowersOnlyBefore: {
+ type: 'number',
+ nullable: true, optional: true,
+ },
+ makeNotesHiddenBefore: {
+ type: 'number',
+ nullable: true, optional: true,
+ },
instance: {
type: 'object',
nullable: false, optional: true,
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index f955329fd1..efbc8721b3 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -33,6 +33,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
+import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import type Logger from '@/logger.js';
@@ -229,7 +230,7 @@ export class ActivityPubServerService {
let signature;
try {
- signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' });
+ signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
} catch (e) {
reply.code(401);
return;
@@ -619,7 +620,18 @@ export class ActivityPubServerService {
return;
}
+ // リモートだったらリダイレクト
+ if (user.host != null) {
+ if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
+ reply.code(500);
+ return;
+ }
+ reply.redirect(user.uri, 301);
+ return;
+ }
+
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
+
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
}
@@ -795,21 +807,22 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({
id: userId,
- host: IsNull(),
isSuspended: false,
});
return await this.userInfo(request, reply, user);
});
- fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
+ fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
vary(reply.raw, 'Accept');
+ const acct = Acct.parse(request.params.acct);
+
const user = await this.usersRepository.findOneBy({
- usernameLower: request.params.user.toLowerCase(),
- host: IsNull(),
+ usernameLower: acct.username,
+ host: acct.host ?? IsNull(),
isSuspended: false,
});
diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts
index 8643be0f30..419017aaf4 100644
--- a/packages/backend/src/server/api/GetterService.ts
+++ b/packages/backend/src/server/api/GetterService.ts
@@ -42,6 +42,17 @@ export class GetterService {
return note;
}
+ @bindThis
+ public async getNoteWithUser(noteId: MiNote['id']) {
+ const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
+
+ if (note == null) {
+ throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
+ }
+
+ return note;
+ }
+
/**
* Get note for API processing
*/
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
index 01dea703a3..ece1984cff 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot delete a root account');
}
- await this.deleteAccoountService.deleteAccount(user);
+ await this.deleteAccoountService.deleteAccount(user, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index 2dae1df87d..b8bfda73a4 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -55,7 +55,7 @@ export const paramDef = {
properties: {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
- imageUrl: { type: 'string', nullable: true, minLength: 1 },
+ imageUrl: { type: 'string', nullable: true, minLength: 0 },
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false },
@@ -76,7 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updatedAt: null,
title: ps.title,
text: ps.text,
- imageUrl: ps.imageUrl,
+ /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
+ imageUrl: ps.imageUrl || null,
icon: ps.icon,
display: ps.display,
forExistingUsers: ps.forExistingUsers,
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index fd21309818..87d80cbe80 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
@@ -13,6 +14,49 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ createdAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time',
+ },
+ updatedAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ description: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ url: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ roleIdsThatCanBeUsedThisDecoration: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ },
+ },
+ },
} as const;
export const paramDef = {
@@ -32,14 +76,25 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
+ private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
- await this.avatarDecorationService.create({
+ const created = await this.avatarDecorationService.create({
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
+
+ return {
+ id: created.id,
+ createdAt: this.idService.parse(created.id).date.toISOString(),
+ updatedAt: null,
+ name: created.name,
+ description: created.description,
+ url: created.url,
+ roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
+ };
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index aee90023e1..d785f085ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -4,10 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
-import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts
index b6f0f22d60..9065a71f6a 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts
@@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private deleteAccountService: DeleteAccountService,
) {
- super(meta, paramDef, async (ps) => {
+ super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
- await this.deleteAccountService.deleteAccount(user);
+ await this.deleteAccountService.deleteAccount(user, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 4232bc6e39..616a77e337 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -137,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (local != null) return local;
}
+ // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
return await this.mergePack(
me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index a504441df3..3148bae827 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -195,6 +195,9 @@ export const paramDef = {
noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' },
noindex: { type: 'boolean' },
+ requireSigninToViewContents: { type: 'boolean' },
+ makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true },
+ makeNotesHiddenBefore: { type: 'integer', nullable: true },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
speakAsCat: { type: 'boolean' },
@@ -353,6 +356,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
+ if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
+ if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore;
+ if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
@@ -495,6 +501,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
+ const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) {
let hasProhibitedWords = false;
@@ -524,6 +531,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]);
}
+ if (newFollowedMessage != null) {
+ const tokens = mfm.parse(newFollowedMessage);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
+ }
+
updates.emojis = emojis;
updates.tags = tags;
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index f82ba5473d..c189a708f2 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -8,7 +8,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import type { NotesRepository } from '@/models/_.js';
-import { QueryService } from '@/core/QueryService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -28,6 +27,12 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
+
+ signinRequired: {
+ message: 'Signin required.',
+ code: 'SIGNIN_REQUIRED',
+ id: '8e75455b-738c-471d-9f80-62693f33372e',
+ },
},
} as const;
@@ -44,25 +49,29 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = await this.notesRepository.createQueryBuilder('note')
- .where('note.id = :noteId', { noteId: ps.noteId });
+ .where('note.id = :noteId', { noteId: ps.noteId })
+ .innerJoinAndSelect('user');
this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateBlockedUserQuery(query, me);
}
-
+
const note = await query.getOne();
if (note === null) {
throw new ApiError(meta.errors.noSuchNote);
}
+ if (note.user!.requireSigninToViewContents && me == null) {
+ throw new ApiError(meta.errors.signinRequired);
+ }
+
return await this.noteEntityService.pack(note, me, {
detail: true,
});
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 263d062961..2d81e922dc 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -43,6 +43,12 @@ export const meta = {
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
},
+
+ signinRequired: {
+ message: 'Signin required.',
+ code: 'SIGNIN_REQUIRED',
+ id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
+ },
},
} as const;
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index c14f6c9123..a2cb50ee9f 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -30,6 +30,7 @@ import type {
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
+ RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
@@ -41,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
-import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type {
+ AnnouncementsRepository,
+ ChannelsRepository,
+ ClipsRepository,
+ FlashsRepository,
+ GalleryPostsRepository,
+ MiMeta,
+ NotesRepository,
+ PagesRepository,
+ ReversiGamesRepository,
+ UserProfilesRepository,
+ UsersRepository,
+} from '@/models/_.js';
import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
@@ -102,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository,
+ @Inject(DI.announcementsRepository)
+ private announcementsRepository: AnnouncementsRepository,
+
private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
@@ -111,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
+ private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
private roleService: RoleService,
@@ -121,6 +139,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
+ @Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@@ -251,6 +270,7 @@ export class ClientServerService {
this.deliverQueue,
this.inboxQueue,
this.dbQueue,
+ this.relationshipQueue,
this.objectStorageQueue,
this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue,
@@ -557,7 +577,7 @@ export class ClientServerService {
}
});
- //#region SSR (for crawlers)
+ //#region SSR
// User
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
const { username, host } = Acct.parse(request.params.user);
@@ -582,11 +602,17 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
+
+ const _user = await this.userEntityService.pack(user);
+
return await reply.view('user', {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
+ user: _user,
+ }),
});
} else {
// リモートユーザーなので
@@ -616,12 +642,15 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/notes/:note', async (request, reply) => {
vary(reply.raw, 'Accept');
- const note = await this.notesRepository.findOneBy({
- id: request.params.note,
- visibility: In(['public', 'home']),
+ const note = await this.notesRepository.findOne({
+ where: {
+ id: request.params.note,
+ visibility: In(['public', 'home']),
+ },
+ relations: ['user'],
});
- if (note) {
+ if (note && !note.user!.requireSigninToViewContents) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
reply.header('Cache-Control', 'public, max-age=15');
@@ -636,6 +665,9 @@ export class ClientServerService {
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
+ note: _note,
+ }),
});
} else {
return await renderBase(reply);
@@ -724,6 +756,9 @@ export class ClientServerService {
profile,
avatarUrl: _clip.user.avatarUrl,
...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
+ clip: _clip,
+ }),
});
} else {
return await renderBase(reply);
@@ -788,6 +823,24 @@ export class ClientServerService {
return await renderBase(reply);
}
});
+
+ // 個別お知らせページ
+ fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
+ const announcement = await this.announcementsRepository.findOneBy({
+ id: request.params.announcementId,
+ });
+
+ if (announcement) {
+ const _announcement = await this.announcementEntityService.pack(announcement);
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('announcement', {
+ announcement: _announcement,
+ ...await this.generateCommonPugData(this.meta),
+ });
+ } else {
+ return await renderBase(reply);
+ }
+ });
//#endregion
//#region noindex pages
diff --git a/packages/backend/src/server/web/views/announcement.pug b/packages/backend/src/server/web/views/announcement.pug
new file mode 100644
index 0000000000..7a4052e8a4
--- /dev/null
+++ b/packages/backend/src/server/web/views/announcement.pug
@@ -0,0 +1,21 @@
+extends ./base
+
+block vars
+ - const title = announcement.title;
+ - const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
+ - const url = `${config.url}/announcements/${announcement.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content=description)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= description)
+ meta(property='og:url' content= url)
+ if announcement.imageUrl
+ meta(property='og:image' content=announcement.imageUrl)
+ meta(property='twitter:card' content='summary_large_image')
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 84c4832802..f57dbbbf4e 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -2,6 +2,7 @@ block vars
block loadClientEntry
- const entry = config.frontendEntry;
+ - const baseUrl = config.url;
doctype html
@@ -36,7 +37,7 @@ html
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json')
- link(rel='search' type='application/opensearchdescription+xml' title=(title || "Sharkey") href=`${url}/opensearch.xml`)
+ link(rel='search' type='application/opensearchdescription+xml' title=(title || "Sharkey") href=`${baseUrl}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
@@ -79,6 +80,9 @@ html
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson
+ script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
+ != clientCtx
+
script
include ../boot.js