((resolve, reject) => {
+ socket.on('error', () => resolve(false));
+ socket.on('unexpected-response', () => resolve(false));
+ setTimeout(() => resolve(true), 3000);
+ });
+
+ socket.close();
+ assert.strictEqual(established, false);
+
+ const fired = await waitFire(
+ { token: application2 }, 'hybridTimeline',
+ () => api('notes/create', { text: 'Hello, world!' }, ayano),
+ msg => msg.type === 'note' && msg.body.userId === ayano.id,
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
/*
describe('Hashtag Timeline', () => {
diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts
new file mode 100644
index 000000000..14e32e162
--- /dev/null
+++ b/packages/backend/test/e2e/well-known.ts
@@ -0,0 +1,111 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { host, origin, relativeFetch, signup, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+import type * as misskey from 'misskey-js';
+
+describe('.well-known', () => {
+ let app: INestApplicationContext;
+ let alice: misskey.entities.User;
+
+ beforeAll(async () => {
+ app = await startServer();
+
+ alice = await signup({ username: 'alice' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ test('nodeinfo', async () => {
+ const res = await relativeFetch('.well-known/nodeinfo');
+ assert.ok(res.ok);
+ assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
+
+ const nodeInfo = await res.json();
+ assert.deepStrictEqual(nodeInfo, {
+ links: [{
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+ href: `${origin}/nodeinfo/2.1`,
+ }, {
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+ href: `${origin}/nodeinfo/2.0`,
+ }],
+ });
+ });
+
+ test('webfinger', async () => {
+ const preflight = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`, {
+ method: 'options',
+ headers: {
+ 'Access-Control-Request-Method': 'GET',
+ Origin: 'http://example.com',
+ },
+ });
+ assert.ok(preflight.ok);
+ assert.strictEqual(preflight.headers.get('Access-Control-Allow-Headers'), 'Accept');
+
+ const res = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`);
+ assert.ok(res.ok);
+ assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
+ assert.strictEqual(res.headers.get('Access-Control-Expose-Headers'), 'Vary');
+ assert.strictEqual(res.headers.get('Vary'), 'Accept');
+
+ const webfinger = await res.json();
+
+ assert.deepStrictEqual(webfinger, {
+ subject: `acct:alice@${host}`,
+ links: [{
+ rel: 'self',
+ type: 'application/activity+json',
+ href: `${origin}/users/${alice.id}`,
+ }, {
+ rel: 'http://webfinger.net/rel/profile-page',
+ type: 'text/html',
+ href: `${origin}/@alice`,
+ }, {
+ rel: 'http://ostatus.org/schema/1.0/subscribe',
+ template: `${origin}/authorize-follow?acct={uri}`,
+ }],
+ });
+ });
+
+ test('host-meta', async () => {
+ const res = await relativeFetch('.well-known/host-meta');
+ assert.ok(res.ok);
+ assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
+ });
+
+ test('host-meta.json', async () => {
+ const res = await relativeFetch('.well-known/host-meta.json');
+ assert.ok(res.ok);
+ assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
+
+ const hostMeta = await res.json();
+ assert.deepStrictEqual(hostMeta, {
+ links: [{
+ rel: 'lrdd',
+ type: 'application/jrd+json',
+ template: `${origin}/.well-known/webfinger?resource={uri}`,
+ }],
+ });
+ });
+
+ test('oauth-authorization-server', async () => {
+ const res = await relativeFetch('.well-known/oauth-authorization-server');
+ assert.ok(res.ok);
+ assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*');
+
+ const serverInfo = await res.json() as any;
+ assert.strictEqual(serverInfo.issuer, origin);
+ assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`);
+ assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`);
+ });
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 97118d73c..46b8ea9cd 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -6,6 +6,7 @@
import * as assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { isAbsolute, basename } from 'node:path';
+import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch';
@@ -25,6 +26,8 @@ interface UserToken {
const config = loadConfig();
export const port = config.port;
+export const origin = config.url;
+export const host = new URL(config.url).host;
export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
@@ -126,6 +129,15 @@ export const post = async (user: UserToken, params?: misskey.Endpoints['notes/cr
return res.body ? res.body.createdNote : null;
};
+export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
+ const res = await api('miauth/gen-token', {
+ session: randomUUID(),
+ permission: permissions,
+ }, user);
+
+ return (res.body as misskey.entities.MiauthGenTokenResponse).token;
+};
+
// 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts
export const hiddenNote = (note: any): any => {
const temp = {
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index b0ff06bd3..3edd30bc3 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -38,14 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index fa863cc8e..ac25c1099 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -224,7 +224,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -307,7 +307,7 @@ const renotesPagination = computed(() => ({
params: {
noteId: appearNote.value.id,
},
-} satisfies Paging));
+}));
const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
@@ -316,7 +316,7 @@ const reactionsPagination = computed(() => ({
noteId: appearNote.value.id,
type: reactionTabType.value,
},
-} satisfies Paging));
+}));
useNoteCapture({
rootEl: el,
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5e7ca5539..3aacf4c2d 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -126,6 +127,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
const modal = inject('modal');
@@ -182,6 +184,8 @@ const poll = ref<{
const useCw = ref(!!props.initialCw);
const showPreview = ref(defaultStore.state.showPreview);
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
+const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction);
+watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
const cw = ref(props.initialCw ?? null);
const localOnly = ref(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
@@ -863,6 +867,14 @@ async function insertEmoji(ev: MouseEvent) {
);
}
+async function insertMfmFunction(ev: MouseEvent) {
+ mfmFunctionPicker(
+ ev.currentTarget ?? ev.target,
+ textareaEl.value,
+ text,
+ );
+}
+
function showActions(ev) {
os.popupMenu(postFormActions.map(action => ({
text: action.title,
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 8de226802..250b7b96d 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -198,7 +198,8 @@ if (!mock) {
}
.limitWidth {
- max-width: 150px;
+ max-width: 70px;
+ object-fit: contain;
}
.count {
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index f5fa86a90..8e8e26ed5 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.enableAll }}
- {{ i18n.t(`_permissions.${kind}`) }}
+ {{ i18n.t(`_permissions.${kind}`) }}
@@ -54,7 +54,7 @@ const props = withDefaults(defineProps<{
title?: string | null;
information?: string | null;
initialName?: string | null;
- initialPermissions?: string[] | null;
+ initialPermissions?: (typeof Misskey.permissions)[number][] | null;
}>(), {
title: null,
information: null,
@@ -67,16 +67,17 @@ const emit = defineEmits<{
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
}>();
+const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
const dialog = shallowRef>();
const name = ref(props.initialName);
-const permissions = ref({});
+const permissions = ref(>{});
if (props.initialPermissions) {
for (const kind of props.initialPermissions) {
permissions.value[kind] = true;
}
} else {
- for (const kind of Misskey.permissions) {
+ for (const kind of defaultPermissions) {
permissions.value[kind] = false;
}
}
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index d924a54ff..5f3f5b81d 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -37,15 +37,15 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
-const pinnedUsers = { endpoint: 'pinned-users', noPaging: true } satisfies Paging;
+const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive',
origin: 'local',
sort: '+follower',
-} } satisfies Paging;
+} };