diff --git a/locales/index.d.ts b/locales/index.d.ts index 70eba52ea0..4a46883e9f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11631,9 +11631,43 @@ export interface Locale extends ILocale { */ "robotsTxt": string; /** - * Adding entries here will override the default robots.txt packaged with Sharkey. Maximum 2048 characters. + * Adding entries here will override the default robots.txt packaged with Sharkey. */ "robotsTxtDescription": string; + /** + * Default content warning for new posts + */ + "defaultCW": string; + /** + * The value here will be auto-filled as the content warning for all new posts and replies. + */ + "defaultCWDescription": string; + /** + * Automatic CW priority + */ + "defaultCWPriority": string; + /** + * Select preferred action when default CW and keep CW settings are both enabled at the same time. + */ + "defaultCWPriorityDescription": string; + "_defaultCWPriority": { + /** + * Use Default (use the default CW, ignoring the inherited CW) + */ + "default": string; + /** + * Use Parent (use the inherited CW, ignoring the default CW) + */ + "parent": string; + /** + * Use Default, then Parent (use the default CW, and append the inherited CW) + */ + "defaultParent": string; + /** + * Use Parent, then Default (use the inherited CW, and append the default CW) + */ + "parentDefault": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/packages/backend/migration/1738446745738-add_user_profile_default_cw.js b/packages/backend/migration/1738446745738-add_user_profile_default_cw.js new file mode 100644 index 0000000000..205ca2087a --- /dev/null +++ b/packages/backend/migration/1738446745738-add_user_profile_default_cw.js @@ -0,0 +1,11 @@ +export class AddUserProfileDefaultCw1738446745738 { + name = 'AddUserProfileDefaultCw1738446745738' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw" text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw"`); + } +} diff --git a/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js b/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js new file mode 100644 index 0000000000..90de25e06f --- /dev/null +++ b/packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js @@ -0,0 +1,13 @@ +export class AddUserProfileDefaultCwPriority1738468079662 { + name = 'AddUserProfileDefaultCwPriority1738468079662' + + async up(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_default_cw_priority_enum" AS ENUM ('default', 'parent', 'defaultParent', 'parentDefault')`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw_priority" "public"."user_profile_default_cw_priority_enum" NOT NULL DEFAULT 'parent'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw_priority"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_default_cw_priority_enum"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 6bfe865038..6ea2d6629a 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -49,11 +49,13 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; -import { isSystemAccount } from '@/misc/is-system-account.js'; + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ const Ajv = _Ajv.default; const ajv = new Ajv(); @@ -669,6 +671,8 @@ export class UserEntityService implements OnModuleInit { achievements: profile!.achievements, loggedInDays: profile!.loggedInDates.length, policies: this.roleService.getUserPolicies(user.id), + defaultCW: profile!.defaultCW, + defaultCWPriority: profile!.defaultCWPriority, } : {}), ...(opts.includeSecrets ? { diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 751b1aff08..449c2f370b 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -4,7 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes, noteVisibilities, defaultCWPriorities } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; @@ -36,10 +36,10 @@ export class MiUserProfile { }) public birthday: string | null; - @Column("varchar", { + @Column('varchar', { length: 128, nullable: true, - comment: "The ListenBrainz username of the User.", + comment: 'The ListenBrainz username of the User.', }) public listenbrainz: string | null; @@ -290,6 +290,19 @@ export class MiUserProfile { unlockedAt: number; }[]; + @Column('text', { + name: 'default_cw', + nullable: true, + }) + public defaultCW: string | null; + + @Column('enum', { + name: 'default_cw_priority', + enum: defaultCWPriorities, + default: 'parent', + }) + public defaultCWPriority: typeof defaultCWPriorities[number]; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index f953008b3f..93b031e9c5 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -752,6 +752,15 @@ export const packedMeDetailedOnlySchema = { }, }, //#endregion + defaultCW: { + type: 'string', + nullable: true, optional: false, + }, + defaultCWPriority: { + type: 'string', + enum: ['default', 'parent', 'defaultParent', 'parentDefault'], + nullable: false, optional: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 09c06a108d..e1552fed8a 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -133,6 +133,12 @@ export const meta = { id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191', httpStatusCode: 422, }, + + maxCwLength: { + message: 'You tried setting a default content warning which is too long.', + code: 'MAX_CW_LENGTH', + id: '7004c478-bda3-4b4f-acb2-4316398c9d52', + }, }, res: { @@ -243,6 +249,12 @@ export const paramDef = { uniqueItems: true, items: { type: 'string' }, }, + defaultCW: { type: 'string', nullable: true }, + defaultCWPriority: { + type: 'string', + enum: ['default', 'parent', 'defaultParent', 'parentDefault'], + nullable: false, + }, }, } as const; @@ -494,6 +506,19 @@ export default class extends Endpoint { // eslint- updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null; } + let defaultCW = ps.defaultCW; + if (defaultCW !== undefined) { + if (defaultCW === '') defaultCW = null; + if (defaultCW && defaultCW.length > this.config.maxCwLength) { + throw new ApiError(meta.errors.maxCwLength); + } + + profileUpdates.defaultCW = defaultCW; + } + if (ps.defaultCWPriority !== undefined) { + profileUpdates.defaultCWPriority = ps.defaultCWPriority; + } + //#region emojis/tags let emojis = [] as string[]; diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 37bed27fb1..067481d9da 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -58,6 +58,8 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const followingVisibilities = ['public', 'followers', 'private'] as const; export const followersVisibilities = ['public', 'followers', 'private'] as const; +export const defaultCWPriorities = ['default', 'parent', 'defaultParent', 'parentDefault'] as const; + /** * ユーザーがエクスポートできるものの種類 * diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 11ae6dbd6a..6f057ed5eb 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -364,6 +364,29 @@ if (defaultStore.state.keepCw && props.reply && props.reply.cw) { cw.value = props.reply.cw; } +// apply default CW +if ($i.defaultCW) { + useCw.value = true; + + if (!cw.value || $i.defaultCWPriority === 'default') { + cw.value = $i.defaultCW; + } else if ($i.defaultCWPriority !== 'parent') { + // This is a fancy way of simulating /\bsearch\b/ without a regular expression. + // We're checking to see whether the default CW appears inside the existing CW, but *only* if there's word boundaries. + const parts = cw.value.split($i.defaultCW); + const hasExistingDefaultCW = parts.length === 2 && !/\w$/.test(parts[0]) && !/^\w/.test(parts[1]); + if (!hasExistingDefaultCW) { + // We need to merge the CWs + if ($i.defaultCWPriority === 'defaultParent') { + cw.value = `${$i.defaultCW}, ${cw.value}`; + } else if ($i.defaultCWPriority === 'parentDefault') { + cw.value = `${cw.value}, ${$i.defaultCW}`; + } + } + } + // else { do nothing, because existing CW takes priority. } +} + function watchForDraft() { watch(text, () => saveDraft()); watch(useCw, () => saveDraft()); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 790f9e44e2..db51506596 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -155,10 +155,24 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._visibility.disableFederation }} + + {{ i18n.ts.keepCw }} + + + + + + + + + + + + + + - - {{ i18n.ts.keepCw }} @@ -193,6 +207,8 @@ const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); const followingVisibility = ref($i.followingVisibility); const followersVisibility = ref($i.followersVisibility); +const defaultCW = ref($i.defaultCW); +const defaultCWPriority = ref($i.defaultCWPriority); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -251,6 +267,8 @@ function save() { publicReactions: !!publicReactions.value, followingVisibility: followingVisibility.value, followersVisibility: followersVisibility.value, + defaultCWPriority: defaultCWPriority.value, + defaultCW: defaultCW.value, }); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 888e46e008..c7268ade6a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4217,6 +4217,9 @@ export type components = { /** Format: date-time */ lastUsed: string; }[]; + defaultCW: string | null; + /** @enum {string} */ + defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault'; }; UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly']; MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly']; @@ -5224,6 +5227,7 @@ export type components = { enableFC: boolean; fcSiteKey: string | null; enableAchievements: boolean | null; + robotsTxt: string | null; enableTestcaptcha: boolean; swPublickey: string | null; /** @default /assets/ai.png */ @@ -5434,6 +5438,7 @@ export type operations = { enableStatsForFederatedInstances: boolean; enableServerMachineStats: boolean; enableAchievements: boolean; + robotsTxt: string | null; enableIdenticonGeneration: boolean; manifestJsonOverride: string; policies: Record; @@ -10163,6 +10168,7 @@ export type operations = { enableStatsForFederatedInstances?: boolean; enableServerMachineStats?: boolean; enableAchievements?: boolean; + robotsTxt?: string | null; enableIdenticonGeneration?: boolean; serverRules?: string[]; bannedEmailDomains?: string[]; @@ -21631,6 +21637,9 @@ export type operations = { }; emailNotificationTypes?: string[]; alsoKnownAs?: string[]; + defaultCW?: string | null; + /** @enum {string} */ + defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault'; }; }; }; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 8219ccd046..cd3a44407a 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -437,3 +437,13 @@ _permissions: robotsTxt: "Custom robots.txt" robotsTxtDescription: "Adding entries here will override the default robots.txt packaged with Sharkey." + +defaultCW: "Default content warning for new posts" +defaultCWDescription: "The value here will be auto-filled as the content warning for all new posts and replies." +defaultCWPriority: "Automatic CW priority" +defaultCWPriorityDescription: "Select preferred action when default CW and keep CW settings are both enabled at the same time." +_defaultCWPriority: + default: "Use Default (use the default CW, ignoring the inherited CW)" + parent: "Use Parent (use the inherited CW, ignoring the default CW)" + defaultParent: "Use Default, then Parent (use the default CW, and append the inherited CW)" + parentDefault: "Use Parent, then Default (use the inherited CW, and append the default CW)"