enhance: 非ログイン時にはMisskey Hub経由で別サーバーに遷移できるように
This commit is contained in:
parent
7beb4ed131
commit
51d62a0a4f
|
|
@ -724,6 +724,10 @@ export interface Locale extends ILocale {
|
|||
* リモートで表示
|
||||
*/
|
||||
"showOnRemote": string;
|
||||
/**
|
||||
* リモートで続行
|
||||
*/
|
||||
"continueOnRemote": string;
|
||||
/**
|
||||
* 全般
|
||||
*/
|
||||
|
|
@ -1897,9 +1901,13 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"onlyOneFileCanBeAttached": string;
|
||||
/**
|
||||
* 続行する前に、サインアップまたはサインインが必要です
|
||||
* 続行する前に、登録またはログインが必要です
|
||||
*/
|
||||
"signinRequired": string;
|
||||
/**
|
||||
* 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
|
||||
*/
|
||||
"signinOrContinueOnRemote": string;
|
||||
/**
|
||||
* 招待
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ addAccount: "アカウントを追加"
|
|||
reloadAccountsList: "アカウントリストの情報を更新"
|
||||
loginFailed: "ログインに失敗しました"
|
||||
showOnRemote: "リモートで表示"
|
||||
continueOnRemote: "リモートで続行"
|
||||
general: "全般"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
|
|
@ -470,7 +471,8 @@ quoteQuestion: "引用として添付しますか?"
|
|||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
||||
signinRequired: "続行する前に、登録またはログインが必要です"
|
||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
|
||||
invitations: "招待"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しています"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { Config } from '@/config.js';
|
|||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
|
@ -31,6 +32,11 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
|||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
|
|
@ -100,6 +106,11 @@ export class ClientServerService {
|
|||
private feedService: FeedService,
|
||||
private roleService: RoleService,
|
||||
private clientLoggerService: ClientLoggerService,
|
||||
private apResolverService: ApResolverService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private apPersonService: ApPersonService,
|
||||
private apNoteService: ApNoteService,
|
||||
private utilityService: UtilityService,
|
||||
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
|
|
@ -745,6 +756,63 @@ export class ClientServerService {
|
|||
return await reply.view('flush');
|
||||
});
|
||||
|
||||
// マストドン互換・照会してリダイレクトするエンドポイント
|
||||
fastify.get('/authorize_interaction', async (request, reply) => {
|
||||
const { uri } = request.query as { uri?: string; };
|
||||
|
||||
if (!uri) {
|
||||
reply.redirect(302, '/');
|
||||
return;
|
||||
}
|
||||
|
||||
// ブロックしてたら中断
|
||||
const fetchedMeta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
||||
reply.redirect(302, '/');
|
||||
return;
|
||||
}
|
||||
|
||||
const [localUser, localNote] = await Promise.all([
|
||||
this.apDbResolverService.getUserFromApId(uri),
|
||||
this.apDbResolverService.getNoteFromApId(uri),
|
||||
]);
|
||||
|
||||
if (localUser != null) {
|
||||
reply.redirect(302, `/@${localUser.username}${localUser.host == null ? '' : '@' + localUser.host}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (localNote != null) {
|
||||
reply.redirect(302, `/notes/${localNote.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// リモートから一旦オブジェクトフェッチ
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
const object = await resolver.resolve(uri) as any;
|
||||
|
||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||
// これはDBに存在する可能性があるため再度DB検索
|
||||
if (uri !== object.id) {
|
||||
const [remoteUser, remoteNote] = await Promise.all([
|
||||
this.apDbResolverService.getUserFromApId(object.id),
|
||||
this.apDbResolverService.getNoteFromApId(object.id),
|
||||
]);
|
||||
|
||||
if (remoteUser != null) {
|
||||
reply.redirect(302, `/@${remoteUser.username}${remoteUser.host == null ? '' : '@' + remoteUser.host}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteNote != null) {
|
||||
reply.redirect(302, `/notes/${remoteNote.id}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reply.redirect(302, '/');
|
||||
});
|
||||
|
||||
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
|
||||
fastify.get('/streaming', async (request, reply) => {
|
||||
reply.code(503);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { host } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
|
|||
const wait = ref(false);
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
if (props.user.isFollowing == null && $i) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
})
|
||||
|
|
@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
|
|||
}
|
||||
|
||||
async function onClick() {
|
||||
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
|
||||
|
||||
wait.value = true;
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="misskeyHub" class="_gaps_m">
|
||||
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openMisskeyHub(misskeyHub)">
|
||||
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
|
||||
</MkButton>
|
||||
<div :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
|
|
@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.retry }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="user && user.securityKeys" class="or-hr">
|
||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
||||
<div v-if="user && user.securityKeys" :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
|
||||
|
|
@ -53,6 +61,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
|||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import type { MisskeyHubOptions } from '@/scripts/please-login.js';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -77,22 +86,16 @@ const emit = defineEmits<{
|
|||
(ev: 'login', v: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
withAvatar?: boolean;
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
misskeyHub?: MisskeyHubOptions,
|
||||
}>(), {
|
||||
withAvatar: true,
|
||||
autoSet: false,
|
||||
message: '',
|
||||
misskeyHub: undefined,
|
||||
});
|
||||
|
||||
function onUsernameChange(): void {
|
||||
|
|
@ -219,6 +222,28 @@ function resetPassword(): void {
|
|||
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function openMisskeyHub(hubOptions: MisskeyHubOptions): void {
|
||||
switch (hubOptions.type) {
|
||||
case 'web': {
|
||||
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(hubOptions.path)}`, '_blank', 'noopener');
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
const rawParams = { ...hubOptions.params };
|
||||
// undefinedの値をすべて除去
|
||||
Object.keys(rawParams).forEach(key => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (rawParams[key] === undefined) {
|
||||
delete rawParams[key];
|
||||
}
|
||||
});
|
||||
const params = new URLSearchParams(rawParams);
|
||||
window.open(`https://misskey-hub.net/share/?${params.toString()}`, '_blank', 'noopener');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -231,4 +256,25 @@ function resetPassword(): void {
|
|||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.orHr {
|
||||
position: relative;
|
||||
margin: .4em auto;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
.orMsg {
|
||||
position: absolute;
|
||||
top: -.6em;
|
||||
display: inline-block;
|
||||
padding: 0 1em;
|
||||
background: var(--panel);
|
||||
font-size: 0.8em;
|
||||
color: var(--fgOnPanel);
|
||||
margin: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
||||
<MkSignin :autoSet="autoSet" :message="message" :misskeyHub="misskeyHub" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import type { MisskeyHubOptions } from '@/scripts/please-login.js';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
|
|||
withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
misskeyHub?: MisskeyHubOptions,
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
message: '',
|
||||
misskeyHub: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
||||
<MkFollowButton :class="$style.follow" :user="user" mini/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
|||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
|
@ -592,6 +593,15 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
|
|||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
|
||||
type: 'share',
|
||||
params: {
|
||||
text: props.initialText ?? props.initialNote.text,
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||
},
|
||||
} : undefined));
|
||||
|
||||
showMovedDialog();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
|
|
@ -114,6 +115,8 @@ function shareWithNote() {
|
|||
}
|
||||
|
||||
function like() {
|
||||
pleaseLogin();
|
||||
|
||||
os.apiWithDialog('flash/like', {
|
||||
flashId: flash.value.id,
|
||||
}).then(() => {
|
||||
|
|
@ -123,6 +126,8 @@ function like() {
|
|||
}
|
||||
|
||||
async function unlike() {
|
||||
pleaseLogin();
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div v-if="$i" class="actions">
|
||||
<div class="actions">
|
||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||
copyToClipboard(`${url}/${canonical}`);
|
||||
},
|
||||
}, {
|
||||
}, ...($i ? [{
|
||||
icon: 'ti ti-mail',
|
||||
text: i18n.ts.sendMessage,
|
||||
action: () => {
|
||||
|
|
@ -250,7 +250,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
},
|
||||
}));
|
||||
},
|
||||
}] as any;
|
||||
}] : [])] as any;
|
||||
|
||||
if ($i && meId !== user.id) {
|
||||
if (iAmModerator) {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,21 @@ import { $i } from '@/account.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export function pleaseLogin(path?: string) {
|
||||
export type MisskeyHubOptions = {
|
||||
type: 'web';
|
||||
path: string;
|
||||
} | {
|
||||
type: 'share';
|
||||
params: Record<string, string>;
|
||||
};
|
||||
|
||||
export function pleaseLogin(path?: string, misskeyHub?: MisskeyHubOptions) {
|
||||
if ($i) return;
|
||||
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
|
||||
autoSet: true,
|
||||
message: i18n.ts.signinRequired,
|
||||
message: misskeyHub ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
|
||||
misskeyHub,
|
||||
}, {
|
||||
cancelled: () => {
|
||||
if (path) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ const devConfig = {
|
|||
},
|
||||
'/url': 'http://localhost:3000',
|
||||
'/proxy': 'http://localhost:3000',
|
||||
'/authorize_interaction': 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue