Merge remote-tracking branch 'upstream/develop' into merge-upstream
This commit is contained in:
		
							
								
								
									
										11
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -12,6 +12,17 @@ | ||||
|  | ||||
| --> | ||||
|  | ||||
| ## 13.12.2 | ||||
|  | ||||
| ### General | ||||
| - 投稿したコンテンツのAIによる学習を軽減するオプションを追加 | ||||
|  | ||||
| ### Client | ||||
| - Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 | ||||
|  | ||||
| ### Server | ||||
| - センシティブワードの登録にAnd、正規表現が使用できるようになりました。 | ||||
|  | ||||
| ## 13.12.1 | ||||
|  | ||||
| ### Client | ||||
|   | ||||
| @@ -1038,8 +1038,11 @@ thisChannelArchived: "Dieser Kanal wurde archiviert." | ||||
| displayOfNote: "Anzeige von Notizen" | ||||
| initialAccountSetting: "Kontoeinrichtung" | ||||
| youFollowing: "Gefolgt" | ||||
| preventAiLarning: "Verwendung in machinellem Lernen (AI/KI) ablehnen" | ||||
| preventAiLarningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (AI/KI) zu verwenden. Dies wird durch das Hinzufügen eines \"noai\"-HTML-Tags an den jeweiligen Inhalt erreicht. Da dieser Tag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich." | ||||
| _initialAccountSetting: | ||||
|   accountCreated: "Dein Konto wurde erfolgreich erstellt!" | ||||
|   letsStartAccountSetup: "Lass uns nun dein Konto einrichten." | ||||
|   letsFillYourProfile: "Lass uns zuerst dein Profil einrichten." | ||||
|   profileSetting: "Profileinstellungen" | ||||
|   theseSettingsCanEditLater: "Diese Einstellungen kannst du jederzeit ändern." | ||||
|   | ||||
| @@ -1036,20 +1036,23 @@ channelArchiveConfirmTitle: "Really archive {name}?" | ||||
| channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore." | ||||
| thisChannelArchived: "This channel has been archived." | ||||
| displayOfNote: "Note display" | ||||
| initialAccountSetting: "Profile configuration" | ||||
| initialAccountSetting: "Profile setup" | ||||
| youFollowing: "Followed" | ||||
| preventAiLarning: "Reject usage in Machine Learning (AI)" | ||||
| preventAiLarningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (AI) data sets. This is achieved by adding a \"noai\" HTML-Tag to the respective content. A complete prevention can however not be achieved through this tag, as it may simply be ignored." | ||||
| _initialAccountSetting: | ||||
|   accountCreated: "Your account was successfully created!" | ||||
|   letsStartAccountSetup: "For starters, let's set up your profile." | ||||
|   letsFillYourProfile: "First, let's set up your profile." | ||||
|   profileSetting: "Profile settings" | ||||
|   theseSettingsCanEditLater: "You can always change these settings later." | ||||
|   youCanEditMoreSettingsInSettingsPageLater: "There are many more settings you can configure from the \"Settings\" page. Be sure to visit it later." | ||||
|   followUsers: "Try following some users that interest you to build up your timeline." | ||||
|   pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." | ||||
|   initialAccountSettingCompleted: "Profile configuration complete!" | ||||
|   initialAccountSettingCompleted: "Profile setup complete!" | ||||
|   haveFun: "Enjoy {name}!" | ||||
|   ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." | ||||
|   skipAreYouSure: "Really skip profile configuration?" | ||||
|   skipAreYouSure: "Really skip profile setup?" | ||||
| _serverRules: | ||||
|   description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." | ||||
| _accountMigration: | ||||
|   | ||||
| @@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール" | ||||
| resetPasswordConfirm: "パスワードリセットしますか?" | ||||
| sensitiveWords: "センシティブワード" | ||||
| sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" | ||||
| sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" | ||||
| notesSearchNotAvailable: "ノート検索は利用できません。" | ||||
| license: "ライセンス" | ||||
| unfavoriteConfirm: "お気に入り解除しますか?" | ||||
| @@ -1038,6 +1039,8 @@ thisChannelArchived: "このチャンネルはアーカイブされています | ||||
| displayOfNote: "ノートの表示" | ||||
| initialAccountSetting: "初期設定" | ||||
| youFollowing: "フォロー中" | ||||
| preventAiLarning: "AIによる学習を拒否" | ||||
| preventAiLarningDescription: "投稿したノートや画像などのコンテンツを学習の対象にしないようAIに要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されます。この機能は実験的であり、AIによる学習を完全に防止するものではありません。" | ||||
|  | ||||
| _initialAccountSetting: | ||||
|   accountCreated: "アカウントの作成が完了しました!" | ||||
|   | ||||
| @@ -1240,6 +1240,7 @@ _achievements: | ||||
|       title: "잠깐 쉬어" | ||||
|       description: "클라이언트를 시작하고 30분이 경과하였습니다" | ||||
|     _client60min: | ||||
|       title: "No \"Miss\" in Misskey" | ||||
|       description: "클라이언트를 시작하고 60분이 경과하였습니다" | ||||
|     _noteDeletedWithin1min: | ||||
|       title: "있었는데요 없었습니다" | ||||
|   | ||||
| @@ -1036,7 +1036,21 @@ channelArchiveConfirmTitle: "要封存{name}嗎?" | ||||
| channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。" | ||||
| thisChannelArchived: "這個頻道已被封存。" | ||||
| displayOfNote: "顯示貼文" | ||||
| youFollowing: "關注中" | ||||
| initialAccountSetting: "初始設定" | ||||
| youFollowing: "追隨中" | ||||
| _initialAccountSetting: | ||||
|   accountCreated: "帳戶已建立完成!" | ||||
|   letsStartAccountSetup: "來進行帳戶的初始設定吧。" | ||||
|   letsFillYourProfile: "首先,來設定您的個人檔案吧。" | ||||
|   profileSetting: "個人檔案設定" | ||||
|   theseSettingsCanEditLater: "這裡的設定可以在之後變更。" | ||||
|   youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。" | ||||
|   followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。" | ||||
|   pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" | ||||
|   initialAccountSettingCompleted: "初始設定完成了!" | ||||
|   haveFun: "盡情享受{name}吧!" | ||||
|   ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。" | ||||
|   skipAreYouSure: "要略過初始設定嗎?" | ||||
| _serverRules: | ||||
|   description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" | ||||
| _accountMigration: | ||||
| @@ -1464,7 +1478,7 @@ _channel: | ||||
|   removeBanner: "移除橫幅圖像" | ||||
|   featured: "熱門貼文" | ||||
|   owned: "管理中" | ||||
|   following: "關注中" | ||||
|   following: "追隨中" | ||||
|   usersCount: "有{n}人參與" | ||||
|   notesCount: "有{n}個貼文" | ||||
|   nameAndDescription: "名稱與說明" | ||||
| @@ -1586,6 +1600,16 @@ _time: | ||||
|   minute: "分鐘" | ||||
|   hour: "小時" | ||||
|   day: "日" | ||||
| _timelineTutorial: | ||||
|   title: "Misskey的使用方法" | ||||
|   step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。" | ||||
|   step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。" | ||||
|   step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。" | ||||
|   step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。" | ||||
|   step3_1: "貼文發出去了嗎?" | ||||
|   step3_2: "如果你的貼文出現在時間軸上,就代表發文成功。" | ||||
|   step4_1: "可以對貼文標記「反應」。" | ||||
|   step4_2: "點擊貼文的「+」圖示,即可選擇喜好的表情符號來標記反應。" | ||||
| _2fa: | ||||
|   alreadyRegistered: "此設備已經被註冊過了" | ||||
|   registerTOTP: "開始設定驗證應用程式" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "13.12.1", | ||||
| 	"version": "13.12.2", | ||||
| 	"codename": "nasubi", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|   | ||||
| @@ -0,0 +1,11 @@ | ||||
| export class PreventAiLarning1683682889948 { | ||||
|     name = 'PreventAiLarning1683682889948' | ||||
|  | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user_profile" ADD "preventAiLarning" boolean NOT NULL DEFAULT true`); | ||||
|     } | ||||
|  | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "preventAiLarning"`); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| export class PublicReactionsDefaultTrue1683683083083 { | ||||
|     name = 'PublicReactionsDefaultTrue1683683083083' | ||||
|  | ||||
|     async up(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "publicReactions" SET DEFAULT true`); | ||||
|     } | ||||
|  | ||||
|     async down(queryRunner) { | ||||
|         await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "publicReactions" SET DEFAULT false`); | ||||
|     } | ||||
| } | ||||
| @@ -18,10 +18,12 @@ export async function server() { | ||||
| 	const serverService = app.get(ServerService); | ||||
| 	await serverService.launch(); | ||||
|  | ||||
| 	if (process.env.NODE_ENV !== 'test') { | ||||
| 		app.get(ChartManagementService).start(); | ||||
| 		app.get(JanitorService).start(); | ||||
| 		app.get(QueueStatsService).start(); | ||||
| 		app.get(ServerStatsService).start(); | ||||
| 	} | ||||
|  | ||||
| 	return app; | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import * as mfm from 'mfm-js'; | ||||
| import { In, DataSource } from 'typeorm'; | ||||
| import * as Redis from 'ioredis'; | ||||
| import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; | ||||
| import RE2 from 're2'; | ||||
| import { extractMentions } from '@/misc/extract-mentions.js'; | ||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; | ||||
| import { extractHashtags } from '@/misc/extract-hashtags.js'; | ||||
| @@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||
| 		if (data.channel != null) data.localOnly = true; | ||||
|  | ||||
| 		if (data.visibility === 'public' && data.channel == null) { | ||||
| 			if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) { | ||||
| 			const sensitiveWords = (await this.metaService.fetch()).sensitiveWords; | ||||
| 			if (this.isSensitive(data, sensitiveWords)) { | ||||
| 				data.visibility = 'home'; | ||||
| 			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { | ||||
| 				data.visibility = 'home'; | ||||
| @@ -671,6 +673,31 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||
| 		this.index(note); | ||||
| 	} | ||||
| 	 | ||||
| 	@bindThis | ||||
| 	private isSensitive(note: Option, sensitiveWord: string[]): boolean { | ||||
| 		if (sensitiveWord.length > 0) { | ||||
| 			const text = note.cw ?? note.text ?? ''; | ||||
| 			if (text === '') return false; | ||||
| 			const matched = sensitiveWord.some(filter => { | ||||
| 				// represents RegExp | ||||
| 				const regexp = filter.match(/^\/(.+)\/(.*)$/); | ||||
| 				// This should never happen due to input sanitisation. | ||||
| 				if (!regexp) { | ||||
| 					const words = filter.split(' '); | ||||
| 					return words.every(keyword => text.includes(keyword)); | ||||
| 				} | ||||
| 				try { | ||||
| 					return new RE2(regexp[1], regexp[2]).test(text); | ||||
| 				} catch (err) { | ||||
| 					// This should never happen due to input sanitisation. | ||||
| 					return false; | ||||
| 				} | ||||
| 			}); | ||||
| 			if (matched) return true; | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	private incRenoteCount(renote: Note) { | ||||
| 		this.notesRepository.createQueryBuilder().update() | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { setTimeout } from 'node:timers/promises'; | ||||
| import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; | ||||
| import Bull from 'bull'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| @@ -41,9 +42,9 @@ export type SystemQueue = Bull.Queue<Record<string, unknown>>; | ||||
| export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; | ||||
| export type DeliverQueue = Bull.Queue<DeliverJobData>; | ||||
| export type InboxQueue = Bull.Queue<InboxJobData>; | ||||
| export type DbQueue = Bull.Queue<DbJobData<keyof DbJobMap>>; | ||||
| export type DbQueue = Bull.Queue; | ||||
| export type RelationshipQueue = Bull.Queue<RelationshipJobData>; | ||||
| export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>; | ||||
| export type ObjectStorageQueue = Bull.Queue; | ||||
| export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>; | ||||
|  | ||||
| const $system: Provider = { | ||||
| @@ -118,4 +119,36 @@ const $webhookDeliver: Provider = { | ||||
| 		$webhookDeliver, | ||||
| 	], | ||||
| }) | ||||
| export class QueueModule {} | ||||
| export class QueueModule implements OnApplicationShutdown { | ||||
| 	constructor( | ||||
| 		@Inject('queue:system') public systemQueue: SystemQueue, | ||||
| 		@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, | ||||
| 		@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:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, | ||||
| 	) {} | ||||
|  | ||||
| 	async onApplicationShutdown(signal: string): Promise<void> { | ||||
| 		if (process.env.NODE_ENV === 'test') { | ||||
| 			// XXX: | ||||
| 			// Shutting down the existing connections causes errors on Jest as | ||||
| 			// Misskey has asynchronous postgres/redis connections that are not | ||||
| 			// awaited. | ||||
| 			// Let's wait for some random time for them to finish. | ||||
| 			await setTimeout(5000); | ||||
| 		} | ||||
| 		await Promise.all([ | ||||
| 			this.systemQueue.close(), | ||||
| 			this.endedPollNotificationQueue.close(), | ||||
| 			this.deliverQueue.close(), | ||||
| 			this.inboxQueue.close(), | ||||
| 			this.dbQueue.close(), | ||||
| 			this.relationshipQueue.close(), | ||||
| 			this.objectStorageQueue.close(), | ||||
| 			this.webhookDeliverQueue.close(), | ||||
| 		]); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -445,6 +445,7 @@ export class UserEntityService implements OnModuleInit { | ||||
| 				carefulBot: profile!.carefulBot, | ||||
| 				autoAcceptFollowed: profile!.autoAcceptFollowed, | ||||
| 				noCrawle: profile!.noCrawle, | ||||
| 				preventAiLarning: profile!.preventAiLarning, | ||||
| 				isExplorable: user.isExplorable, | ||||
| 				isDeleted: user.isDeleted, | ||||
| 				hideOnlineStatus: user.hideOnlineStatus, | ||||
|   | ||||
| @@ -76,7 +76,7 @@ export class UserProfile { | ||||
| 	public emailNotificationTypes: string[]; | ||||
|  | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 		default: true, | ||||
| 	}) | ||||
| 	public publicReactions: boolean; | ||||
|  | ||||
| @@ -147,6 +147,11 @@ export class UserProfile { | ||||
| 	}) | ||||
| 	public noCrawle: boolean; | ||||
|  | ||||
| 	@Column('boolean', { | ||||
| 		default: true, | ||||
| 	}) | ||||
| 	public preventAiLarning: boolean; | ||||
|  | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 	}) | ||||
|   | ||||
| @@ -302,7 +302,11 @@ export const packedMeDetailedOnlySchema = { | ||||
| 		}, | ||||
| 		noCrawle: { | ||||
| 			type: 'boolean', | ||||
| 			nullable: true, optional: false, | ||||
| 			nullable: false, optional: false, | ||||
| 		}, | ||||
| 		preventAiLarning: { | ||||
| 			type: 'boolean', | ||||
| 			nullable: false, optional: false, | ||||
| 		}, | ||||
| 		isExplorable: { | ||||
| 			type: 'boolean', | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; | ||||
| import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; | ||||
| import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; | ||||
| import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; | ||||
| import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; | ||||
| import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; | ||||
| import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; | ||||
| import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; | ||||
| import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; | ||||
| import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; | ||||
| import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; | ||||
| import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; | ||||
| import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; | ||||
| import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; | ||||
| import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; | ||||
| import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; | ||||
| import type Bull from 'bull'; | ||||
|  | ||||
| @Injectable() | ||||
| export class DbQueueProcessorsService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
|  | ||||
| 		private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, | ||||
| 		private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, | ||||
| 		private exportNotesProcessorService: ExportNotesProcessorService, | ||||
| 		private exportFavoritesProcessorService: ExportFavoritesProcessorService, | ||||
| 		private exportFollowingProcessorService: ExportFollowingProcessorService, | ||||
| 		private exportMutingProcessorService: ExportMutingProcessorService, | ||||
| 		private exportBlockingProcessorService: ExportBlockingProcessorService, | ||||
| 		private exportUserListsProcessorService: ExportUserListsProcessorService, | ||||
| 		private exportAntennasProcessorService: ExportAntennasProcessorService, | ||||
| 		private importFollowingProcessorService: ImportFollowingProcessorService, | ||||
| 		private importMutingProcessorService: ImportMutingProcessorService, | ||||
| 		private importBlockingProcessorService: ImportBlockingProcessorService, | ||||
| 		private importUserListsProcessorService: ImportUserListsProcessorService, | ||||
| 		private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, | ||||
| 		private importAntennasProcessorService: ImportAntennasProcessorService, | ||||
| 		private deleteAccountProcessorService: DeleteAccountProcessorService, | ||||
| 	) { | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public start(q: Bull.Queue): void { | ||||
| 		q.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done)); | ||||
| 		q.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done)); | ||||
| 		q.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done)); | ||||
| 		q.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done)); | ||||
| 		q.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done)); | ||||
| 		q.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done)); | ||||
| 		q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); | ||||
| 		q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); | ||||
| 		q.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done)); | ||||
| 		q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); | ||||
| 		q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); | ||||
| 		q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); | ||||
| 		q.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done)); | ||||
| 		q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); | ||||
| 		q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); | ||||
| 		q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); | ||||
| 		q.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done)); | ||||
| 		q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); | ||||
| 	} | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; | ||||
| import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; | ||||
| import type Bull from 'bull'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
|  | ||||
| @Injectable() | ||||
| export class ObjectStorageQueueProcessorsService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
|  | ||||
| 		private deleteFileProcessorService: DeleteFileProcessorService, | ||||
| 		private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, | ||||
| 	) { | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public start(q: Bull.Queue): void { | ||||
| 		q.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job)); | ||||
| 		q.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done)); | ||||
| 	} | ||||
| } | ||||
| @@ -3,14 +3,10 @@ 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'; | ||||
| import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js'; | ||||
| import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js'; | ||||
| import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; | ||||
| import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; | ||||
| import { InboxProcessorService } from './processors/InboxProcessorService.js'; | ||||
| import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; | ||||
| import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js'; | ||||
| import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; | ||||
| import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; | ||||
| import { CleanProcessorService } from './processors/CleanProcessorService.js'; | ||||
| @@ -68,10 +64,6 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor | ||||
| 		DeleteFileProcessorService, | ||||
| 		CleanRemoteFilesProcessorService, | ||||
| 		RelationshipProcessorService, | ||||
| 		SystemQueueProcessorsService, | ||||
| 		ObjectStorageQueueProcessorsService, | ||||
| 		DbQueueProcessorsService, | ||||
| 		RelationshipQueueProcessorsService, | ||||
| 		WebhookDeliverProcessorService, | ||||
| 		EndedPollNotificationProcessorService, | ||||
| 		DeliverProcessorService, | ||||
|   | ||||
| @@ -5,15 +5,36 @@ import type Logger from '@/logger.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { getJobInfo } from './get-job-info.js'; | ||||
| import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js'; | ||||
| import { ObjectStorageQueueProcessorsService } from './ObjectStorageQueueProcessorsService.js'; | ||||
| import { DbQueueProcessorsService } from './DbQueueProcessorsService.js'; | ||||
| import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; | ||||
| import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; | ||||
| import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; | ||||
| import { InboxProcessorService } from './processors/InboxProcessorService.js'; | ||||
| import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; | ||||
| import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js'; | ||||
| import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; | ||||
| import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js'; | ||||
| import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; | ||||
| import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; | ||||
| import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; | ||||
| import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; | ||||
| import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; | ||||
| import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; | ||||
| import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; | ||||
| import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; | ||||
| import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; | ||||
| import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; | ||||
| import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; | ||||
| import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; | ||||
| import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; | ||||
| import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; | ||||
| import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; | ||||
| import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; | ||||
| import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; | ||||
| import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; | ||||
| import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; | ||||
| import { CleanProcessorService } from './processors/CleanProcessorService.js'; | ||||
| import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; | ||||
| import { QueueLoggerService } from './QueueLoggerService.js'; | ||||
| import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js'; | ||||
|  | ||||
| @Injectable() | ||||
| export class QueueProcessorService { | ||||
| @@ -25,14 +46,35 @@ export class QueueProcessorService { | ||||
|  | ||||
| 		private queueLoggerService: QueueLoggerService, | ||||
| 		private queueService: QueueService, | ||||
| 		private systemQueueProcessorsService: SystemQueueProcessorsService, | ||||
| 		private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService, | ||||
| 		private dbQueueProcessorsService: DbQueueProcessorsService, | ||||
| 		private relationshipQueueProcessorsService: RelationshipQueueProcessorsService, | ||||
| 		private webhookDeliverProcessorService: WebhookDeliverProcessorService, | ||||
| 		private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, | ||||
| 		private deliverProcessorService: DeliverProcessorService, | ||||
| 		private inboxProcessorService: InboxProcessorService, | ||||
| 		private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, | ||||
| 		private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService, | ||||
| 		private exportNotesProcessorService: ExportNotesProcessorService, | ||||
| 		private exportFavoritesProcessorService: ExportFavoritesProcessorService, | ||||
| 		private exportFollowingProcessorService: ExportFollowingProcessorService, | ||||
| 		private exportMutingProcessorService: ExportMutingProcessorService, | ||||
| 		private exportBlockingProcessorService: ExportBlockingProcessorService, | ||||
| 		private exportUserListsProcessorService: ExportUserListsProcessorService, | ||||
| 		private exportAntennasProcessorService: ExportAntennasProcessorService, | ||||
| 		private importFollowingProcessorService: ImportFollowingProcessorService, | ||||
| 		private importMutingProcessorService: ImportMutingProcessorService, | ||||
| 		private importBlockingProcessorService: ImportBlockingProcessorService, | ||||
| 		private importUserListsProcessorService: ImportUserListsProcessorService, | ||||
| 		private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, | ||||
| 		private importAntennasProcessorService: ImportAntennasProcessorService, | ||||
| 		private deleteAccountProcessorService: DeleteAccountProcessorService, | ||||
| 		private deleteFileProcessorService: DeleteFileProcessorService, | ||||
| 		private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, | ||||
| 		private relationshipProcessorService: RelationshipProcessorService, | ||||
| 		private tickChartsProcessorService: TickChartsProcessorService, | ||||
| 		private resyncChartsProcessorService: ResyncChartsProcessorService, | ||||
| 		private cleanChartsProcessorService: CleanChartsProcessorService, | ||||
| 		private aggregateRetentionProcessorService: AggregateRetentionProcessorService, | ||||
| 		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, | ||||
| 		private cleanProcessorService: CleanProcessorService, | ||||
| 	) { | ||||
| 		this.logger = this.queueLoggerService.logger; | ||||
| 	} | ||||
| @@ -119,14 +161,6 @@ export class QueueProcessorService { | ||||
| 			.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) | ||||
| 			.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); | ||||
|  | ||||
| 		this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job)); | ||||
| 		this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job)); | ||||
| 		this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); | ||||
| 		this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job)); | ||||
| 		this.dbQueueProcessorsService.start(this.queueService.dbQueue); | ||||
| 		this.relationshipQueueProcessorsService.start(this.queueService.relationshipQueue); | ||||
| 		this.objectStorageQueueProcessorsService.start(this.queueService.objectStorageQueue); | ||||
|  | ||||
| 		this.queueService.systemQueue.add('tickCharts', { | ||||
| 		}, { | ||||
| 			repeat: { cron: '55 * * * *' }, | ||||
| @@ -163,6 +197,46 @@ export class QueueProcessorService { | ||||
| 			removeOnComplete: true, | ||||
| 		}); | ||||
|  | ||||
| 		this.systemQueueProcessorsService.start(this.queueService.systemQueue); | ||||
| 		this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job)); | ||||
| 		this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job)); | ||||
| 		this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); | ||||
| 		this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job)); | ||||
|  | ||||
| 		this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); | ||||
| 		this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); | ||||
| 		this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done)); | ||||
| 		this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); | ||||
|  | ||||
| 		this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job)); | ||||
| 		this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done)); | ||||
| 	 | ||||
| 		{ | ||||
| 			const maxJobs = this.config.relashionshipJobConcurrency ?? 16; | ||||
| 			this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job)); | ||||
| 			this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job)); | ||||
| 			this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job)); | ||||
| 			this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job)); | ||||
| 		} | ||||
|  | ||||
| 		this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done)); | ||||
| 		this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done)); | ||||
| 		this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done)); | ||||
| 		this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done)); | ||||
| 		this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done)); | ||||
| 		this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,26 +0,0 @@ | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; | ||||
| import type Bull from 'bull'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
|  | ||||
| @Injectable() | ||||
| export class RelationshipQueueProcessorsService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
|  | ||||
| 		private relationshipProcessorService: RelationshipProcessorService, | ||||
| 	) { | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public start(q: Bull.Queue): void { | ||||
| 		const maxJobs = this.config.relashionshipJobConcurrency ?? 16; | ||||
| 		q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job)); | ||||
| 		q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job)); | ||||
| 		q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job)); | ||||
| 		q.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job)); | ||||
| 	} | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; | ||||
| import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; | ||||
| import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; | ||||
| import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; | ||||
| import { CleanProcessorService } from './processors/CleanProcessorService.js'; | ||||
| import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; | ||||
| import type Bull from 'bull'; | ||||
|  | ||||
| @Injectable() | ||||
| export class SystemQueueProcessorsService { | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
|  | ||||
| 		private tickChartsProcessorService: TickChartsProcessorService, | ||||
| 		private resyncChartsProcessorService: ResyncChartsProcessorService, | ||||
| 		private cleanChartsProcessorService: CleanChartsProcessorService, | ||||
| 		private aggregateRetentionProcessorService: AggregateRetentionProcessorService, | ||||
| 		private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, | ||||
| 		private cleanProcessorService: CleanProcessorService, | ||||
| 	) { | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public start(q: Bull.Queue): void { | ||||
| 		q.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done)); | ||||
| 		q.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done)); | ||||
| 		q.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done)); | ||||
| 		q.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done)); | ||||
| 		q.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done)); | ||||
| 		q.process('clean', (job, done) => this.cleanProcessorService.process(job, done)); | ||||
| 	} | ||||
| } | ||||
| @@ -68,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 				emailVerified: profile.emailVerified, | ||||
| 				autoAcceptFollowed: profile.autoAcceptFollowed, | ||||
| 				noCrawle: profile.noCrawle, | ||||
| 				preventAiLarning: profile.preventAiLarning, | ||||
| 				alwaysMarkNsfw: profile.alwaysMarkNsfw, | ||||
| 				autoSensitive: profile.autoSensitive, | ||||
| 				carefulBot: profile.carefulBot, | ||||
|   | ||||
| @@ -98,7 +98,7 @@ export const meta = { | ||||
| 			message: 'This feature is restricted by your role.', | ||||
| 			code: 'RESTRICTED_BY_ROLE', | ||||
| 			id: '8feff0ba-5ab5-585b-31f4-4df816663fad', | ||||
| 		} | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	res: { | ||||
| @@ -138,6 +138,7 @@ export const paramDef = { | ||||
| 		carefulBot: { type: 'boolean' }, | ||||
| 		autoAcceptFollowed: { type: 'boolean' }, | ||||
| 		noCrawle: { type: 'boolean' }, | ||||
| 		preventAiLarning: { type: 'boolean' }, | ||||
| 		isBot: { type: 'boolean' }, | ||||
| 		isCat: { type: 'boolean' }, | ||||
| 		showTimelineReplies: { type: 'boolean' }, | ||||
| @@ -242,6 +243,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 			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.preventAiLarning === 'boolean') profileUpdates.preventAiLarning = ps.preventAiLarning; | ||||
| 			if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; | ||||
| 			if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; | ||||
| 			if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; | ||||
|   | ||||
| @@ -36,8 +36,8 @@ import { RoleService } from '@/core/RoleService.js'; | ||||
| import manifest from './manifest.json' assert { type: 'json' }; | ||||
| import { FeedService } from './FeedService.js'; | ||||
| import { UrlPreviewService } from './UrlPreviewService.js'; | ||||
| import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; | ||||
| import { ClientLoggerService } from './ClientLoggerService.js'; | ||||
| import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; | ||||
|  | ||||
| const _filename = fileURLToPath(import.meta.url); | ||||
| const _dirname = dirname(_filename); | ||||
| @@ -437,6 +437,10 @@ export class ClientServerService { | ||||
| 					: []; | ||||
|  | ||||
| 				reply.header('Cache-Control', 'public, max-age=15'); | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('user', { | ||||
| 					user, profile, me, | ||||
| 					avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), | ||||
| @@ -481,6 +485,10 @@ export class ClientServerService { | ||||
| 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); | ||||
| 				const meta = await this.metaService.fetch(); | ||||
| 				reply.header('Cache-Control', 'public, max-age=15'); | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('note', { | ||||
| 					note: _note, | ||||
| 					profile, | ||||
| @@ -520,6 +528,10 @@ export class ClientServerService { | ||||
| 				} else { | ||||
| 					reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); | ||||
| 				} | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('page', { | ||||
| 					page: _page, | ||||
| 					profile, | ||||
| @@ -544,6 +556,10 @@ export class ClientServerService { | ||||
| 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); | ||||
| 				const meta = await this.metaService.fetch(); | ||||
| 				reply.header('Cache-Control', 'public, max-age=15'); | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('flash', { | ||||
| 					flash: _flash, | ||||
| 					profile, | ||||
| @@ -568,6 +584,10 @@ export class ClientServerService { | ||||
| 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); | ||||
| 				const meta = await this.metaService.fetch(); | ||||
| 				reply.header('Cache-Control', 'public, max-age=15'); | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('clip', { | ||||
| 					clip: _clip, | ||||
| 					profile, | ||||
| @@ -590,6 +610,10 @@ export class ClientServerService { | ||||
| 				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); | ||||
| 				const meta = await this.metaService.fetch(); | ||||
| 				reply.header('Cache-Control', 'public, max-age=15'); | ||||
| 				if (profile.preventAiLarning) { | ||||
| 					reply.header('X-Robots-Tag', 'noimageai'); | ||||
| 					reply.header('X-Robots-Tag', 'noai'); | ||||
| 				} | ||||
| 				return await reply.view('gallery-post', { | ||||
| 					post: _post, | ||||
| 					profile, | ||||
|   | ||||
| @@ -21,6 +21,9 @@ block og | ||||
| block meta | ||||
| 	if profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -21,6 +21,9 @@ block og | ||||
| block meta | ||||
| 	if profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -21,6 +21,9 @@ block og | ||||
| block meta | ||||
| 	if user.host || profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -22,6 +22,9 @@ block og | ||||
| block meta | ||||
| 	if user.host || isRenote || profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -21,6 +21,9 @@ block og | ||||
| block meta | ||||
| 	if profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -20,6 +20,9 @@ block og | ||||
| block meta | ||||
| 	if user.host || profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 	if profile.preventAiLarning | ||||
| 		meta(name='robots' content='noimageai') | ||||
| 		meta(name='robots' content='noai') | ||||
|  | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
|   | ||||
| @@ -541,6 +541,61 @@ describe('Note', () => { | ||||
|  | ||||
| 			assert.strictEqual(res.status, 400); | ||||
| 		}); | ||||
|  | ||||
| 		test('センシティブな投稿はhomeになる (単語指定)', async () => { | ||||
| 			const sensitive = await api('admin/update-meta', { | ||||
| 				sensitiveWords: [ | ||||
| 					"test", | ||||
| 				] | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(sensitive.status, 204); | ||||
|  | ||||
| 			await new Promise(x => setTimeout(x, 2)); | ||||
|  | ||||
| 			const note1 = await api('/notes/create', { | ||||
| 				text: 'hogetesthuge', | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(note1.status, 200); | ||||
| 			assert.strictEqual(note1.body.createdNote.visibility, 'home'); | ||||
|  | ||||
| 		}); | ||||
|  | ||||
| 		test('センシティブな投稿はhomeになる (正規表現)', async () => { | ||||
| 			const sensitive = await api('admin/update-meta', { | ||||
| 				sensitiveWords: [ | ||||
| 					"/Test/i", | ||||
| 				] | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(sensitive.status, 204); | ||||
|  | ||||
| 			const note2 = await api('/notes/create', { | ||||
| 				text: 'hogetesthuge', | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(note2.status, 200); | ||||
| 			assert.strictEqual(note2.body.createdNote.visibility, 'home'); | ||||
| 		}); | ||||
|  | ||||
| 		test('センシティブな投稿はhomeになる (スペースアンド)', async () => { | ||||
| 			const sensitive = await api('admin/update-meta', { | ||||
| 				sensitiveWords: [ | ||||
| 					"Test hoge" | ||||
| 				] | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(sensitive.status, 204); | ||||
|  | ||||
| 			const note2 = await api('/notes/create', { | ||||
| 				text: 'hogeTesthuge', | ||||
| 			}, alice); | ||||
|  | ||||
| 			assert.strictEqual(note2.status, 200); | ||||
| 			assert.strictEqual(note2.body.createdNote.visibility, 'home'); | ||||
|  | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	describe('notes/delete', () => { | ||||
|   | ||||
| @@ -145,6 +145,7 @@ describe('ユーザー', () => { | ||||
| 			carefulBot: user.carefulBot, | ||||
| 			autoAcceptFollowed: user.autoAcceptFollowed, | ||||
| 			noCrawle: user.noCrawle, | ||||
| 			preventAiLarning: user.preventAiLarning, | ||||
| 			isExplorable: user.isExplorable, | ||||
| 			isDeleted: user.isDeleted, | ||||
| 			hideOnlineStatus: user.hideOnlineStatus, | ||||
| @@ -370,7 +371,7 @@ describe('ユーザー', () => { | ||||
| 		assert.deepStrictEqual(response.pinnedNotes, []); | ||||
| 		assert.strictEqual(response.pinnedPageId, null); | ||||
| 		assert.strictEqual(response.pinnedPage, null); | ||||
| 		assert.strictEqual(response.publicReactions, false); | ||||
| 		assert.strictEqual(response.publicReactions, true); | ||||
| 		assert.strictEqual(response.ffVisibility, 'public'); | ||||
| 		assert.strictEqual(response.twoFactorEnabled, false); | ||||
| 		assert.strictEqual(response.usePasswordLessLogin, false); | ||||
| @@ -390,6 +391,7 @@ describe('ユーザー', () => { | ||||
| 		assert.strictEqual(response.carefulBot, false); | ||||
| 		assert.strictEqual(response.autoAcceptFollowed, true); | ||||
| 		assert.strictEqual(response.noCrawle, false); | ||||
| 		assert.strictEqual(response.preventAiLarning, true); | ||||
| 		assert.strictEqual(response.isExplorable, true); | ||||
| 		assert.strictEqual(response.isDeleted, false); | ||||
| 		assert.strictEqual(response.hideOnlineStatus, false); | ||||
| @@ -462,6 +464,8 @@ describe('ユーザー', () => { | ||||
| 		{ parameters: (): object => ({ autoAcceptFollowed: false }) }, | ||||
| 		{ parameters: (): object => ({ noCrawle: true }) }, | ||||
| 		{ parameters: (): object => ({ noCrawle: false }) }, | ||||
| 		{ parameters: (): object => ({ preventAiLarning: false }) }, | ||||
| 		{ parameters: (): object => ({ preventAiLarning: true }) }, | ||||
| 		{ parameters: (): object => ({ isBot: true }) }, | ||||
| 		{ parameters: (): object => ({ isBot: false }) }, | ||||
| 		{ parameters: (): object => ({ isCat: true }) }, | ||||
|   | ||||
| @@ -1,144 +0,0 @@ | ||||
| <template> | ||||
| <div | ||||
| 	class="ziffeoms" | ||||
| 	:class="{ disabled, checked }" | ||||
| > | ||||
| 	<input | ||||
| 		ref="input" | ||||
| 		type="checkbox" | ||||
| 		:disabled="disabled" | ||||
| 		@keydown.enter="toggle" | ||||
| 	> | ||||
| 	<span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> | ||||
| 		<i class="check ti ti-check"></i> | ||||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 		<!-- TODO: 無名slotの方は廃止 --> | ||||
| 		<span @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||
| 		<p class="caption"><slot name="caption"></slot></p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { toRefs, Ref } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import MkRippleEffect from '@/components/MkRippleEffect.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	modelValue: boolean | Ref<boolean>; | ||||
| 	disabled?: boolean; | ||||
| }>(); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'update:modelValue', v: boolean): void; | ||||
| }>(); | ||||
|  | ||||
| let button = $shallowRef<HTMLElement>(); | ||||
| const checked = toRefs(props).modelValue; | ||||
| const toggle = () => { | ||||
| 	if (props.disabled) return; | ||||
| 	emit('update:modelValue', !checked.value); | ||||
|  | ||||
| 	if (!checked.value) { | ||||
| 		const rect = button.getBoundingClientRect(); | ||||
| 		const x = rect.left + (button.offsetWidth / 2); | ||||
| 		const y = rect.top + (button.offsetHeight / 2); | ||||
| 		os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end'); | ||||
| 	} | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ziffeoms { | ||||
| 	position: relative; | ||||
| 	display: flex; | ||||
| 	transition: all 0.2s ease; | ||||
|  | ||||
| 	> * { | ||||
| 		user-select: none; | ||||
| 	} | ||||
|  | ||||
| 	> input { | ||||
| 		position: absolute; | ||||
| 		width: 0; | ||||
| 		height: 0; | ||||
| 		opacity: 0; | ||||
| 		margin: 0; | ||||
| 	} | ||||
|  | ||||
| 	> .button { | ||||
| 		position: relative; | ||||
| 		display: inline-flex; | ||||
| 		flex-shrink: 0; | ||||
| 		margin: 0; | ||||
| 		box-sizing: border-box; | ||||
| 		width: 23px; | ||||
| 		height: 23px; | ||||
| 		outline: none; | ||||
| 		background: var(--panel); | ||||
| 		border: solid 1px var(--panel); | ||||
| 		border-radius: 4px; | ||||
| 		cursor: pointer; | ||||
| 		transition: inherit; | ||||
|  | ||||
| 		> .check { | ||||
| 			margin: auto; | ||||
| 			opacity: 0; | ||||
| 			color: var(--fgOnAccent); | ||||
| 			font-size: 13px; | ||||
| 			transform: scale(0.5); | ||||
| 			transition: all 0.2s ease; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| 		> .button { | ||||
| 			border-color: var(--inputBorderHover) !important; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	> .label { | ||||
| 		margin-left: 12px; | ||||
| 		margin-top: 2px; | ||||
| 		display: block; | ||||
| 		transition: inherit; | ||||
| 		color: var(--fg); | ||||
|  | ||||
| 		> span { | ||||
| 			display: block; | ||||
| 			line-height: 20px; | ||||
| 			cursor: pointer; | ||||
| 			transition: inherit; | ||||
| 		} | ||||
|  | ||||
| 		> .caption { | ||||
| 			margin: 8px 0 0 0; | ||||
| 			color: var(--fgTransparentWeak); | ||||
| 			font-size: 0.85em; | ||||
|  | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&.disabled { | ||||
| 		opacity: 0.6; | ||||
| 		cursor: not-allowed; | ||||
| 	} | ||||
|  | ||||
| 	&.checked { | ||||
| 		> .button { | ||||
| 			background-color: var(--accent) !important; | ||||
| 			border-color: var(--accent) !important; | ||||
|  | ||||
| 			> .check { | ||||
| 				opacity: 1; | ||||
| 				transform: scale(1); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| @@ -1,15 +1,15 @@ | ||||
| <template> | ||||
| <MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> | ||||
| 	<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> | ||||
| 		<div ref="headerEl" class="header"> | ||||
| 			<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> | ||||
| 			<span class="title"> | ||||
| 	<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> | ||||
| 		<div ref="headerEl" :class="$style.header"> | ||||
| 			<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> | ||||
| 			<span :class="$style.title"> | ||||
| 				<slot name="header"></slot> | ||||
| 			</span> | ||||
| 			<button v-if="!withOkButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button> | ||||
| 			<button v-if="withOkButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button> | ||||
| 			<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button> | ||||
| 			<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button> | ||||
| 		</div> | ||||
| 		<div class="body"> | ||||
| 		<div :class="$style.body"> | ||||
| 			<slot :width="bodyWidth" :height="bodyHeight"></slot> | ||||
| 		</div> | ||||
| 	</div> | ||||
| @@ -81,8 +81,8 @@ defineExpose({ | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ebkgoccj { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	margin: auto; | ||||
| 	overflow: hidden; | ||||
| 	display: flex; | ||||
| @@ -96,28 +96,31 @@ defineExpose({ | ||||
| 		--root-margin: 16px; | ||||
| 	} | ||||
|  | ||||
| 	> .header { | ||||
| 		$height: 46px; | ||||
| 		$height-narrow: 42px; | ||||
| 	--headerHeight: 46px; | ||||
| 	--headerHeightNarrow: 42px; | ||||
| } | ||||
|  | ||||
| .header { | ||||
| 	display: flex; | ||||
| 	flex-shrink: 0; | ||||
| 	background: var(--windowHeader); | ||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||
| 	backdrop-filter: var(--blur, blur(15px)); | ||||
| } | ||||
|  | ||||
| 		> button { | ||||
| 			height: $height; | ||||
| 			width: $height; | ||||
| .headerButton { | ||||
| 	height: var(--headerHeight); | ||||
| 	width: var(--headerHeight); | ||||
|  | ||||
| 	@media (max-width: 500px) { | ||||
| 				height: $height-narrow; | ||||
| 				width: $height-narrow; | ||||
| 		height: var(--headerHeightNarrow); | ||||
| 		width: var(--headerHeightNarrow); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 		> .title { | ||||
| .title { | ||||
| 	flex: 1; | ||||
| 			line-height: $height; | ||||
| 	line-height: var(--headerHeight); | ||||
| 	padding-left: 32px; | ||||
| 	font-weight: bold; | ||||
| 	white-space: nowrap; | ||||
| @@ -126,21 +129,19 @@ defineExpose({ | ||||
| 	pointer-events: none; | ||||
|  | ||||
| 	@media (max-width: 500px) { | ||||
| 				line-height: $height-narrow; | ||||
| 		line-height: var(--headerHeightNarrow); | ||||
| 		padding-left: 16px; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 		> button + .title { | ||||
| .headerButton + .title { | ||||
| 	padding-left: 0; | ||||
| } | ||||
| 	} | ||||
|  | ||||
| 	> .body { | ||||
| .body { | ||||
| 	flex: 1; | ||||
| 	overflow: auto; | ||||
| 	background: var(--panel); | ||||
| 	container-type: size; | ||||
| } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| <template> | ||||
| <div | ||||
| 	v-adaptive-border | ||||
| 	class="novjtctn" | ||||
| 	:class="{ disabled, checked }" | ||||
| 	:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" | ||||
| 	:aria-checked="checked" | ||||
| 	:aria-disabled="disabled" | ||||
| 	@click="toggle" | ||||
| @@ -10,11 +9,12 @@ | ||||
| 	<input | ||||
| 		type="radio" | ||||
| 		:disabled="disabled" | ||||
| 		:class="$style.input" | ||||
| 	> | ||||
| 	<span class="button"> | ||||
| 	<span :class="$style.button"> | ||||
| 		<span></span> | ||||
| 	</span> | ||||
| 	<span class="label"><slot></slot></span> | ||||
| 	<span :class="$style.label"><slot></slot></span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -39,8 +39,8 @@ function toggle(): void { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .novjtctn { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	position: relative; | ||||
| 	display: inline-block; | ||||
| 	text-align: left; | ||||
| @@ -53,18 +53,12 @@ function toggle(): void { | ||||
| 	border-radius: 6px; | ||||
| 	font-size: 90%; | ||||
| 	transition: all 0.2s; | ||||
|  | ||||
| 	> * { | ||||
| 	user-select: none; | ||||
| 	} | ||||
|  | ||||
| 	&.disabled { | ||||
| 		opacity: 0.6; | ||||
|  | ||||
| 		&, * { | ||||
| 		cursor: not-allowed !important; | ||||
| 	} | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| 		border-color: var(--inputBorderHover) !important; | ||||
| @@ -74,10 +68,7 @@ function toggle(): void { | ||||
| 		background-color: var(--accentedBg) !important; | ||||
| 		border-color: var(--accentedBg) !important; | ||||
| 		color: var(--accent); | ||||
|  | ||||
| 		&, * { | ||||
| 		cursor: default !important; | ||||
| 		} | ||||
|  | ||||
| 		> .button { | ||||
| 			border-color: var(--accent); | ||||
| @@ -89,8 +80,9 @@ function toggle(): void { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 	> input { | ||||
| .input { | ||||
| 	position: absolute; | ||||
| 	width: 0; | ||||
| 	height: 0; | ||||
| @@ -98,7 +90,7 @@ function toggle(): void { | ||||
| 	margin: 0; | ||||
| } | ||||
|  | ||||
| 	> .button { | ||||
| .button { | ||||
| 	position: absolute; | ||||
| 	width: 14px; | ||||
| 	height: 14px; | ||||
| @@ -122,11 +114,10 @@ function toggle(): void { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 	> .label { | ||||
| .label { | ||||
| 	margin-left: 28px; | ||||
| 	display: block; | ||||
| 	line-height: 20px; | ||||
| 	cursor: pointer; | ||||
| } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| <template> | ||||
| <div class="vblkjoeq"> | ||||
| 	<div class="label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show"> | ||||
| 		<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> | ||||
| <div> | ||||
| 	<div :class="$style.label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show"> | ||||
| 		<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> | ||||
| 		<select | ||||
| 			ref="inputEl" | ||||
| 			v-model="v" | ||||
| 			v-adaptive-border | ||||
| 			class="select" | ||||
| 			:class="$style.inputCore" | ||||
| 			:disabled="disabled" | ||||
| 			:required="required" | ||||
| 			:readonly="readonly" | ||||
| @@ -18,9 +18,9 @@ | ||||
| 		> | ||||
| 			<slot></slot> | ||||
| 		</select> | ||||
| 		<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> | ||||
| 		<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> | ||||
| 	</div> | ||||
| 	<div class="caption"><slot name="caption"></slot></div> | ||||
| 	<div :class="$style.caption"><slot name="caption"></slot></div> | ||||
|  | ||||
| 	<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> | ||||
| </div> | ||||
| @@ -169,9 +169,8 @@ function show(ev: MouseEvent) { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .vblkjoeq { | ||||
| 	> .label { | ||||
| <style lang="scss" module> | ||||
| .label { | ||||
| 	font-size: 0.85em; | ||||
| 	padding: 0 0 8px 0; | ||||
| 	user-select: none; | ||||
| @@ -181,7 +180,7 @@ function show(ev: MouseEvent) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 	> .caption { | ||||
| .caption { | ||||
| 	font-size: 0.85em; | ||||
| 	padding: 8px 0 0 0; | ||||
| 	color: var(--fgTransparentWeak); | ||||
| @@ -191,17 +190,39 @@ function show(ev: MouseEvent) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 	> .input { | ||||
| .input { | ||||
| 	position: relative; | ||||
| 	cursor: pointer; | ||||
|  | ||||
| 		&:hover { | ||||
| 			> .select { | ||||
| 				border-color: var(--inputBorderHover) !important; | ||||
| 	&.inline { | ||||
| 		display: inline-block; | ||||
| 		margin: 0; | ||||
| 	} | ||||
|  | ||||
| 	&.focused { | ||||
| 		> .inputCore { | ||||
| 			border-color: var(--accent) !important; | ||||
| 			//box-shadow: 0 0 0 4px var(--focus); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 		> .select { | ||||
| 	&.disabled { | ||||
| 		opacity: 0.7; | ||||
|  | ||||
| 		&, | ||||
| 		> .inputCore { | ||||
| 			cursor: not-allowed !important; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| 		> .inputCore { | ||||
| 			border-color: var(--inputBorderHover) !important; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .inputCore { | ||||
| 	appearance: none; | ||||
| 	-webkit-appearance: none; | ||||
| 	display: block; | ||||
| @@ -219,14 +240,14 @@ function show(ev: MouseEvent) { | ||||
| 	outline: none; | ||||
| 	box-shadow: none; | ||||
| 	box-sizing: border-box; | ||||
| 			cursor: pointer; | ||||
| 	transition: border-color 0.1s ease-out; | ||||
| 	cursor: pointer; | ||||
| 	pointer-events: none; | ||||
| 	user-select: none; | ||||
| } | ||||
|  | ||||
| 		> .prefix, | ||||
| 		> .suffix { | ||||
| .prefix, | ||||
| .suffix { | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	position: absolute; | ||||
| @@ -235,55 +256,29 @@ function show(ev: MouseEvent) { | ||||
| 	padding: 0 12px; | ||||
| 	font-size: 1em; | ||||
| 	height: v-bind("height + 'px'"); | ||||
| 			pointer-events: none; | ||||
|  | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
|  | ||||
| 			> * { | ||||
| 				display: inline-block; | ||||
| 	min-width: 16px; | ||||
| 	max-width: 150px; | ||||
| 	overflow: hidden; | ||||
| 	white-space: nowrap; | ||||
| 	text-overflow: ellipsis; | ||||
| 	box-sizing: border-box; | ||||
| 	pointer-events: none; | ||||
|  | ||||
| 	&:empty { | ||||
| 		display: none; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 		> .prefix { | ||||
| .prefix { | ||||
| 	left: 0; | ||||
| 	padding-right: 6px; | ||||
| } | ||||
|  | ||||
| 		> .suffix { | ||||
| .suffix { | ||||
| 	right: 0; | ||||
| 	padding-left: 6px; | ||||
| } | ||||
|  | ||||
| 		&.inline { | ||||
| 			display: inline-block; | ||||
| 			margin: 0; | ||||
| 		} | ||||
|  | ||||
| 		&.focused { | ||||
| 			> select { | ||||
| 				border-color: var(--accent) !important; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		&.disabled { | ||||
| 			opacity: 0.7; | ||||
|  | ||||
| 			&, * { | ||||
| 				cursor: not-allowed !important; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .chevron { | ||||
| 	transition: transform 0.1s ease-out; | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,19 @@ | ||||
| <template> | ||||
| <div | ||||
| 	class="ziffeomt" | ||||
| 	:class="{ disabled, checked }" | ||||
| > | ||||
| <div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> | ||||
| 	<input | ||||
| 		ref="input" | ||||
| 		type="checkbox" | ||||
| 		:disabled="disabled" | ||||
| 		:class="$style.input" | ||||
| 		@keydown.enter="toggle" | ||||
| 	> | ||||
| 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> | ||||
| 		<div class="knob"></div> | ||||
| 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle"> | ||||
| 		<div :class="$style.knob"></div> | ||||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 	<span :class="$style.body"> | ||||
| 		<!-- TODO: 無名slotの方は廃止 --> | ||||
| 		<span @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||
| 		<p class="caption"><slot name="caption"></slot></p> | ||||
| 		<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||
| 		<p :class="$style.caption"><slot name="caption"></slot></p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
| @@ -45,52 +43,12 @@ const toggle = () => { | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ziffeomt { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	position: relative; | ||||
| 	display: flex; | ||||
| 	transition: all 0.2s ease; | ||||
|  | ||||
| 	> * { | ||||
| 	user-select: none; | ||||
| 	} | ||||
|  | ||||
| 	> input { | ||||
| 		position: absolute; | ||||
| 		width: 0; | ||||
| 		height: 0; | ||||
| 		opacity: 0; | ||||
| 		margin: 0; | ||||
| 	} | ||||
|  | ||||
| 	> .button { | ||||
| 		position: relative; | ||||
| 		display: inline-flex; | ||||
| 		flex-shrink: 0; | ||||
| 		margin: 0; | ||||
| 		box-sizing: border-box; | ||||
| 		width: 32px; | ||||
| 		height: 23px; | ||||
| 		outline: none; | ||||
| 		background: var(--switchOffBg); | ||||
| 		background-clip: content-box; | ||||
| 		border: solid 1px var(--switchOffBg); | ||||
| 		border-radius: 999px; | ||||
| 		cursor: pointer; | ||||
| 		transition: inherit; | ||||
| 		user-select: none; | ||||
|  | ||||
| 		> .knob { | ||||
| 			position: absolute; | ||||
| 			top: 3px; | ||||
| 			left: 3px; | ||||
| 			width: 15px; | ||||
| 			height: 15px; | ||||
| 			background: var(--switchOffFg); | ||||
| 			border-radius: 999px; | ||||
| 			transition: all 0.2s ease; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| 		> .button { | ||||
| @@ -98,31 +56,6 @@ const toggle = () => { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	> .label { | ||||
| 		margin-left: 12px; | ||||
| 		margin-top: 2px; | ||||
| 		display: block; | ||||
| 		transition: inherit; | ||||
| 		color: var(--fg); | ||||
|  | ||||
| 		> span { | ||||
| 			display: block; | ||||
| 			line-height: 20px; | ||||
| 			cursor: pointer; | ||||
| 			transition: inherit; | ||||
| 		} | ||||
|  | ||||
| 		> .caption { | ||||
| 			margin: 8px 0 0 0; | ||||
| 			color: var(--fgTransparentWeak); | ||||
| 			font-size: 0.85em; | ||||
|  | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&.disabled { | ||||
| 		opacity: 0.6; | ||||
| 		cursor: not-allowed; | ||||
| @@ -140,4 +73,66 @@ const toggle = () => { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .input { | ||||
| 	position: absolute; | ||||
| 	width: 0; | ||||
| 	height: 0; | ||||
| 	opacity: 0; | ||||
| 	margin: 0; | ||||
| } | ||||
|  | ||||
| .button { | ||||
| 	position: relative; | ||||
| 	display: inline-flex; | ||||
| 	flex-shrink: 0; | ||||
| 	margin: 0; | ||||
| 	box-sizing: border-box; | ||||
| 	width: 32px; | ||||
| 	height: 23px; | ||||
| 	outline: none; | ||||
| 	background: var(--switchOffBg); | ||||
| 	background-clip: content-box; | ||||
| 	border: solid 1px var(--switchOffBg); | ||||
| 	border-radius: 999px; | ||||
| 	cursor: pointer; | ||||
| 	transition: inherit; | ||||
| 	user-select: none; | ||||
| } | ||||
|  | ||||
| .knob { | ||||
| 	position: absolute; | ||||
| 	top: 3px; | ||||
| 	left: 3px; | ||||
| 	width: 15px; | ||||
| 	height: 15px; | ||||
| 	background: var(--switchOffFg); | ||||
| 	border-radius: 999px; | ||||
| 	transition: all 0.2s ease; | ||||
| } | ||||
|  | ||||
| .body { | ||||
| 	margin-left: 12px; | ||||
| 	margin-top: 2px; | ||||
| 	display: block; | ||||
| 	transition: inherit; | ||||
| 	color: var(--fg); | ||||
| } | ||||
|  | ||||
| .label { | ||||
| 	display: block; | ||||
| 	line-height: 20px; | ||||
| 	cursor: pointer; | ||||
| 	transition: inherit; | ||||
| } | ||||
|  | ||||
| .caption { | ||||
| 	margin: 8px 0 0 0; | ||||
| 	color: var(--fgTransparentWeak); | ||||
| 	font-size: 0.85em; | ||||
|  | ||||
| 	&:empty { | ||||
| 		display: none; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -32,6 +32,7 @@ | ||||
| 	<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| export type Widget = { | ||||
| 	name: string; | ||||
| @@ -42,6 +43,7 @@ export type DefaultStoredWidget = { | ||||
| 	place: string | null; | ||||
| } & Widget; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, ref } from 'vue'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> | ||||
| 	<img :class="$style.inner" :src="url" decoding="async"/> | ||||
| 	<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> | ||||
| 	<div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]"> | ||||
| 	<div v-if="user.isCat" :class="[$style.ears]"> | ||||
| 		<div :class="$style.earLeft"> | ||||
| 			<div v-if="false" :class="$style.layer"> | ||||
| 				<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> | ||||
| @@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => { | ||||
| 		padding: 50%; | ||||
| 		pointer-events: none; | ||||
|  | ||||
| 		&.mask { | ||||
| 			-webkit-mask: | ||||
| 				url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') center / 50% 50%, | ||||
| 				linear-gradient(#fff, #fff); | ||||
| 			-webkit-mask-composite: destination-out, source-over; | ||||
| 			mask: | ||||
| 				url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%, | ||||
| 				linear-gradient(#fff, #fff); // polyfill of `image(#fff)` | ||||
|  | ||||
| 			> .earLeft { | ||||
| 				animation: eartightleft 6s infinite; | ||||
| 			} | ||||
|  | ||||
| 			> .earRight { | ||||
| 				animation: eartightright 6s infinite; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .earLeft, | ||||
| 		> .earRight { | ||||
| 			contain: strict; | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
| 					 | ||||
| 					<MkTextarea v-model="sensitiveWords"> | ||||
| 						<template #label>{{ i18n.ts.sensitiveWords }}</template> | ||||
| 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> | ||||
| 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> | ||||
| 					</MkTextarea> | ||||
| 				</div> | ||||
| 			</FormSuspense> | ||||
|   | ||||
| @@ -28,9 +28,9 @@ | ||||
| 		<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> | ||||
| 			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> | ||||
|  | ||||
| 			<div class="vxjfqztj"> | ||||
| 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA> | ||||
| 				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA> | ||||
| 			<div> | ||||
| 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA> | ||||
| 				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA> | ||||
| 			</div> | ||||
| 		</MkFoldableSection> | ||||
|  | ||||
| @@ -132,15 +132,3 @@ os.api('hashtags/list', { | ||||
| 	tagsRemote = tags; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .vxjfqztj { | ||||
| 	> * { | ||||
| 		margin-right: 16px; | ||||
|  | ||||
| 		&.local { | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -24,6 +24,10 @@ | ||||
| 		{{ i18n.ts.noCrawle }} | ||||
| 		<template #caption>{{ i18n.ts.noCrawleDescription }}</template> | ||||
| 	</MkSwitch> | ||||
| 	<MkSwitch v-model="preventAiLarning" @update:model-value="save()"> | ||||
| 		{{ i18n.ts.preventAiLarning }}<span class="_beta">{{ i18n.ts.beta }}</span> | ||||
| 		<template #caption>{{ i18n.ts.preventAiLarningDescription }}</template> | ||||
| 	</MkSwitch> | ||||
| 	<MkSwitch v-model="isExplorable" @update:model-value="save()"> | ||||
| 		{{ i18n.ts.makeExplorable }} | ||||
| 		<template #caption>{{ i18n.ts.makeExplorableDescription }}</template> | ||||
| @@ -71,6 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| let isLocked = $ref($i.isLocked); | ||||
| let autoAcceptFollowed = $ref($i.autoAcceptFollowed); | ||||
| let noCrawle = $ref($i.noCrawle); | ||||
| let preventAiLarning = $ref($i.preventAiLarning); | ||||
| let isExplorable = $ref($i.isExplorable); | ||||
| let hideOnlineStatus = $ref($i.hideOnlineStatus); | ||||
| let publicReactions = $ref($i.publicReactions); | ||||
| @@ -86,6 +91,7 @@ function save() { | ||||
| 		isLocked: !!isLocked, | ||||
| 		autoAcceptFollowed: !!autoAcceptFollowed, | ||||
| 		noCrawle: !!noCrawle, | ||||
| 		preventAiLarning: !!preventAiLarning, | ||||
| 		isExplorable: !!isExplorable, | ||||
| 		hideOnlineStatus: !!hideOnlineStatus, | ||||
| 		publicReactions: !!publicReactions, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 riku6460
					riku6460