From 0a8f8e77621a82ff4a79bc7d475aadf7a6a48f3c Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Fri, 6 Mar 2026 15:33:11 -0500 Subject: [PATCH] feat: add localHost option to allow instances to have seperate user hosts from the instance url --- .config/cypress-devcontainer.yml | 5 ++++ .config/docker_example.yml | 5 ++++ .config/example.yml | 5 ++++ .devcontainer/devcontainer.yml | 5 ++++ CHANGELOG.md | 1 + packages/backend/src/config.ts | 4 +++ packages/backend/src/core/MfmService.ts | 2 +- .../src/core/RemoteUserResolveService.ts | 2 +- packages/backend/src/core/UtilityService.ts | 5 ++-- .../activitypub/models/ApPersonService.ts | 3 +- .../src/core/entities/UserEntityService.ts | 4 +-- packages/backend/src/server/ServerService.ts | 2 +- .../src/server/WellKnownServerService.ts | 4 +-- .../backend/src/server/web/FeedService.ts | 2 +- .../src/server/web/HtmlTemplateService.ts | 1 + packages/backend/src/server/web/views/_.ts | 1 + .../src/server/web/views/base-embed.tsx | 1 + .../backend/src/server/web/views/base.tsx | 1 + .../frontend-embed/src/components/EmAcct.vue | 2 +- .../src/components/EmMention.vue | 2 +- .../frontend-embed/src/components/EmMfm.ts | 4 +-- packages/frontend-shared/js/config.ts | 2 ++ packages/frontend/src/accounts.ts | 20 ++++++------- .../src/components/MkAccountMoved.vue | 2 +- .../src/components/MkFollowButton.vue | 4 +-- .../frontend/src/components/MkMention.vue | 2 +- .../frontend/src/components/MkPostForm.vue | 8 +++--- .../src/components/MkSignin.input.vue | 4 +-- .../src/components/MkUserSelectDialog.vue | 4 +-- .../frontend/src/components/global/MkAcct.vue | 4 +-- .../frontend/src/components/global/MkMfm.ts | 4 +-- packages/frontend/src/pages/chat/XMessage.vue | 4 +-- packages/frontend/src/pages/qr.show.vue | 4 +-- packages/frontend/src/pages/search.note.vue | 4 +-- .../src/pages/settings/2fa.qrdialog.vue | 4 +-- packages/frontend/src/pages/theme-editor.vue | 6 ++-- packages/frontend/src/pages/welcome.setup.vue | 4 +-- packages/frontend/src/preferences/manager.ts | 28 +++++++++---------- .../frontend/src/utility/get-note-menu.ts | 4 +-- .../frontend/src/utility/get-user-menu.ts | 6 ++-- 40 files changed, 106 insertions(+), 73 deletions(-) diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 7656526019f..be241855dd3 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -21,6 +21,11 @@ setupPassword: example_password_please_change_this_or_you_will_get_hacked # Final accessible URL seen by a user. url: 'http://misskey.local' +# Use a seperate hostname for federation. All users on this +# instance will appear with this host instead of the one with +# the misskey web url if set. +#localHost: 'misskey.local' + # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # URL SETTINGS AFTER THAT! diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 60a314c31ce..6d146d60f05 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -9,6 +9,11 @@ # You can set url from an environment variable instead. url: https://example.tld/ +# Use a seperate hostname for federation. All users on this +# instance will appear with this host instead of the one with +# the misskey web url if set. +#localHost: 'example.tld' + # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # URL SETTINGS AFTER THAT! diff --git a/.config/example.yml b/.config/example.yml index 0aa89406778..b102658feba 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -79,6 +79,11 @@ # Final accessible URL seen by a user. url: https://example.tld/ +# Use a seperate hostname for federation. All users on this +# instance will appear with this host instead of the one with +# the misskey web url if set. +#localHost: 'example.tld' + # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # URL SETTINGS AFTER THAT! diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index cd99d063d59..b9c6c224ebf 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -8,6 +8,11 @@ # Final accessible URL seen by a user. url: http://127.0.0.1:3000/ +# Use a seperate hostname for federation. All users on this +# instance will appear with this host instead of the one with +# the misskey web url if set. +#localHost: '127.0.0.1:3000' + # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # URL SETTINGS AFTER THAT! diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa5b3b879b..5079f498ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,7 @@ ### General - 依存関係の更新 +- Added `localHost` option to allow misskey instances to federate their users on a different host than the one from the accessable web url. ### Client - Enhance: アプリ内ウィンドウの初期サイズを画面サイズに応じて自動で調整するように diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index d67fe6fc6fa..4654d3b8af6 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -25,6 +25,7 @@ type RedisOptionsSource = Partial & { */ type Source = { url?: string; + localHost?: string; port?: number; socket?: string; trustProxy?: FastifyServerOptions['trustProxy']; @@ -119,6 +120,7 @@ type Source = { export type Config = { url: string; + localHost: string; port: number; socket: string | undefined; trustProxy: NonNullable; @@ -259,6 +261,7 @@ export function loadConfig(): Config { const hostname = url.hostname; const scheme = url.protocol.replace(/:$/, ''); const wsScheme = scheme.replace('http', 'ws'); + const localHost = config.localHost ?? host; const dbDb = config.db.db ?? process.env.DATABASE_DB ?? ''; const dbUser = config.db.user ?? process.env.DATABASE_USER ?? ''; @@ -275,6 +278,7 @@ export function loadConfig(): Config { publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, setupPassword: config.setupPassword, url: url.origin, + localHost, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, trustProxy: config.trustProxy ?? [ diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 274966d9213..a0735552e1a 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -386,7 +386,7 @@ export class MfmService { const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); const href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) - : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`; + : `${this.config.url}/${acct.endsWith(`@${this.config.localHost}`) ? acct.substring(0, acct.length - this.config.localHost.length - 1) : acct}`; try { const url = new URL(href); return `${escapeHtml(acct)}`; diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index a2f1b73cdbd..b2b7f4418bc 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -56,7 +56,7 @@ export class RemoteUserResolveService { host = this.utilityService.toPuny(host); - if (host === this.utilityService.toPuny(this.config.host)) { + if ((host === this.utilityService.toPuny(this.config.localHost)) || (host === this.utilityService.toPuny(this.config.host))) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index e3ceebccaeb..6601fa9bd3c 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -26,13 +26,14 @@ export class UtilityService { @bindThis public getFullApAccount(username: string, host: string | null): string { - return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`; + return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.localHost)}`; } @bindThis public isSelfHost(host: string | null): boolean { if (host == null) return true; - return this.toPuny(this.config.host) === this.toPuny(host); + return (this.toPuny(this.config.localHost) === this.toPuny(host)) || + (this.toPuny(this.config.host) === this.toPuny(host)); } @bindThis diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 39396cb7411..9175865aed1 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -305,7 +305,8 @@ export class ApPersonService implements OnModuleInit { if (typeof uri !== 'string') throw new Error('uri is not string'); const host = this.utilityService.punyHost(uri); - if (host === this.utilityService.toPuny(this.config.host)) { + if ((host === this.utilityService.toPuny(this.config.localHost)) || + (host === this.utilityService.toPuny(this.config.host))) { throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 996f0bad2e8..521952a14fc 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -384,10 +384,10 @@ export class UserEntityService implements OnModuleInit { @bindThis public getIdenticonUrl(user: MiUser): string { - if ((user.host == null || user.host === this.config.host) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合 + if ((user.host == null || user.host === this.config.host || user.host === this.config.localHost) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合 return this.meta.iconUrl; } else { - return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`; + return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.localHost}`; } } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 23ead0febac..eeda9cfc1a1 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -213,7 +213,7 @@ export class ServerService implements OnApplicationShutdown { const user = await this.usersRepository.findOne({ where: { usernameLower: username.toLowerCase(), - host: (host == null) || (host === this.config.host) ? IsNull() : host, + host: (host == null) || (host === this.config.host) || (host === this.config.localHost) ? IsNull() : host, isSuspended: false, }, }); diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index ebfd1a421d9..56e320bc527 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -137,7 +137,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { resource)); const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => - !acct.host || acct.host === this.config.host.toLowerCase() ? { + !acct.host || acct.host === this.config.host.toLowerCase() || acct.host === this.config.localHost.toLowerCase() ? { usernameLower: acct.username.toLowerCase(), host: IsNull(), isSuspended: false, @@ -162,7 +162,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { return; } - const subject = `acct:${user.username}@${this.config.host}`; + const subject = `acct:${user.username}@${this.config.localHost}`; const self = { rel: 'self', type: 'application/activity+json', diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index eae76453212..077c090ad7a 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -60,7 +60,7 @@ export class FeedService { const feed = new Feed({ id: author.link, - title: `${author.name} (@${user.username}@${this.config.host})`, + title: `${author.name} (@${user.username}@${this.config.localHost})`, updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, generator: 'Misskey', description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 2859b2b9852..9c39fcd14b7 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -165,6 +165,7 @@ export class HtmlTemplateService { infoImageUrl: this.meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: this.meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', instanceUrl: this.config.url, + localHost: this.config.localHost, metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)), now: Date.now(), federationEnabled: this.meta.federation !== 'none', diff --git a/packages/backend/src/server/web/views/_.ts b/packages/backend/src/server/web/views/_.ts index f9b290b13ab..83e8d9c28b1 100644 --- a/packages/backend/src/server/web/views/_.ts +++ b/packages/backend/src/server/web/views/_.ts @@ -40,6 +40,7 @@ export type CommonData = MinimumCommonData & { infoImageUrl: string; notFoundImageUrl: string; instanceUrl: string; + localHost: string; now: number; federationEnabled: boolean; frontendViteFiles: ViteFiles | null; diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx index a656bb28a71..cb2dfbe7e6f 100644 --- a/packages/backend/src/server/web/views/base-embed.tsx +++ b/packages/backend/src/server/web/views/base-embed.tsx @@ -41,6 +41,7 @@ export function BaseEmbed(props: PropsWithChildren + diff --git a/packages/backend/src/server/web/views/base.tsx b/packages/backend/src/server/web/views/base.tsx index 828cecad7c9..18236948ff4 100644 --- a/packages/backend/src/server/web/views/base.tsx +++ b/packages/backend/src/server/web/views/base.tsx @@ -43,6 +43,7 @@ export function Layout(props: PropsWithChildren + diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue index ff794d9b6e6..ea6ec104429 100644 --- a/packages/frontend-embed/src/components/EmAcct.vue +++ b/packages/frontend-embed/src/components/EmAcct.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 706ea074170..2423703e12d 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -6,7 +6,7 @@ import { h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; -import { host } from '@@/js/config.js'; +import { localHost } from '@@/js/config.js'; import type { VNode, SetupContext } from 'vue'; import type { MkABehavior } from '@/components/global/MkA.vue'; import MkUrl from '@/components/global/MkUrl.vue'; @@ -369,7 +369,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext { - const localUrl = `${url}/chat/messages/${props.message.id}`; + const localHost = `${url}/chat/messages/${props.message.id}`; const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkAbuseReportWindow.vue').then(x => x.default), { user: props.message.fromUser!, - initialComment: `${localUrl}\n-----\n`, + initialComment: `${localHost}\n-----\n`, }, { closed: () => dispose(), }); diff --git a/packages/frontend/src/pages/qr.show.vue b/packages/frontend/src/pages/qr.show.vue index 28f80e09636..44f3fcc4940 100644 --- a/packages/frontend/src/pages/qr.show.vue +++ b/packages/frontend/src/pages/qr.show.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only import tinycolor from 'tinycolor2'; import QRCodeStyling from 'qr-code-styling'; import { computed, ref, shallowRef, watch, onMounted, onUnmounted, useTemplateRef } from 'vue'; -import { url, host } from '@@/js/config.js'; +import { url, localHost } from '@@/js/config.js'; import type { Directive } from 'vue'; import { instance } from '@/instance.js'; import { ensureSignin } from '@/i.js'; @@ -40,7 +40,7 @@ import { i18n } from '@/i18n.js'; const $i = ensureSignin(); -const acct = computed(() => `@${$i.username}@${host}`); +const acct = computed(() => `@${$i.username}@${localHost}`); const userProfileUrl = computed(() => userPage($i, undefined, true)); const shareData = computed(() => ({ title: i18n.tsx._qr.shareTitle({ name: userName($i), acct: acct.value }), diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index c1b96f89b98..57370c0aa5d 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only