diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-12-12 17:38:29 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-12-12 17:38:29 +0000 |
| commit | b266a5f9f65332dd147d60e863ec7fc809f79920 (patch) | |
| tree | cbbe19f77214d5ddf9cac1a2e642216480d329ab | |
| parent | update css variable references (diff) | |
| parent | merge: Add "enable RSS" user privacy toggle (resolves #826) (!806) (diff) | |
| download | sharkey-b266a5f9f65332dd147d60e863ec7fc809f79920.tar.gz sharkey-b266a5f9f65332dd147d60e863ec7fc809f79920.tar.bz2 sharkey-b266a5f9f65332dd147d60e863ec7fc809f79920.zip | |
Merge branch 'develop' into feature/2024.10
20 files changed, 93 insertions, 13 deletions
diff --git a/locales/index.d.ts b/locales/index.d.ts index 8775a1ee8d..5b0ae8b904 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10978,6 +10978,14 @@ export interface Locale extends ILocale { */ "makeIndexableDescription": string; /** + * Enable RSS feed + */ + "enableRss": string; + /** + * Generate an RSS feed containing your basic profile details and public notes. Users can subscribe to the feed without a follow request or approval. + */ + "enableRssDescription": string; + /** * Require approval for new users */ "approvalRequiredForSignup": string; diff --git a/packages/backend/migration/1733748798177-add_user_enableRss.js b/packages/backend/migration/1733748798177-add_user_enableRss.js new file mode 100644 index 0000000000..64662ca7b8 --- /dev/null +++ b/packages/backend/migration/1733748798177-add_user_enableRss.js @@ -0,0 +1,13 @@ +export class AddUserEnableRss1733748798177 { + name = 'AddUserEnableRss1733748798177' + + async up(queryRunner) { + // Disable by default, then specifically enable for all existing local users. + await queryRunner.query(`ALTER TABLE "user" ADD "enable_rss" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`UPDATE "user" SET "enable_rss" = true WHERE host IS NULL;`) + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "enable_rss"`); + } +} diff --git a/packages/backend/migration/1733754069260-alter_user_hideOnlineStatus_default_true.js b/packages/backend/migration/1733754069260-alter_user_hideOnlineStatus_default_true.js new file mode 100644 index 0000000000..c0db48ceea --- /dev/null +++ b/packages/backend/migration/1733754069260-alter_user_hideOnlineStatus_default_true.js @@ -0,0 +1,11 @@ +export class AlterUserHideOnlineStatusDefaultTrue1733754069260 { + name = 'AlterUserHideOnlineStatusDefaultTrue1733754069260' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "hideOnlineStatus" SET DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "hideOnlineStatus" SET DEFAULT false`); + } +} diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index a1e23a49c1..0ad448e95f 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -135,6 +135,7 @@ export class SignupService { isRoot: isTheFirstUser, approved: isTheFirstUser || (opts.approved ?? !this.meta.approvalRequiredForSignup), signupReason: reason, + enableRss: false, })); await transactionalEntityManager.save(new MiUserKeypair({ diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 1530d91532..dfe7a259c4 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -98,6 +98,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser { approved: true, signupReason: null, noindex: false, + enableRss: true, ...override, }; } @@ -214,6 +215,7 @@ function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<' isAdmin: false, isSystem: false, isSilenced: user.isSilenced, + enableRss: true, ...override, }; } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 11c88c63a4..fb706a775f 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -531,8 +531,10 @@ export class ApRendererService { discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, + hideOnlineStatus: user.hideOnlineStatus, noindex: user.noindex, indexable: !user.noindex, + enableRss: user.enableRss, speakAsCat: user.speakAsCat, attachment: attachment.length ? attachment : undefined, }; diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 45ca495145..d7b6fc6589 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -567,8 +567,10 @@ const extension_context_definition = { speakAsCat: 'firefish:speakAsCat', // Sharkey sharkey: 'https://joinsharkey.org/ns#', + hideOnlineStatus: 'sharkey:hideOnlineStatus', backgroundUrl: 'sharkey:backgroundUrl', listenbrainz: 'sharkey:listenbrainz', + enableRss: 'sharkey:enableRss', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', } satisfies Context; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index d32811c7e5..5c71dbc626 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -391,10 +391,13 @@ export class ApPersonService implements OnModuleInit { lastFetchedAt: new Date(), name: truncate(person.name, nameLength), noindex: (person as any).noindex ?? false, + enableRss: person.enableRss === true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo, movedAt: person.movedTo ? new Date() : null, alsoKnownAs: person.alsoKnownAs, + // We use "!== false" to handle incorrect types, missing / null values, and "default to true" logic. + hideOnlineStatus: person.hideOnlineStatus !== false, isExplorable: person.discoverable, username: person.preferredUsername, approved: true, @@ -593,9 +596,12 @@ export class ApPersonService implements OnModuleInit { isCat: (person as any).isCat === true, speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true, noindex: (person as any).noindex ?? false, + enableRss: person.enableRss === true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, + // We use "!== false" to handle incorrect types, missing / null values, and "default to true" logic. + hideOnlineStatus: person.hideOnlineStatus !== false, isExplorable: person.discoverable, ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(() => ({}))), } as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index e61d89f9db..d67f8cf62e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -218,7 +218,9 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; + hideOnlineStatus?: boolean; noindex?: boolean; + enableRss?: boolean; listenbrainz?: string; backgroundUrl?: string; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 4f6b412609..6bfe865038 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -539,6 +539,7 @@ export class UserEntityService implements OnModuleInit { isBot: user.isBot, isCat: user.isCat, noindex: user.noindex, + enableRss: user.enableRss, isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), speakAsCat: user.speakAsCat ?? false, approved: user.approved, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index a4481a1f81..3a825d36a7 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -32,7 +32,7 @@ export class MiUser { public lastActiveDate: Date | null; @Column('boolean', { - default: false, + default: true, }) public hideOnlineStatus: boolean; @@ -160,7 +160,7 @@ export class MiUser { length: 128, nullable: true, }) public backgroundBlurhash: string | null; - + @Column('jsonb', { default: [], }) @@ -328,6 +328,17 @@ export class MiUser { }) public signupReason: string | null; + /** + * True if profile RSS feeds are enabled for this user. + * Enabled by default (opt-out) for existing users, to avoid breaking any existing feeds. + * Disabled by default (opt-in) for newly created users, for privacy. + */ + @Column('boolean', { + name: 'enable_rss', + default: true, + }) + public enableRss: boolean; + constructor(data: Partial<MiUser>) { if (data == null) return; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 46aa06c392..f953008b3f 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -130,6 +130,10 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: false, }, + enableRss: { + type: 'boolean', + nullable: false, optional: false, + }, isBot: { type: 'boolean', nullable: false, optional: true, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 832bf1418d..fe28b50338 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -198,6 +198,7 @@ export const paramDef = { requireSigninToViewContents: { type: 'boolean' }, makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true }, makeNotesHiddenBefore: { type: 'integer', nullable: true }, + enableRss: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, speakAsCat: { type: 'boolean' }, @@ -352,6 +353,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.noindex === 'boolean') updates.noindex = ps.noindex; + if (typeof ps.enableRss === 'boolean') updates.enableRss = ps.enableRss; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; @@ -619,12 +621,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // these two methods need to be kept in sync with // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial<MiUser>): boolean { - for (const field of ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs'] as (keyof MiUser)[]) { + const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss']; + for (const field of basicFields) { if ((field in newUser) && oldUser[field] !== newUser[field]) { return true; } } - for (const arrayField of ['emojis', 'tags'] as (keyof MiUser)[]) { + + const arrayFields: (keyof MiUser)[] = ['emojis', 'tags']; + for (const arrayField of arrayFields) { if ((arrayField in newUser) !== (arrayField in oldUser)) { return true; } @@ -634,7 +639,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { return true; } - if (oldArray.join("\0") !== newArray.join("\0")) { + if (oldArray.join('\0') !== newArray.join('\0')) { return true; } } @@ -642,12 +647,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial<MiUserProfile>): boolean { - for (const field of ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'] as (keyof MiUserProfile)[]) { + const basicFields: (keyof MiUserProfile)[] = ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz']; + for (const field of basicFields) { if ((field in newProfile) && oldProfile[field] !== newProfile[field]) { return true; } } - for (const arrayField of ['fields'] as (keyof MiUserProfile)[]) { + + const arrayFields: (keyof MiUserProfile)[] = ['fields']; + for (const arrayField of arrayFields) { if ((arrayField in newProfile) !== (arrayField in oldProfile)) { return true; } @@ -657,7 +665,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { return true; } - if (oldArray.join("\0") !== newArray.join("\0")) { + if (oldArray.join('\0') !== newArray.join('\0')) { return true; } } diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 2997121cfd..e59314bf55 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -530,6 +530,7 @@ export class ClientServerService { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, + enableRss: true, }); return user && await this.feedService.packFeed(user); diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index b7278ac742..0bb1450f87 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </p> <footer> <img class="icon" :src="flash.user.avatarUrl"/> - <p>{{ userName(flash.user) }}</p> + <MkUserName :key="flash.user.id" :user="flash.user"/> </footer> </article> </MkA> @@ -23,7 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; import * as Misskey from 'misskey-js'; -import { userName } from '@/filters/user.js'; const props = defineProps<{ flash: Misskey.entities.Flash; diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue index 35a37a1f7d..29fdc91a2b 100644 --- a/packages/frontend/src/components/MkPagePreview.vue +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> <footer> <img v-if="page.user.avatarUrl" class="icon" :src="page.user.avatarUrl"/> - <p>{{ userName(page.user) }}</p> + <MkUserName :key="page.user.id" :user="page.user"/> </footer> </article> </MkA> @@ -30,7 +30,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; import * as Misskey from 'misskey-js'; -import { userName } from '@/filters/user.js'; import MediaImage from '@/components/MkMediaImage.vue'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index bc998d6158..fb4a2b1c78 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -44,7 +44,7 @@ import MkFolder from '@/components/MkFolder.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; const isLocked = ref(false); -const hideOnlineStatus = ref(false); +const hideOnlineStatus = ref(true); const noCrawle = ref(false); watch([isLocked, hideOnlineStatus, noCrawle], () => { diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 78df0faf4d..790f9e44e2 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -43,6 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.makeExplorable }} <template #caption>{{ i18n.ts.makeExplorableDescription }}</template> </MkSwitch> + <MkSwitch v-model="enableRss" @update:modelValue="save()"> + {{ i18n.ts.enableRss }} + <template #caption>{{ i18n.ts.enableRssDescription }}</template> + </MkSwitch> <FormSection> <template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template> @@ -180,6 +184,7 @@ const isLocked = ref($i.isLocked); const autoAcceptFollowed = ref($i.autoAcceptFollowed); const noCrawle = ref($i.noCrawle); const noindex = ref($i.noindex); +const enableRss = ref($i.enableRss); const isExplorable = ref($i.isExplorable); const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false); const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null); @@ -237,6 +242,7 @@ function save() { autoAcceptFollowed: !!autoAcceptFollowed.value, noCrawle: !!noCrawle.value, noindex: !!noindex.value, + enableRss: !!enableRss.value, isExplorable: !!isExplorable.value, requireSigninToViewContents: !!requireSigninToViewContents.value, makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 22e4a4a8c1..888e46e008 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3931,6 +3931,7 @@ export type components = { /** @default false */ isSystem?: boolean; noindex: boolean; + enableRss: boolean; isBot?: boolean; isCat?: boolean; speakAsCat?: boolean; @@ -21482,6 +21483,7 @@ export type operations = { requireSigninToViewContents?: boolean; makeNotesFollowersOnlyBefore?: number | null; makeNotesHiddenBefore?: number | null; + enableRss?: boolean; isBot?: boolean; isCat?: boolean; speakAsCat?: boolean; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index f15646a156..4e462edda7 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -88,6 +88,8 @@ searchEngineCustomURIDescription: "The custom URI must be input in the format li searchEngineCusomURI: "Custom URI" makeIndexable: "Make public notes not indexable" makeIndexableDescription: "Stop note search from indexing your public notes." +enableRss: "Enable RSS feed" +enableRssDescription: "Generate an RSS feed containing your basic profile details and public notes. Users can subscribe to the feed without a follow request or approval." sendErrorReportsDescription: "When turned on, detailed error information will be shared with Sharkey when a problem occurs, helping to improve the quality of Sharkey.\nThis will include information such the version of your OS, what browser you're using, your activity in Sharkey, etc." noInquiryUrlWarning: "Contact URL is not set." misskeyUpdated: "Sharkey has been updated!" |