Merge remote-tracking branch 'upstream/develop' into merge-upstream

This commit is contained in:
riku6460 2023-05-10 18:48:15 +09:00
commit f3fd0cd93e
No known key found for this signature in database
GPG key ID: 27414FA27DB94CF6
42 changed files with 639 additions and 671 deletions

View file

@ -12,6 +12,17 @@
--> -->
## 13.12.2
### General
- 投稿したコンテンツのAIによる学習を軽減するオプションを追加
### Client
- Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正
### Server
- センシティブワードの登録にAnd、正規表現が使用できるようになりました。
## 13.12.1 ## 13.12.1
### Client ### Client

View file

@ -1038,8 +1038,11 @@ thisChannelArchived: "Dieser Kanal wurde archiviert."
displayOfNote: "Anzeige von Notizen" displayOfNote: "Anzeige von Notizen"
initialAccountSetting: "Kontoeinrichtung" initialAccountSetting: "Kontoeinrichtung"
youFollowing: "Gefolgt" 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: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
letsFillYourProfile: "Lass uns zuerst dein Profil einrichten." letsFillYourProfile: "Lass uns zuerst dein Profil einrichten."
profileSetting: "Profileinstellungen" profileSetting: "Profileinstellungen"
theseSettingsCanEditLater: "Diese Einstellungen kannst du jederzeit ändern." theseSettingsCanEditLater: "Diese Einstellungen kannst du jederzeit ändern."

View file

@ -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." 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." thisChannelArchived: "This channel has been archived."
displayOfNote: "Note display" displayOfNote: "Note display"
initialAccountSetting: "Profile configuration" initialAccountSetting: "Profile setup"
youFollowing: "Followed" 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: _initialAccountSetting:
accountCreated: "Your account was successfully created!" accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."
letsFillYourProfile: "First, let's set up your profile." letsFillYourProfile: "First, let's set up your profile."
profileSetting: "Profile settings" profileSetting: "Profile settings"
theseSettingsCanEditLater: "You can always change these settings later." 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." 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." 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." 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}!" haveFun: "Enjoy {name}!"
ifYouNeedLearnMore: "If you'd like to learn more about how to use {name} (Misskey), please visit {link}." 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: _serverRules:
description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended."
_accountMigration: _accountMigration:

View file

@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?" resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード" sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
notesSearchNotAvailable: "ノート検索は利用できません。" notesSearchNotAvailable: "ノート検索は利用できません。"
license: "ライセンス" license: "ライセンス"
unfavoriteConfirm: "お気に入り解除しますか?" unfavoriteConfirm: "お気に入り解除しますか?"
@ -1038,6 +1039,8 @@ thisChannelArchived: "このチャンネルはアーカイブされています
displayOfNote: "ノートの表示" displayOfNote: "ノートの表示"
initialAccountSetting: "初期設定" initialAccountSetting: "初期設定"
youFollowing: "フォロー中" youFollowing: "フォロー中"
preventAiLarning: "AIによる学習を拒否"
preventAiLarningDescription: "投稿したートや画像などのコンテンツを学習の対象にしないようAIに要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されます。この機能は実験的であり、AIによる学習を完全に防止するものではありません。"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!" accountCreated: "アカウントの作成が完了しました!"

View file

@ -1240,6 +1240,7 @@ _achievements:
title: "잠깐 쉬어" title: "잠깐 쉬어"
description: "클라이언트를 시작하고 30분이 경과하였습니다" description: "클라이언트를 시작하고 30분이 경과하였습니다"
_client60min: _client60min:
title: "No \"Miss\" in Misskey"
description: "클라이언트를 시작하고 60분이 경과하였습니다" description: "클라이언트를 시작하고 60분이 경과하였습니다"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "있었는데요 없었습니다" title: "있었는데요 없었습니다"

View file

