summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarie <github@yuugi.dev>2025-02-07 03:47:20 +0000
committerMarie <github@yuugi.dev>2025-02-07 03:47:20 +0000
commitd629b882b0b5b231557d395e5554f2b01ac69f92 (patch)
tree5ebc3b31443f0b61dedee01489a73de9e5d2ad3d
parentmerge: Increase the rate limit for `/api/i` endpoint (!882) (diff)
parentfix check for parts.length (diff)
downloadsharkey-d629b882b0b5b231557d395e5554f2b01ac69f92.tar.gz
sharkey-d629b882b0b5b231557d395e5554f2b01ac69f92.tar.bz2
sharkey-d629b882b0b5b231557d395e5554f2b01ac69f92.zip
merge: Allow users to set a default content warning for their new posts (resolves #901) (!881)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/881 Closes #901 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
-rw-r--r--locales/index.d.ts36
-rw-r--r--packages/backend/migration/1738446745738-add_user_profile_default_cw.js11
-rw-r--r--packages/backend/migration/1738468079662-add_user_profile_default_cw_priority.js13
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts6
-rw-r--r--packages/backend/src/models/UserProfile.ts19
-rw-r--r--packages/backend/src/models/json-schema/user.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts25
-rw-r--r--packages/backend/src/types.ts2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue23
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue22
-rw-r--r--packages/misskey-js/src/autogen/types.ts9
-rw-r--r--sharkey-locales/en-US.yml10
12 files changed, 178 insertions, 7 deletions
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<typeof meta, typeof paramDef> { // 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
<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
</div>
</MkFolder>
+
+ <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
+
+ <MkInput v-model="defaultCW" type="text" manualSave @update:modelValue="save()">
+ <template #label>{{ i18n.ts.defaultCW }}</template>
+ <template #caption>{{ i18n.ts.defaultCWDescription }}</template>
+ </MkInput>
+
+ <MkSelect v-model="defaultCWPriority" :disabled="!defaultCW || !keepCw" @update:modelValue="save()">
+ <template #label>{{ i18n.ts.defaultCWPriority }}</template>
+ <template #caption>{{ i18n.ts.defaultCWPriorityDescription }}</template>
+ <option value="default">{{ i18n.ts._defaultCWPriority.default }}</option>
+ <option value="parent">{{ i18n.ts._defaultCWPriority.parent }}</option>
+ <option value="parentDefault">{{ i18n.ts._defaultCWPriority.parentDefault }}</option>
+ <option value="defaultParent">{{ i18n.ts._defaultCWPriority.defaultParent }}</option>
+ </MkSelect>
</div>
</FormSection>
-
- <MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
</div>
</template>
@@ -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<string, never>;
@@ -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)"