@ -1036,7 +1036,21 @@ channelArchiveConfirmTitle: "要封存{name}嗎?"
channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。" channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。"
thisChannelArchived: "這個頻道已被封存。" thisChannelArchived: "這個頻道已被封存。"
displayOfNote: "顯示貼文" displayOfNote: "顯示貼文"
youFollowing: "關注中" initialAccountSetting: "初始設定"
youFollowing: "追隨中"
_initialAccountSetting:
accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
letsFillYourProfile: "首先,來設定您的個人檔案吧。"
profileSetting: "個人檔案設定"
theseSettingsCanEditLater: "這裡的設定可以在之後變更。"
youCanEditMoreSettingsInSettingsPageLater: "除此之外,還可以在「設定」頁面進行各種設定。之後請確認看看。"
followUsers: "為了構築時間軸,試著追蹤您感興趣的使用者吧。"
pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。"
initialAccountSettingCompleted: "初始設定完成了!"
haveFun: "盡情享受{name}吧!"
ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。"
skipAreYouSure: "要略過初始設定嗎?"
_serverRules: _serverRules:
description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。"
_accountMigration: _accountMigration:
@ -1464,7 +1478,7 @@ _channel:
removeBanner: "移除橫幅圖像" removeBanner: "移除橫幅圖像"
featured: "熱門貼文" featured: "熱門貼文"
owned: "管理中" owned: "管理中"
following: "關注中" following: "追隨中"
usersCount: "有{n}人參與" usersCount: "有{n}人參與"
notesCount: "有{n}個貼文" notesCount: "有{n}個貼文"
nameAndDescription: "名稱與說明" nameAndDescription: "名稱與說明"
@ -1586,6 +1600,16 @@ _time:
minute: "分鐘" minute: "分鐘"
hour: "小時" hour: "小時"
day: "日" day: "日"
_timelineTutorial:
title: "Misskey的使用方法"
step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。"
step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。"
step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。"
step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。"
step3_1: "貼文發出去了嗎?"
step3_2: "如果你的貼文出現在時間軸上,就代表發文成功。"
step4_1: "可以對貼文標記「反應」。"
step4_2: "點擊貼文的「+」圖示,即可選擇喜好的表情符號來標記反應。"
_2fa: _2fa:
alreadyRegistered: "此設備已經被註冊過了" alreadyRegistered: "此設備已經被註冊過了"
registerTOTP: "開始設定驗證應用程式" registerTOTP: "開始設定驗證應用程式"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.12.1", "version": "13.12.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -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"`);
}
}

View file

@ -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`);
}
}

View file

@ -18,10 +18,12 @@ export async function server() {
const serverService = app.get(ServerService); const serverService = app.get(ServerService);
await serverService.launch(); await serverService.launch();
app.get(ChartManagementService).start(); if (process.env.NODE_ENV !== 'test') {
app.get(JanitorService).start(); app.get(ChartManagementService).start();
app.get(QueueStatsService).start(); app.get(JanitorService).start();
app.get(ServerStatsService).start(); app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
}
return app; return app;
} }

View file

@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm'; import { In, DataSource } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.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.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) { 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'; data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home'; data.visibility = 'home';
@ -671,6 +673,31 @@ export class NoteCreateService implements OnApplicationShutdown {
this.index(note); 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 @bindThis
private incRenoteCount(renote: Note) { private incRenoteCount(renote: Note) {
this.notesRepository.createQueryBuilder().update() this.notesRepository.createQueryBuilder().update()

View file

@ -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 Bull from 'bull';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.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 EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>; export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>; 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 RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue<ObjectStorageJobData>; export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>; export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
const $system: Provider = { const $system: Provider = {
@ -118,4 +119,36 @@ const $webhookDeliver: Provider = {
$webhookDeliver, $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(),
]);
}
}

View file

@ -445,6 +445,7 @@ export class UserEntityService implements OnModuleInit {
carefulBot: profile!.carefulBot, carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed, autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle, noCrawle: profile!.noCrawle,
preventAiLarning: profile!.preventAiLarning,
isExplorable: user.isExplorable, isExplorable: user.isExplorable,
isDeleted: user.isDeleted, isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus, hideOnlineStatus: user.hideOnlineStatus,

View file

@ -76,7 +76,7 @@ export class UserProfile {
public emailNotificationTypes: string[]; public emailNotificationTypes: string[];
@Column('boolean', { @Column('boolean', {
default: false, default: true,
}) })
public publicReactions: boolean; public publicReactions: boolean;
@ -147,6 +147,11 @@ export class UserProfile {
}) })
public noCrawle: boolean; public noCrawle: boolean;
@Column('boolean', {
default: true,
})
public preventAiLarning: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View file

@ -302,7 +302,11 @@ export const packedMeDetailedOnlySchema = {
}, },
noCrawle: { noCrawle: {
type: 'boolean', type: 'boolean',
nullable: true, optional: false, nullable: false, optional: false,
},
preventAiLarning: {
type: 'boolean',
nullable: false, optional: false,
}, },
isExplorable: { isExplorable: {
type: 'boolean', type: 'boolean',

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -3,14 +3,10 @@ import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.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 { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { SystemQueueProcessorsService } from './SystemQueueProcessorsService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
@ -68,10 +64,6 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteFileProcessorService, DeleteFileProcessorService,
CleanRemoteFilesProcessorService, CleanRemoteFilesProcessorService,
RelationshipProcessorService, RelationshipProcessorService,
SystemQueueProcessorsService,
ObjectStorageQueueProcessorsService,
DbQueueProcessorsService,
RelationshipQueueProcessorsService,
WebhookDeliverProcessorService, WebhookDeliverProcessorService,
EndedPollNotificationProcessorService, EndedPollNotificationProcessorService,
DeliverProcessorService, DeliverProcessorService,

View file

@ -5,15 +5,36 @@ import type Logger from '@/logger.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { getJobInfo } from './get-job-info.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 { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.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 { QueueLoggerService } from './QueueLoggerService.js';
import { RelationshipQueueProcessorsService } from './RelationshipQueueProcessorsService.js';
@Injectable() @Injectable()
export class QueueProcessorService { export class QueueProcessorService {
@ -25,14 +46,35 @@ export class QueueProcessorService {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private queueService: QueueService, private queueService: QueueService,
private systemQueueProcessorsService: SystemQueueProcessorsService,
private objectStorageQueueProcessorsService: ObjectStorageQueueProcessorsService,
private dbQueueProcessorsService: DbQueueProcessorsService,
private relationshipQueueProcessorsService: RelationshipQueueProcessorsService,
private webhookDeliverProcessorService: WebhookDeliverProcessorService, private webhookDeliverProcessorService: WebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deliverProcessorService: DeliverProcessorService, private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService, 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; 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('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}`)); .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', { this.queueService.systemQueue.add('tickCharts', {
}, { }, {
repeat: { cron: '55 * * * *' }, repeat: { cron: '55 * * * *' },
@ -163,6 +197,46 @@ export class QueueProcessorService {
removeOnComplete: true, 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));
} }
} }

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -68,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
emailVerified: profile.emailVerified, emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed, autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle, noCrawle: profile.noCrawle,
preventAiLarning: profile.preventAiLarning,
alwaysMarkNsfw: profile.alwaysMarkNsfw, alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive, autoSensitive: profile.autoSensitive,
carefulBot: profile.carefulBot, carefulBot: profile.carefulBot,

View file

@ -98,7 +98,7 @@ export const meta = {
message: 'This feature is restricted by your role.', message: 'This feature is restricted by your role.',
code: 'RESTRICTED_BY_ROLE', code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad', id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
} },
}, },
res: { res: {
@ -138,6 +138,7 @@ export const paramDef = {
carefulBot: { type: 'boolean' }, carefulBot: { type: 'boolean' },
autoAcceptFollowed: { type: 'boolean' }, autoAcceptFollowed: { type: 'boolean' },
noCrawle: { type: 'boolean' }, noCrawle: { type: 'boolean' },
preventAiLarning: { type: 'boolean' },
isBot: { type: 'boolean' }, isBot: { type: 'boolean' },
isCat: { type: 'boolean' }, isCat: { type: 'boolean' },
showTimelineReplies: { 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.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; 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.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;

View file

@ -36,8 +36,8 @@ import { RoleService } from '@/core/RoleService.js';
import manifest from './manifest.json' assert { type: 'json' }; import manifest from './manifest.json' assert { type: 'json' };
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -437,6 +437,10 @@ export class ClientServerService {
: []; : [];
reply.header('Cache-Control', 'public, max-age=15'); 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', { return await reply.view('user', {
user, profile, me, user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
@ -481,6 +485,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); 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', { return await reply.view('note', {
note: _note, note: _note,
profile, profile,
@ -520,6 +528,10 @@ export class ClientServerService {
} else { } else {
reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); 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', { return await reply.view('page', {
page: _page, page: _page,
profile, profile,
@ -544,6 +556,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); 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', { return await reply.view('flash', {
flash: _flash, flash: _flash,
profile, profile,
@ -568,6 +584,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); 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', { return await reply.view('clip', {
clip: _clip, clip: _clip,
profile, profile,
@ -590,6 +610,10 @@ export class ClientServerService {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15'); 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', { return await reply.view('gallery-post', {
post: _post, post: _post,
profile, profile,

View file

@ -21,6 +21,9 @@ block og
block meta block meta
if profile.noCrawle if profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta block meta
if profile.noCrawle if profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta block meta
if user.host || profile.noCrawle if user.host || profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -22,6 +22,9 @@ block og
block meta block meta
if user.host || isRenote || profile.noCrawle if user.host || isRenote || profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -21,6 +21,9 @@ block og
block meta block meta
if profile.noCrawle if profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -20,6 +20,9 @@ block og
block meta block meta
if user.host || profile.noCrawle if user.host || profile.noCrawle
meta(name='robots' content='noindex') 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-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -541,6 +541,61 @@ describe('Note', () => {
assert.strictEqual(res.status, 400); 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', () => { describe('notes/delete', () => {

View file

@ -145,6 +145,7 @@ describe('ユーザー', () => {
carefulBot: user.carefulBot, carefulBot: user.carefulBot,
autoAcceptFollowed: user.autoAcceptFollowed, autoAcceptFollowed: user.autoAcceptFollowed,
noCrawle: user.noCrawle, noCrawle: user.noCrawle,
preventAiLarning: user.preventAiLarning,
isExplorable: user.isExplorable, isExplorable: user.isExplorable,
isDeleted: user.isDeleted, isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus, hideOnlineStatus: user.hideOnlineStatus,
@ -370,7 +371,7 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.pinnedNotes, []); assert.deepStrictEqual(response.pinnedNotes, []);
assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPageId, null);
assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.pinnedPage, null);
assert.strictEqual(response.publicReactions, false); assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.ffVisibility, 'public'); assert.strictEqual(response.ffVisibility, 'public');
assert.strictEqual(response.twoFactorEnabled, false); assert.strictEqual(response.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false); assert.strictEqual(response.usePasswordLessLogin, false);
@ -390,6 +391,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.carefulBot, false); assert.strictEqual(response.carefulBot, false);
assert.strictEqual(response.autoAcceptFollowed, true); assert.strictEqual(response.autoAcceptFollowed, true);
assert.strictEqual(response.noCrawle, false); assert.strictEqual(response.noCrawle, false);
assert.strictEqual(response.preventAiLarning, true);
assert.strictEqual(response.isExplorable, true); assert.strictEqual(response.isExplorable, true);
assert.strictEqual(response.isDeleted, false); assert.strictEqual(response.isDeleted, false);
assert.strictEqual(response.hideOnlineStatus, false); assert.strictEqual(response.hideOnlineStatus, false);
@ -462,6 +464,8 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ autoAcceptFollowed: false }) }, { parameters: (): object => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) }, { parameters: (): object => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) }, { parameters: (): object => ({ noCrawle: false }) },
{ parameters: (): object => ({ preventAiLarning: false }) },
{ parameters: (): object => ({ preventAiLarning: true }) },
{ parameters: (): object => ({ isBot: true }) }, { parameters: (): object => ({ isBot: true }) },
{ parameters: (): object => ({ isBot: false }) }, { parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) }, { parameters: (): object => ({ isCat: true }) },

View file

@ -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>

View file

@ -1,15 +1,15 @@
<template> <template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')"> <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="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
<div ref="headerEl" class="header"> <div ref="headerEl" :class="$style.header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button> <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
<span class="title"> <span :class="$style.title">
<slot name="header"></slot> <slot name="header"></slot>
</span> </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="$style.headerButton" 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" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
</div> </div>
<div class="body"> <div :class="$style.body">
<slot :width="bodyWidth" :height="bodyHeight"></slot> <slot :width="bodyWidth" :height="bodyHeight"></slot>
</div> </div>
</div> </div>
@ -81,8 +81,8 @@ defineExpose({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.ebkgoccj { .root {
margin: auto; margin: auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@ -96,51 +96,52 @@ defineExpose({
--root-margin: 16px; --root-margin: 16px;
} }
> .header { --headerHeight: 46px;
$height: 46px; --headerHeightNarrow: 42px;
$height-narrow: 42px; }
display: flex;
flex-shrink: 0;
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
> button { .header {
height: $height; display: flex;
width: $height; flex-shrink: 0;
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
@media (max-width: 500px) { .headerButton {
height: $height-narrow; height: var(--headerHeight);
width: $height-narrow; width: var(--headerHeight);
}
}
> .title { @media (max-width: 500px) {
flex: 1; height: var(--headerHeightNarrow);
line-height: $height; width: var(--headerHeightNarrow);
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
@media (max-width: 500px) {
line-height: $height-narrow;
padding-left: 16px;
}
}
> button + .title {
padding-left: 0;
}
}
> .body {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: size;
} }
} }
.title {
flex: 1;
line-height: var(--headerHeight);
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
@media (max-width: 500px) {
line-height: var(--headerHeightNarrow);
padding-left: 16px;
}
}
.headerButton + .title {
padding-left: 0;
}
.body {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: size;
}
</style> </style>

View file

@ -1,8 +1,7 @@
<template> <template>
<div <div
v-adaptive-border v-adaptive-border
class="novjtctn" :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:class="{ disabled, checked }"
:aria-checked="checked" :aria-checked="checked"
:aria-disabled="disabled" :aria-disabled="disabled"
@click="toggle" @click="toggle"
@ -10,11 +9,12 @@
<input <input
type="radio" type="radio"
:disabled="disabled" :disabled="disabled"
:class="$style.input"
> >
<span class="button"> <span :class="$style.button">
<span></span> <span></span>
</span> </span>
<span class="label"><slot></slot></span> <span :class="$style.label"><slot></slot></span>
</div> </div>
</template> </template>
@ -39,8 +39,8 @@ function toggle(): void {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.novjtctn { .root {
position: relative; position: relative;
display: inline-block; display: inline-block;
text-align: left; text-align: left;
@ -53,17 +53,11 @@ function toggle(): void {
border-radius: 6px; border-radius: 6px;
font-size: 90%; font-size: 90%;
transition: all 0.2s; transition: all 0.2s;
user-select: none;
> * {
user-select: none;
}
&.disabled { &.disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed !important;
&, * {
cursor: not-allowed !important;
}
} }
&:hover { &:hover {
@ -74,10 +68,7 @@ function toggle(): void {
background-color: var(--accentedBg) !important; background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important; border-color: var(--accentedBg) !important;
color: var(--accent); color: var(--accent);
cursor: default !important;
&, * {
cursor: default !important;
}
> .button { > .button {
border-color: var(--accent); border-color: var(--accent);
@ -89,44 +80,44 @@ function toggle(): void {
} }
} }
} }
}
> input { .input {
position: absolute; position: absolute;
width: 0; width: 0;
height: 0; height: 0;
opacity: 0; opacity: 0;
margin: 0; margin: 0;
} }
> .button { .button {
position: absolute; position: absolute;
width: 14px; width: 14px;
height: 14px; height: 14px;
background: none; background: none;
border: solid 2px var(--inputBorder); border: solid 2px var(--inputBorder);
border-radius: 100%; border-radius: 100%;
transition: inherit; transition: inherit;
&:after { &:after {
content: ''; content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
> .label {
margin-left: 28px;
display: block; display: block;
line-height: 20px; position: absolute;
cursor: pointer; top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
} }
} }
.label {
margin-left: 28px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style> </style>

View file

@ -1,13 +1,13 @@
<template> <template>
<div class="vblkjoeq"> <div>
<div class="label" @click="focus"><slot name="label"></slot></div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show"> <div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<select <select
ref="inputEl" ref="inputEl"
v-model="v" v-model="v"
v-adaptive-border v-adaptive-border
class="select" :class="$style.inputCore"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"
@ -18,9 +18,9 @@
> >
<slot></slot> <slot></slot>
</select> </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>
<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> <MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div> </div>
@ -169,121 +169,116 @@ function show(ev: MouseEvent) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.vblkjoeq { .label {
> .label { font-size: 0.85em;
font-size: 0.85em; padding: 0 0 8px 0;
padding: 0 0 8px 0; user-select: none;
user-select: none;
&:empty { &:empty {
display: none; display: none;
}
}
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty {
display: none;
}
}
.input {
position: relative;
cursor: pointer;
&.inline {
display: inline-block;
margin: 0;
}
&.focused {
> .inputCore {
border-color: var(--accent) !important;
//box-shadow: 0 0 0 4px var(--focus);
} }
} }
> .caption { &.disabled {
font-size: 0.85em; opacity: 0.7;
padding: 8px 0 0 0;
color: var(--fgTransparentWeak);
&:empty { &,
display: none; > .inputCore {
cursor: not-allowed !important;
} }
} }
> .input { &:hover {
position: relative; > .inputCore {
cursor: pointer; border-color: var(--inputBorderHover) !important;
&:hover {
> .select {
border-color: var(--inputBorderHover) !important;
}
}
> .select {
appearance: none;
-webkit-appearance: none;
display: block;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.1s ease-out;
pointer-events: none;
user-select: none;
}
> .prefix,
> .suffix {
display: flex;
align-items: center;
position: absolute;
z-index: 1;
top: 0;
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;
}
}
> .prefix {
left: 0;
padding-right: 6px;
}
> .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> .inputCore {
appearance: none;
-webkit-appearance: none;
display: block;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--fg);
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
transition: border-color 0.1s ease-out;
cursor: pointer;
pointer-events: none;
user-select: none;
}
.prefix,
.suffix {
display: flex;
align-items: center;
position: absolute;
z-index: 1;
top: 0;
padding: 0 12px;
font-size: 1em;
height: v-bind("height + 'px'");
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 {
left: 0;
padding-right: 6px;
}
.suffix {
right: 0;
padding-left: 6px;
}
.chevron { .chevron {
transition: transform 0.1s ease-out; transition: transform 0.1s ease-out;
} }

View file

@ -1,21 +1,19 @@
<template> <template>
<div <div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
class="ziffeomt"
:class="{ disabled, checked }"
>
<input <input
ref="input" ref="input"
type="checkbox" type="checkbox"
:disabled="disabled" :disabled="disabled"
:class="$style.input"
@keydown.enter="toggle" @keydown.enter="toggle"
> >
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
<div class="knob"></div> <div :class="$style.knob"></div>
</span> </span>
<span class="label"> <span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 --> <!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span> <span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p> <p :class="$style.caption"><slot name="caption"></slot></p>
</span> </span>
</div> </div>
</template> </template>
@ -45,52 +43,12 @@ const toggle = () => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.ziffeomt { .root {
position: relative; position: relative;
display: flex; display: flex;
transition: all 0.2s ease; transition: all 0.2s ease;
user-select: none;
> * {
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 { &:hover {
> .button { > .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 { &.disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; 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> </style>

View file

@ -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)"/> <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> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
export type Widget = { export type Widget = {
name: string; name: string;
@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
place: string | null; place: string | null;
} & Widget; } & Widget;
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'; import { defineAsyncComponent, ref } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';

View file

@ -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"> <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"/> <img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <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 :class="$style.earLeft">
<div v-if="false" :class="$style.layer"> <div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
@ -154,24 +154,6 @@ watch(() => props.user.avatarBlurhash, () => {
padding: 50%; padding: 50%;
pointer-events: none; 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, > .earLeft,
> .earRight { > .earRight {
contain: strict; contain: strict;

View file

@ -27,7 +27,7 @@
<MkTextarea v-model="sensitiveWords"> <MkTextarea v-model="sensitiveWords">
<template #label>{{ i18n.ts.sensitiveWords }}</template> <template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea> </MkTextarea>
</div> </div>
</FormSuspense> </FormSuspense>

View file

@ -28,9 +28,9 @@
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> <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> <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
<div class="vxjfqztj"> <div>
<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 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}`">{{ 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> </div>
</MkFoldableSection> </MkFoldableSection>
@ -132,15 +132,3 @@ os.api('hashtags/list', {
tagsRemote = tags; tagsRemote = tags;
}); });
</script> </script>
<style lang="scss" scoped>
.vxjfqztj {
> * {
margin-right: 16px;
&.local {
font-weight: bold;
}
}
}
</style>

View file

@ -24,6 +24,10 @@
{{ i18n.ts.noCrawle }} {{ i18n.ts.noCrawle }}
<template #caption>{{ i18n.ts.noCrawleDescription }}</template> <template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch> </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()"> <MkSwitch v-model="isExplorable" @update:model-value="save()">
{{ i18n.ts.makeExplorable }} {{ i18n.ts.makeExplorable }}
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template> <template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
@ -71,6 +75,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
let isLocked = $ref($i.isLocked); let isLocked = $ref($i.isLocked);
let autoAcceptFollowed = $ref($i.autoAcceptFollowed); let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
let noCrawle = $ref($i.noCrawle); let noCrawle = $ref($i.noCrawle);
let preventAiLarning = $ref($i.preventAiLarning);
let isExplorable = $ref($i.isExplorable); let isExplorable = $ref($i.isExplorable);
let hideOnlineStatus = $ref($i.hideOnlineStatus); let hideOnlineStatus = $ref($i.hideOnlineStatus);
let publicReactions = $ref($i.publicReactions); let publicReactions = $ref($i.publicReactions);
@ -86,6 +91,7 @@ function save() {
isLocked: !!isLocked, isLocked: !!isLocked,
autoAcceptFollowed: !!autoAcceptFollowed, autoAcceptFollowed: !!autoAcceptFollowed,
noCrawle: !!noCrawle, noCrawle: !!noCrawle,
preventAiLarning: !!preventAiLarning,
isExplorable: !!isExplorable, isExplorable: !!isExplorable,
hideOnlineStatus: !!hideOnlineStatus, hideOnlineStatus: !!hideOnlineStatus,
publicReactions: !!publicReactions, publicReactions: !!publicReactions,