Compare commits
8 Commits
46903429c6
...
1681b95694
Author | SHA1 | Date |
---|---|---|
かっこかり | 1681b95694 | |
かっこかり | f5563c8304 | |
おさむのひと | 4ac8aad50a | |
かっこかり | ceb4640669 | |
kakkokari-gtyih | 0f5b8107cc | |
kakkokari-gtyih | 797da3baac | |
kakkokari-gtyih | 489978a2ba | |
kakkokari-gtyih | 69de24d361 |
|
@ -65,6 +65,10 @@ temp
|
|||
tsdoc-metadata.json
|
||||
misskey-assets
|
||||
|
||||
# Vite temporary files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
||||
|
||||
### Client
|
||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||
|
|
|
@ -9477,6 +9477,10 @@ export interface Locale extends ILocale {
|
|||
* Webhookを削除しますか?
|
||||
*/
|
||||
"deleteConfirm": string;
|
||||
/**
|
||||
* スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
|
||||
*/
|
||||
"testRemarks": string;
|
||||
};
|
||||
"_abuseReport": {
|
||||
"_notificationRecipient": {
|
||||
|
|
|
@ -2514,6 +2514,7 @@ _webhookSettings:
|
|||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||
userCreated: "ユーザーが作成されたとき"
|
||||
deleteConfirm: "Webhookを削除しますか?"
|
||||
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
|
||||
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
|
@ -211,6 +212,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
|
|||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
|
@ -359,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
|
@ -503,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$VideoProcessingService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
|
@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
VideoProcessingService,
|
||||
UserWebhookService,
|
||||
SystemWebhookService,
|
||||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
|
@ -791,6 +796,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$VideoProcessingService,
|
||||
$UserWebhookService,
|
||||
$SystemWebhookService,
|
||||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
|
|
|
@ -452,10 +452,15 @@ export class QueueService {
|
|||
|
||||
/**
|
||||
* @see UserWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see UserWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
public userWebhookDeliver(
|
||||
webhook: MiWebhook,
|
||||
type: typeof webhookEventTypes[number],
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
|
@ -468,7 +473,7 @@ export class QueueService {
|
|||
};
|
||||
|
||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
|
@ -479,10 +484,15 @@ export class QueueService {
|
|||
|
||||
/**
|
||||
* @see SystemWebhookDeliverJobData
|
||||
* @see WebhookDeliverProcessorService
|
||||
* @see SystemWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
||||
public systemWebhookDeliver(
|
||||
webhook: MiSystemWebhook,
|
||||
type: SystemWebhookEventType,
|
||||
content: unknown,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
type,
|
||||
content,
|
||||
|
@ -494,7 +504,7 @@ export class QueueService {
|
|||
};
|
||||
|
||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||
attempts: 4,
|
||||
attempts: opts?.attempts ?? 4,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
|
|
|
@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
* SystemWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchSystemWebhooks(params?: {
|
||||
public fetchSystemWebhooks(params?: {
|
||||
ids?: MiSystemWebhook['id'][];
|
||||
isActive?: MiSystemWebhook['isActive'];
|
||||
on?: MiSystemWebhook['on'];
|
||||
|
@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
/**
|
||||
* SystemWebhook をWebhook配送キューに追加する
|
||||
* @see QueueService.systemWebhookDeliver
|
||||
* // TODO: contentの型を厳格化する
|
||||
*/
|
||||
@bindThis
|
||||
public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
|
||||
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||
type: T,
|
||||
content: unknown,
|
||||
) {
|
||||
const webhookEntity = typeof webhook === 'string'
|
||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||
: webhook;
|
||||
if (!webhookEntity || !webhookEntity.isActive) {
|
||||
this.logger.info(`Webhook is not active or not found : ${webhook}`);
|
||||
this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookEntity.on.includes(type)) {
|
||||
this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import { type WebhooksRepository } from '@/models/_.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
|
@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
|
|||
return this.activeWebhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserWebhook の一覧を取得する.
|
||||
*/
|
||||
@bindThis
|
||||
public fetchWebhooks(params?: {
|
||||
ids?: MiWebhook['id'][];
|
||||
isActive?: MiWebhook['active'];
|
||||
on?: MiWebhook['on'];
|
||||
}): Promise<MiWebhook[]> {
|
||||
const query = this.webhooksRepository.createQueryBuilder('webhook');
|
||||
if (params) {
|
||||
if (params.ids && params.ids.length > 0) {
|
||||
query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
|
||||
}
|
||||
if (params.isActive !== undefined) {
|
||||
query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
|
||||
}
|
||||
if (params.on && params.on.length > 0) {
|
||||
query.andWhere(':on <@ webhook.on', { on: params.on });
|
||||
}
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
|
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||
|
||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
|
||||
return {
|
||||
id: 'dummy-abuse-report1',
|
||||
targetUserId: 'dummy-target-user',
|
||||
targetUser: null,
|
||||
reporterId: 'dummy-reporter-user',
|
||||
reporter: null,
|
||||
assigneeId: null,
|
||||
assignee: null,
|
||||
resolved: false,
|
||||
forwarded: false,
|
||||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
return {
|
||||
id: 'dummy-user-1',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 7),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
|
||||
hideOnlineStatus: false,
|
||||
username: 'dummy1',
|
||||
usernameLower: 'dummy1',
|
||||
name: 'DummyUser1',
|
||||
followersCount: 10,
|
||||
followingCount: 5,
|
||||
movedToUri: null,
|
||||
movedAt: null,
|
||||
alsoKnownAs: null,
|
||||
notesCount: 30,
|
||||
avatarId: null,
|
||||
avatar: null,
|
||||
bannerId: null,
|
||||
banner: null,
|
||||
avatarUrl: null,
|
||||
bannerUrl: null,
|
||||
avatarBlurhash: null,
|
||||
bannerBlurhash: null,
|
||||
avatarDecorations: [],
|
||||
tags: [],
|
||||
isSuspended: false,
|
||||
isLocked: false,
|
||||
isBot: false,
|
||||
isCat: true,
|
||||
isRoot: false,
|
||||
isExplorable: true,
|
||||
isHibernated: false,
|
||||
isDeleted: false,
|
||||
emojis: [],
|
||||
host: null,
|
||||
inbox: null,
|
||||
sharedInbox: null,
|
||||
featured: null,
|
||||
uri: null,
|
||||
followersUri: null,
|
||||
token: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||
return {
|
||||
id: 'dummy-note-1',
|
||||
replyId: null,
|
||||
reply: null,
|
||||
renoteId: null,
|
||||
renote: null,
|
||||
threadId: null,
|
||||
text: 'This is a dummy note for testing purposes.',
|
||||
name: null,
|
||||
cw: null,
|
||||
userId: 'dummy-user-1',
|
||||
user: null,
|
||||
localOnly: true,
|
||||
reactionAcceptance: 'likeOnly',
|
||||
renoteCount: 10,
|
||||
repliesCount: 5,
|
||||
clippedCount: 0,
|
||||
reactions: {},
|
||||
visibility: 'public',
|
||||
uri: null,
|
||||
url: null,
|
||||
fileIds: [],
|
||||
attachedFileTypes: [],
|
||||
visibleUserIds: [],
|
||||
mentions: [],
|
||||
mentionedRemoteUsers: '[]',
|
||||
reactionAndUserPairCache: [],
|
||||
emojis: [],
|
||||
tags: [],
|
||||
hasPoll: false,
|
||||
channelId: null,
|
||||
channel: null,
|
||||
userHost: null,
|
||||
replyUserId: null,
|
||||
replyUserHost: null,
|
||||
renoteUserId: null,
|
||||
renoteUserHost: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
|
||||
return {
|
||||
id: note.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
userId: note.userId,
|
||||
user: toPackedUserLite(note.user ?? generateDummyUser()),
|
||||
replyId: note.replyId,
|
||||
renoteId: note.renoteId,
|
||||
isHidden: false,
|
||||
visibility: note.visibility,
|
||||
mentions: note.mentions,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
fileIds: note.fileIds,
|
||||
files: [],
|
||||
tags: note.tags,
|
||||
poll: null,
|
||||
emojis: note.emojis,
|
||||
channelId: note.channelId,
|
||||
channel: note.channel,
|
||||
localOnly: note.localOnly,
|
||||
reactionAcceptance: note.reactionAcceptance,
|
||||
reactionEmojis: {},
|
||||
reactions: {},
|
||||
reactionCount: 0,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
uri: note.uri ?? undefined,
|
||||
url: note.url ?? undefined,
|
||||
reactionAndUserPairCache: note.reactionAndUserPairCache,
|
||||
...(detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
reply: note.reply ? toPackedNote(note.reply, false) : null,
|
||||
renote: note.renote ? toPackedNote(note.renote, true) : null,
|
||||
myReaction: null,
|
||||
} : {}),
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: user.avatarBlurhash,
|
||||
avatarDecorations: user.avatarDecorations.map(it => ({
|
||||
id: it.id,
|
||||
angle: it.angle,
|
||||
flipH: it.flipH,
|
||||
url: 'https://example.com/dummy-image001.png',
|
||||
offsetX: it.offsetX,
|
||||
offsetY: it.offsetY,
|
||||
})),
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
emojis: user.emojis,
|
||||
onlineStatus: 'active',
|
||||
badgeRoles: [],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
|
||||
return {
|
||||
...toPackedUserLite(user),
|
||||
url: null,
|
||||
uri: null,
|
||||
movedTo: null,
|
||||
alsoKnownAs: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: user.bannerBlurhash,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: false,
|
||||
isSuspended: user.isSuspended,
|
||||
description: null,
|
||||
location: null,
|
||||
birthday: null,
|
||||
lang: null,
|
||||
fields: [],
|
||||
verifiedLinks: [],
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
pinnedNoteIds: [],
|
||||
pinnedNotes: [],
|
||||
pinnedPageId: null,
|
||||
pinnedPage: null,
|
||||
publicReactions: true,
|
||||
followersVisibility: 'public',
|
||||
followingVisibility: 'public',
|
||||
twoFactorEnabled: false,
|
||||
usePasswordLessLogin: false,
|
||||
securityKeys: false,
|
||||
roles: [],
|
||||
memo: null,
|
||||
moderationNote: undefined,
|
||||
isFollowing: false,
|
||||
isFollowed: false,
|
||||
hasPendingFollowRequestFromYou: false,
|
||||
hasPendingFollowRequestToYou: false,
|
||||
isBlocking: false,
|
||||
isBlocked: false,
|
||||
isMuted: false,
|
||||
isRenoteMuted: false,
|
||||
notify: 'none',
|
||||
withReplies: true,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
const dummyUser1 = generateDummyUser();
|
||||
const dummyUser2 = generateDummyUser({
|
||||
id: 'dummy-user-2',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 30),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis),
|
||||
username: 'dummy2',
|
||||
usernameLower: 'dummy2',
|
||||
name: 'DummyUser2',
|
||||
followersCount: 40,
|
||||
followingCount: 50,
|
||||
notesCount: 900,
|
||||
});
|
||||
const dummyUser3 = generateDummyUser({
|
||||
id: 'dummy-user-3',
|
||||
updatedAt: new Date(Date.now() - oneDayMillis * 15),
|
||||
lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
|
||||
lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
|
||||
username: 'dummy3',
|
||||
usernameLower: 'dummy3',
|
||||
name: 'DummyUser3',
|
||||
followersCount: 60,
|
||||
followingCount: 70,
|
||||
notesCount: 15900,
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class WebhookTestService {
|
||||
public static NoSuchWebhookError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
private userWebhookService: UserWebhookService,
|
||||
private systemWebhookService: SystemWebhookService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* UserWebhookのテスト送信を行う.
|
||||
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||
*
|
||||
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||
* - Webhookそのものの有効・無効設定(active)
|
||||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testUserWebhook(
|
||||
params: {
|
||||
webhookId: MiWebhook['id'],
|
||||
type: WebhookEventTypes,
|
||||
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||
},
|
||||
sender: MiUser | null,
|
||||
) {
|
||||
const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
|
||||
.then(it => it.filter(it => it.userId === sender?.id));
|
||||
if (webhooks.length === 0) {
|
||||
throw new WebhookTestService.NoSuchWebhookError();
|
||||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
};
|
||||
|
||||
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
const dummyNote1 = generateDummyNote({
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
});
|
||||
const dummyReply1 = generateDummyNote({
|
||||
id: 'dummy-reply-1',
|
||||
replyId: dummyNote1.id,
|
||||
reply: dummyNote1,
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
});
|
||||
const dummyRenote1 = generateDummyNote({
|
||||
id: 'dummy-renote-1',
|
||||
renoteId: dummyNote1.id,
|
||||
renote: dummyNote1,
|
||||
userId: dummyUser2.id,
|
||||
user: dummyUser2,
|
||||
text: null,
|
||||
});
|
||||
const dummyMention1 = generateDummyNote({
|
||||
id: 'dummy-mention-1',
|
||||
userId: dummyUser1.id,
|
||||
user: dummyUser1,
|
||||
text: `@${dummyUser2.username} This is a mention to you.`,
|
||||
mentions: [dummyUser2.id],
|
||||
});
|
||||
|
||||
switch (params.type) {
|
||||
case 'note': {
|
||||
send(toPackedNote(dummyNote1));
|
||||
break;
|
||||
}
|
||||
case 'reply': {
|
||||
send(toPackedNote(dummyReply1));
|
||||
break;
|
||||
}
|
||||
case 'renote': {
|
||||
send(toPackedNote(dummyRenote1));
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
send(toPackedNote(dummyMention1));
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||
break;
|
||||
}
|
||||
case 'followed': {
|
||||
send(toPackedUserLite(dummyUser2));
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemWebhookのテスト送信を行う.
|
||||
* 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
|
||||
*
|
||||
* また、この関数経由で送信されるWebhookは以下の設定を無視する.
|
||||
* - Webhookそのものの有効・無効設定(isActive)
|
||||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testSystemWebhook(
|
||||
params: {
|
||||
webhookId: MiSystemWebhook['id'],
|
||||
type: SystemWebhookEventType,
|
||||
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||
},
|
||||
) {
|
||||
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
|
||||
if (webhooks.length === 0) {
|
||||
throw new WebhookTestService.NoSuchWebhookError();
|
||||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
};
|
||||
|
||||
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
switch (params.type) {
|
||||
case 'abuseReport': {
|
||||
send(generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
reporter: dummyUser2,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'abuseReportResolved': {
|
||||
send(generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
reporter: dummyUser2,
|
||||
assigneeId: dummyUser3.id,
|
||||
assignee: dummyUser3,
|
||||
resolved: true,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'userCreated': {
|
||||
send(toPackedUserLite(dummyUser1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import { id } from './util/id.js';
|
|||
import { MiUser } from './User.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
export type WebhookEventTypes = typeof webhookEventTypes[number];
|
||||
|
||||
@Entity('webhook')
|
||||
export class MiWebhook {
|
||||
|
|
|
@ -92,6 +92,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
|
|||
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
|
@ -258,6 +259,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
|
@ -475,6 +477,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
|
|||
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
|
||||
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
|
||||
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
|
||||
const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
|
||||
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
|
||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
|
||||
|
@ -641,6 +644,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
|
|||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
||||
const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
|
||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
||||
|
@ -862,6 +866,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_systemWebhook_list,
|
||||
$admin_systemWebhook_show,
|
||||
$admin_systemWebhook_update,
|
||||
$admin_systemWebhook_test,
|
||||
$announcements,
|
||||
$announcements_show,
|
||||
$antennas_create,
|
||||
|
@ -1028,6 +1033,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$i_webhooks_test,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
|
@ -1243,6 +1249,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$admin_systemWebhook_list,
|
||||
$admin_systemWebhook_show,
|
||||
$admin_systemWebhook_update,
|
||||
$admin_systemWebhook_test,
|
||||
$announcements,
|
||||
$announcements_show,
|
||||
$antennas_create,
|
||||
|
@ -1409,6 +1416,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$i_webhooks_test,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
|
|
|
@ -98,6 +98,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
|
|||
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
|
||||
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
|
||||
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
|
||||
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
|
@ -264,6 +265,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
|
@ -479,6 +481,7 @@ const eps = [
|
|||
['admin/system-webhook/list', ep___admin_systemWebhook_list],
|
||||
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
||||
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
||||
['admin/system-webhook/test', ep___admin_systemWebhook_test],
|
||||
['announcements', ep___announcements],
|
||||
['announcements/show', ep___announcements_show],
|
||||
['antennas/create', ep___antennas_create],
|
||||
|
@ -645,6 +648,7 @@ const eps = [
|
|||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['i/webhooks/test', ep___i_webhooks_test],
|
||||
['invite/create', ep___invite_create],
|
||||
['invite/delete', ep___invite_delete],
|
||||
['invite/list', ep___invite_list],
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
secure: true,
|
||||
kind: 'read:admin:system-webhook',
|
||||
|
||||
limit: {
|
||||
duration: ms('15min'),
|
||||
max: 60,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
format: 'misskey:id',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: systemWebhookEventTypes,
|
||||
},
|
||||
override: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', nullable: false },
|
||||
secret: { type: 'string', nullable: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['webhookId', 'type'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private webhookTestService: WebhookTestService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
try {
|
||||
await this.webhookTestService.testSystemWebhook({
|
||||
webhookId: ps.webhookId,
|
||||
type: ps.type,
|
||||
override: ps.override,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof WebhookTestService.NoSuchWebhookError) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
// TODO: UserWebhook schemaの適用
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
|
|||
import type { WebhooksRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
// TODO: UserWebhook schemaの適用
|
||||
export const meta = {
|
||||
tags: ['webhooks', 'account'],
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
// TODO: UserWebhook schemaの適用
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { webhookEventTypes } from '@/models/Webhook.js';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
secure: true,
|
||||
kind: 'read:account',
|
||||
|
||||
limit: {
|
||||
duration: ms('15min'),
|
||||
max: 60,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
format: 'misskey:id',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: webhookEventTypes,
|
||||
},
|
||||
override: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
secret: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['webhookId', 'type'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private webhookTestService: WebhookTestService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
await this.webhookTestService.testUserWebhook({
|
||||
webhookId: ps.webhookId,
|
||||
type: ps.type,
|
||||
override: ps.override,
|
||||
}, me);
|
||||
} catch (e) {
|
||||
if (e instanceof WebhookTestService.NoSuchWebhookError) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
@ -6,6 +7,7 @@
|
|||
import { setTimeout } from 'node:timers/promises';
|
||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
||||
|
@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
|
|||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { randomString } from '../utils.js';
|
||||
|
||||
describe('SystemWebhookService', () => {
|
||||
let app: TestingModule;
|
||||
|
@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
|
|||
isActive: true,
|
||||
on: ['abuseReport'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
|
|||
isActive: false,
|
||||
on: ['abuseReport'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
|
|||
isActive: true,
|
||||
on: ['abuseReportResolved'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
|
||||
describe('UserWebhookService', () => {
|
||||
let app: TestingModule;
|
||||
let service: UserWebhookService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let userWebhooksRepository: WebhooksRepository;
|
||||
let idService: IdService;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let root: MiUser;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
return await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
async function createWebhook(data: Partial<MiWebhook> = {}) {
|
||||
return userWebhooksRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
name: randomString(),
|
||||
on: ['mention'],
|
||||
url: 'https://example.com',
|
||||
secret: randomString(),
|
||||
userId: root.id,
|
||||
...data,
|
||||
})
|
||||
.then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
async function beforeAllImpl() {
|
||||
app = await Test
|
||||
.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
UserWebhookService,
|
||||
IdService,
|
||||
LoggerService,
|
||||
GlobalEventService,
|
||||
{
|
||||
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
|
||||
},
|
||||
],
|
||||
})
|
||||
.compile();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userWebhooksRepository = app.get(DI.webhooksRepository);
|
||||
|
||||
service = app.get(UserWebhookService);
|
||||
idService = app.get(IdService);
|
||||
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||
|
||||
app.enableShutdownHooks();
|
||||
}
|
||||
|
||||
async function afterAllImpl() {
|
||||
await app.close();
|
||||
}
|
||||
|
||||
async function beforeEachImpl() {
|
||||
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||
}
|
||||
|
||||
async function afterEachImpl() {
|
||||
await usersRepository.delete({});
|
||||
await userWebhooksRepository.delete({});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
describe('アプリを毎回作り直す必要のないグループ', () => {
|
||||
beforeAll(beforeAllImpl);
|
||||
afterAll(afterAllImpl);
|
||||
beforeEach(beforeEachImpl);
|
||||
afterEach(afterEachImpl);
|
||||
|
||||
describe('fetchSystemWebhooks', () => {
|
||||
test('フィルタなし', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks();
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
|
||||
});
|
||||
|
||||
test('activeのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ isActive: true });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
|
||||
});
|
||||
|
||||
test('特定のイベントのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
|
||||
});
|
||||
|
||||
test('activeな特定のイベントのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true });
|
||||
expect(fetchedWebhooks).toEqual([webhook1]);
|
||||
});
|
||||
|
||||
test('ID指定', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
|
||||
});
|
||||
|
||||
test('ID指定(他条件とANDになるか見たい)', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
|
||||
expect(fetchedWebhooks).toEqual([webhook4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,225 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { beforeAll, describe, jest } from '@jest/globals';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
describe('WebhookTestService', () => {
|
||||
let app: TestingModule;
|
||||
let service: WebhookTestService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let userWebhookService: jest.Mocked<UserWebhookService>;
|
||||
let systemWebhookService: jest.Mocked<SystemWebhookService>;
|
||||
let idService: IdService;
|
||||
|
||||
let root: MiUser;
|
||||
let alice: MiUser;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
const user = await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await userProfilesRepository.insert({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
WebhookTestService,
|
||||
IdService,
|
||||
{
|
||||
provide: QueueService, useFactory: () => ({
|
||||
systemWebhookDeliver: jest.fn(),
|
||||
userWebhookDeliver: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: UserWebhookService, useFactory: () => ({
|
||||
fetchWebhooks: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: SystemWebhookService, useFactory: () => ({
|
||||
fetchSystemWebhooks: jest.fn(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
|
||||
service = app.get(WebhookTestService);
|
||||
idService = app.get(IdService);
|
||||
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||
userWebhookService = app.get(UserWebhookService) as jest.Mocked<UserWebhookService>;
|
||||
systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
|
||||
|
||||
app.enableShutdownHooks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||
|
||||
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
|
||||
]));
|
||||
systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', isActive: true } as MiSystemWebhook,
|
||||
]));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
queueService.systemWebhookDeliver.mockClear();
|
||||
queueService.userWebhookDeliver.mockClear();
|
||||
userWebhookService.fetchWebhooks.mockClear();
|
||||
systemWebhookService.fetchSystemWebhooks.mockClear();
|
||||
|
||||
await usersRepository.delete({});
|
||||
await userProfilesRepository.delete({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
describe('testUserWebhook', () => {
|
||||
test('note', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('note');
|
||||
expect((calls[2] as any).id).toBe('dummy-note-1');
|
||||
});
|
||||
|
||||
test('reply', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('reply');
|
||||
expect((calls[2] as any).id).toBe('dummy-reply-1');
|
||||
});
|
||||
|
||||
test('renote', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('renote');
|
||||
expect((calls[2] as any).id).toBe('dummy-renote-1');
|
||||
});
|
||||
|
||||
test('mention', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('mention');
|
||||
expect((calls[2] as any).id).toBe('dummy-mention-1');
|
||||
});
|
||||
|
||||
test('follow', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('follow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||
});
|
||||
|
||||
test('followed', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('followed');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-2');
|
||||
});
|
||||
|
||||
test('unfollow', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('unfollow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-3');
|
||||
});
|
||||
|
||||
describe('NoSuchWebhookError', () => {
|
||||
test('user not match', async () => {
|
||||
userWebhookService.fetchWebhooks.mockClear();
|
||||
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', active: true } as MiWebhook,
|
||||
]));
|
||||
|
||||
await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root))
|
||||
.rejects.toThrow(WebhookTestService.NoSuchWebhookError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('testSystemWebhook', () => {
|
||||
test('abuseReport', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('abuseReport');
|
||||
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||
expect((calls[2] as any).resolved).toBe(false);
|
||||
});
|
||||
|
||||
test('abuseReportResolved', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('abuseReportResolved');
|
||||
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||
expect((calls[2] as any).resolved).toBe(true);
|
||||
});
|
||||
|
||||
test('userCreated', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('userCreated');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { MenuButton } from '@/types/menu.js';
|
||||
import type { MenuItem, MenuButton } from '@/types/menu.js';
|
||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||
|
@ -288,14 +288,26 @@ export async function openAccountMenu(opts: {
|
|||
});
|
||||
}));
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (opts.withExtraOperation) {
|
||||
popupMenu([...[{
|
||||
type: 'link' as const,
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${ $i.username }`,
|
||||
to: `/@${$i.username}`,
|
||||
avatar: $i,
|
||||
}, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||
type: 'parent' as const,
|
||||
}, {
|
||||
type: 'divider',
|
||||
});
|
||||
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addAccount,
|
||||
children: [{
|
||||
|
@ -306,18 +318,22 @@ export async function openAccountMenu(opts: {
|
|||
action: () => { createAccount(); },
|
||||
}],
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
icon: 'ti ti-users',
|
||||
text: i18n.ts.manageAccounts,
|
||||
to: '/settings/accounts',
|
||||
}]], ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
} else {
|
||||
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
}
|
||||
|
||||
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
|
@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
|
|
|
@ -620,7 +620,9 @@ function fetchMoreFiles() {
|
|||
}
|
||||
|
||||
function getMenu() {
|
||||
const menu: MenuItem[] = [{
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
|
@ -638,19 +640,25 @@ function getMenu() {
|
|||
}, { type: 'divider' }, {
|
||||
text: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
type: 'label',
|
||||
}, folder.value ? {
|
||||
text: i18n.ts.renameFolder,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||
} : undefined, folder.value ? {
|
||||
text: i18n.ts.deleteFolder,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||
} : undefined, {
|
||||
});
|
||||
|
||||
if (folder.value) {
|
||||
menu.push({
|
||||
text: i18n.ts.renameFolder,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||
}, {
|
||||
text: i18n.ts.deleteFolder,
|
||||
icon: 'ti ti-trash',
|
||||
action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: i18n.ts.createFolder,
|
||||
icon: 'ti ti-folder-plus',
|
||||
action: () => { createFolder(); },
|
||||
}];
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
|
|
@ -172,9 +172,7 @@ async function show() {
|
|||
const menuShowing = ref(false);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
let menu: MenuItem[] = [];
|
||||
|
||||
menu = [
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
|
@ -222,7 +220,7 @@ function showMenu(ev: MouseEvent) {
|
|||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.audio.id}`,
|
||||
|
|
|
@ -60,6 +60,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
image: Misskey.entities.DriveFile;
|
||||
|
@ -111,27 +112,39 @@ watch(() => props.image, () => {
|
|||
});
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.hide,
|
||||
icon: 'ti ti-eye-off',
|
||||
action: () => {
|
||||
hide.value = true;
|
||||
},
|
||||
}, ...(iAmModerator ? [{
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
}] : []), ...($i?.id === props.image.userId ? [{
|
||||
type: 'divider' as const,
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.image.id}`,
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if (iAmModerator) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.markAsSensitive,
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ($i?.id === props.image.userId) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.image.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
@ -192,9 +192,7 @@ async function show() {
|
|||
const menuShowing = ref(false);
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
let menu: MenuItem[] = [];
|
||||
|
||||
menu = [
|
||||
const menu: MenuItem[] = [
|
||||
// TODO: 再生キューに追加
|
||||
{
|
||||
type: 'switch',
|
||||
|
@ -247,7 +245,7 @@ function showMenu(ev: MouseEvent) {
|
|||
menu.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
text: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-info-circle',
|
||||
to: `/my/drive/file/${props.video.id}`,
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
items: MenuItem[];
|
||||
|
|
|
@ -193,7 +193,7 @@ import { deepClone } from '@/scripts/clone.js';
|
|||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, shallowRef } from 'vue';
|
||||
import MkModal from './MkModal.vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
defineProps<{
|
||||
items: MenuItem[];
|
||||
|
|
|
@ -26,6 +26,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
@ -136,7 +137,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
if (menuShowing) return;
|
||||
|
||||
const isImage = file.type.startsWith('image/');
|
||||
os.popupMenu([{
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.renameFile,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => { rename(file); },
|
||||
|
@ -148,11 +152,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
text: i18n.ts.describeFile,
|
||||
icon: 'ti ti-text-caption',
|
||||
action: () => { describe(file); },
|
||||
}, ...isImage ? [{
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
}] : [], {
|
||||
});
|
||||
|
||||
if (isImage) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.attachCancel,
|
||||
|
@ -163,7 +173,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => { detachAndDeleteMedia(file); },
|
||||
}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||
menuShowing = true;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -46,7 +46,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
*/
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
||||
export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
|
||||
|
||||
export type MkSystemWebhookEditorProps = {
|
||||
mode: 'create' | 'edit';
|
||||
|
|
|
@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
||||
</MkSwitch>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'edit'" :class="$style.description">
|
||||
{{ i18n.ts._webhookSettings.testRemarks }}
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import {
|
||||
|
@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
|
|||
}
|
||||
}
|
||||
|
||||
async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> {
|
||||
if (!id.value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await os.apiWithDialog('admin/system-webhook/test', {
|
||||
webhookId: id.value,
|
||||
type,
|
||||
override: {
|
||||
secret: secret.value,
|
||||
url: url.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadingScope(async () => {
|
||||
switch (mode.value) {
|
||||
|
@ -235,4 +266,29 @@ onMounted(async () => {
|
|||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.switchBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
.testButton {
|
||||
$buttonSize: 28px;
|
||||
padding: 0;
|
||||
width: $buttonSize;
|
||||
min-width: $buttonSize;
|
||||
max-width: $buttonSize;
|
||||
height: $buttonSize;
|
||||
margin-left: auto;
|
||||
line-height: normal;
|
||||
font-size: 90%;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import * as os from '@/os.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
|||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
@ -85,7 +86,9 @@ const errored = ref(url.value == null);
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
}, {
|
||||
|
@ -95,14 +98,20 @@ function onClick(ev: MouseEvent) {
|
|||
copyToClipboard(`:${props.name}:`);
|
||||
os.success();
|
||||
},
|
||||
}, ...(props.menuReaction && react ? [{
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : []), {
|
||||
});
|
||||
|
||||
if (props.menuReaction && react) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: async () => {
|
||||
|
@ -114,7 +123,9 @@ function onClick(ev: MouseEvent) {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -17,6 +17,7 @@ import * as os from '@/os.js';
|
|||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: string;
|
||||
|
@ -39,7 +40,9 @@ function computeTitle(event: PointerEvent): void {
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'label',
|
||||
text: props.emoji,
|
||||
}, {
|
||||
|
@ -49,14 +52,20 @@ function onClick(ev: MouseEvent) {
|
|||
copyToClipboard(props.emoji);
|
||||
os.success();
|
||||
},
|
||||
}, ...(props.menuReaction && react ? [{
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if (props.menuReaction && react) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.doReaction,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -125,7 +125,7 @@ export const navbarItemDef = reactive({
|
|||
ui: {
|
||||
title: i18n.ts.switchUi,
|
||||
icon: 'ti ti-devices',
|
||||
action: (ev) => {
|
||||
action: (ev: MouseEvent) => {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.default,
|
||||
active: ui === 'default' || ui === null,
|
||||
|
|
|
@ -22,7 +22,7 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
|
|||
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { 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';
|
||||
|
|
|
@ -45,6 +45,7 @@ import { clipsCache } from '@/cache.js';
|
|||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string,
|
||||
|
@ -131,7 +132,9 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
handler: (ev: MouseEvent): void => {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyUrl,
|
||||
action: () => {
|
||||
|
@ -144,17 +147,23 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
action: () => {
|
||||
genEmbedCode('clips', clip.value!.id);
|
||||
},
|
||||
}, ...(isSupportShare() ? [{
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: async () => {
|
||||
navigator.share({
|
||||
title: clip.value!.name,
|
||||
text: clip.value!.description ?? '',
|
||||
url: `${url}/clips/${clip.value!.id}`,
|
||||
});
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if (isSupportShare()) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: async () => {
|
||||
navigator.share({
|
||||
title: clip.value!.name,
|
||||
text: clip.value!.description ?? '',
|
||||
url: `${url}/clips/${clip.value!.id}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
}] : []), {
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
@ -80,7 +80,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 { MenuItem } from '@/types/menu';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -104,18 +104,23 @@ function fetchFlash() {
|
|||
function share(ev: MouseEvent) {
|
||||
if (!flash.value) return;
|
||||
|
||||
os.popupMenu([
|
||||
{
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
},
|
||||
...(isSupportShare() ? [{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
});
|
||||
|
||||
if (isSupportShare()) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.share,
|
||||
icon: 'ti ti-share',
|
||||
action: shareWithNavigator,
|
||||
}] : []),
|
||||
], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
|
|
|
@ -80,7 +80,7 @@ import { $i } from '@/account.js';
|
|||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -171,35 +171,35 @@ function reportAbuse() {
|
|||
function showMenu(ev: MouseEvent) {
|
||||
if (!post.value) return;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
...($i && $i.id !== post.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
},
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !post.value) return;
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
|
||||
}),
|
||||
},
|
||||
] : []),
|
||||
] : []),
|
||||
];
|
||||
if ($i && $i.id !== post.value.userId) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
});
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
if ($i.isModerator || $i.isAdmin) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !post.value) return;
|
||||
|
||||
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
|
|
@ -134,12 +134,14 @@ async function removeUser(item, ev) {
|
|||
|
||||
async function showMembershipMenu(item, ev) {
|
||||
const withRepliesRef = ref(item.withReplies);
|
||||
|
||||
os.popupMenu([{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
icon: 'ti ti-messages',
|
||||
ref: withRepliesRef,
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
||||
watch(withRepliesRef, withReplies => {
|
||||
misskeyApi('users/lists/update-membership', {
|
||||
listId: list.value!.id,
|
||||
|
|
|
@ -121,7 +121,7 @@ import { instance } from '@/instance.js';
|
|||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -165,18 +165,23 @@ function fetchPage() {
|
|||
function share(ev: MouseEvent) {
|
||||
if (!page.value) return;
|
||||
|
||||
os.popupMenu([
|
||||
{
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
},
|
||||
...(isSupportShare() ? [{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: i18n.ts.shareWithNote,
|
||||
icon: 'ti ti-pencil',
|
||||
action: shareWithNote,
|
||||
});
|
||||
|
||||
if (isSupportShare()) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.share,
|
||||
icon: 'ti ti-share',
|
||||
action: shareWithNavigator,
|
||||
}] : []),
|
||||
], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
|
@ -256,51 +261,59 @@ function reportAbuse() {
|
|||
function showMenu(ev: MouseEvent) {
|
||||
if (!page.value) return;
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
...($i && $i.id === page.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-code',
|
||||
text: i18n.ts._pages.viewSource,
|
||||
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
|
||||
},
|
||||
...($i.pinnedPageId === page.value.id ? [{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if ($i && $i.id === page.value.userId) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editThisPage,
|
||||
action: () => router.push(`/pages/edit/${page.value.id}`),
|
||||
});
|
||||
|
||||
if ($i.pinnedPageId === page.value.id) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => pin(false),
|
||||
}] : [{
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => pin(true),
|
||||
}]),
|
||||
] : []),
|
||||
...($i && $i.id !== page.value.userId ? [
|
||||
{
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
},
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !page.value) return;
|
||||
});
|
||||
}
|
||||
} else if ($i && $i.id !== page.value.userId) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-code',
|
||||
text: i18n.ts._pages.viewSource,
|
||||
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
|
||||
}, {
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
});
|
||||
|
||||
os.apiWithDialog('pages/delete', { pageId: page.value.id });
|
||||
}),
|
||||
},
|
||||
] : []),
|
||||
] : []),
|
||||
];
|
||||
if ($i.isModerator || $i.isAdmin) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: () => os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled || !page.value) return;
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
os.apiWithDialog('pages/delete', { pageId: page.value.id });
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
watch(() => path.value, fetchPage, { immediate: true });
|
||||
|
|
|
@ -121,7 +121,7 @@ import MkRadios from '@/components/MkRadios.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
|
|
@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
|
||||
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
|
||||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps_s">
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.description">
|
||||
{{ i18n.ts._webhookSettings.testRemarks }}
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote'));
|
|||
const event_reaction = ref(webhook.on.includes('reaction'));
|
||||
const event_mention = ref(webhook.on.includes('mention'));
|
||||
|
||||
async function save(): Promise<void> {
|
||||
const events = [];
|
||||
function save() {
|
||||
const events: Misskey.entities.UserWebhook['on'] = [];
|
||||
if (event_follow.value) events.push('follow');
|
||||
if (event_followed.value) events.push('followed');
|
||||
if (event_note.value) events.push('note');
|
||||
|
@ -110,8 +138,21 @@ async function del(): Promise<void> {
|
|||
router.push('/settings/webhook');
|
||||
}
|
||||
|
||||
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {
|
||||
await os.apiWithDialog('i/webhooks/test', {
|
||||
webhookId: props.webhookId,
|
||||
type,
|
||||
override: {
|
||||
secret: secret.value,
|
||||
url: url.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
|
@ -119,3 +160,30 @@ definePageMetadata(() => ({
|
|||
icon: 'ti ti-webhook',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.switchBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
.testButton {
|
||||
$buttonSize: 28px;
|
||||
padding: 0;
|
||||
width: $buttonSize;
|
||||
min-width: $buttonSize;
|
||||
max-width: $buttonSize;
|
||||
height: $buttonSize;
|
||||
margin-left: auto;
|
||||
line-height: inherit;
|
||||
font-size: 90%;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -50,7 +50,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
||||
import type { BasicTimelineType } from '@/timelines.js';
|
||||
|
@ -189,7 +189,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
|
|||
}),
|
||||
(channels.length === 0 ? undefined : { type: 'divider' }),
|
||||
{
|
||||
type: 'link' as const,
|
||||
type: 'link',
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
to: '/channels',
|
||||
|
@ -258,16 +258,24 @@ const headerActions = computed(() => {
|
|||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: withRenotes,
|
||||
}, isBasicTimeline(src.value) && hasWithReplies(src.value) ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: withReplies,
|
||||
disabled: onlyFiles,
|
||||
} : undefined, {
|
||||
});
|
||||
|
||||
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: withReplies,
|
||||
disabled: onlyFiles,
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.withSensitive,
|
||||
ref: withSensitive,
|
||||
|
@ -276,7 +284,9 @@ const headerActions = computed(() => {
|
|||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
function rename(file: Misskey.entities.DriveFile) {
|
||||
|
@ -87,8 +87,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
|
|||
|
||||
export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
let menu;
|
||||
menu = [{
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
to: `/my/drive/file/${file.id}`,
|
||||
text: i18n.ts._fileViewer.title,
|
||||
|
@ -109,14 +111,20 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
|||
text: i18n.ts.describeFile,
|
||||
icon: 'ti ti-text-caption',
|
||||
action: () => describe(file),
|
||||
}, ...isImage ? [{
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () => os.cropImage(file, {
|
||||
aspectRatio: NaN,
|
||||
uploadFolder: folder ? folder.id : folder,
|
||||
}),
|
||||
}] : [], { type: 'divider' }, {
|
||||
});
|
||||
|
||||
if (isImage) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () => os.cropImage(file, {
|
||||
aspectRatio: NaN,
|
||||
uploadFolder: folder ? folder.id : folder,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
text: i18n.ts.createNoteFromTheFile,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => os.post({
|
||||
|
@ -138,17 +146,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
|
|||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => deleteFile(file),
|
||||
}];
|
||||
});
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyFileId,
|
||||
action: () => {
|
||||
copyToClipboard(file.id);
|
||||
},
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
return menuItems;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { defaultStore, noteActions } from '@/store.js';
|
|||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu.js';
|
||||
import { clipsCache, favoritedChannelsCache } from '@/cache.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { getAppearNote } from '@/scripts/get-appear-note.js';
|
||||
|
@ -99,11 +99,13 @@ export async function getNoteClipMenu(props: {
|
|||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
name: {
|
||||
type: 'string',
|
||||
default: null,
|
||||
label: i18n.ts.name,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: null,
|
||||
multiline: true,
|
||||
label: i18n.ts.description,
|
||||
},
|
||||
|
@ -264,7 +266,7 @@ export function getNoteMenu(props: {
|
|||
title: i18n.ts.numberOfDays,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
if (canceled || days == null) return;
|
||||
|
||||
os.apiWithDialog('admin/promo/create', {
|
||||
noteId: appearNote.id,
|
||||
|
@ -295,161 +297,23 @@ export function getNoteMenu(props: {
|
|||
props.translation.value = res;
|
||||
}
|
||||
|
||||
let menu: MenuItem[];
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if ($i) {
|
||||
const statePromise = misskeyApi('notes/state', {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
menu = [
|
||||
...(
|
||||
props.currentClip?.userId === $i.id ? [{
|
||||
icon: 'ti ti-backspace',
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
action: unclip,
|
||||
}, { type: 'divider' }] : []
|
||||
), {
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.details,
|
||||
action: openDetail,
|
||||
}, {
|
||||
icon: 'ti ti-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
|
||||
, (appearNote.url || appearNote.uri) ? {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
},
|
||||
} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode),
|
||||
...(isSupportShare() ? [{
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
}] : []),
|
||||
$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
|
||||
icon: 'ti ti-language-hiragana',
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
} : undefined,
|
||||
{ type: 'divider' },
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'ti ti-star-off',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
} : {
|
||||
icon: 'ti ti-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
}),
|
||||
{
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-paperclip',
|
||||
text: i18n.ts.clip,
|
||||
children: () => getNoteClipMenu(props),
|
||||
},
|
||||
statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
} : {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
}),
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? {
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => togglePin(false),
|
||||
} : {
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => togglePin(true),
|
||||
} : undefined,
|
||||
{
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-user',
|
||||
text: i18n.ts.user,
|
||||
children: async () => {
|
||||
const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
|
||||
const { menu, cleanup } = getUserMenu(user);
|
||||
cleanups.push(cleanup);
|
||||
return menu;
|
||||
},
|
||||
},
|
||||
/*
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
{ type: 'divider' },
|
||||
{
|
||||
icon: 'ti ti-speakerphone',
|
||||
text: i18n.ts.promote,
|
||||
action: promote
|
||||
}]
|
||||
: []
|
||||
),*/
|
||||
...(appearNote.userId !== $i.id ? [
|
||||
{ type: 'divider' },
|
||||
appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'parent' as const,
|
||||
icon: 'ti ti-device-tv',
|
||||
text: i18n.ts.channel,
|
||||
children: async () => {
|
||||
const channelChildMenu = [] as MenuItem[];
|
||||
if (props.currentClip?.userId === $i.id) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-backspace',
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
action: unclip,
|
||||
}, { type: 'divider' });
|
||||
}
|
||||
|
||||
const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
|
||||
|
||||
if (channel.pinnedNoteIds.includes(appearNote.id)) {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
|
||||
}),
|
||||
});
|
||||
}
|
||||
return channelChildMenu;
|
||||
},
|
||||
},
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
|
||||
{ type: 'divider' },
|
||||
appearNote.userId === $i.id ? {
|
||||
icon: 'ti ti-edit',
|
||||
text: i18n.ts.deleteAndEdit,
|
||||
action: delEdit,
|
||||
} : undefined,
|
||||
{
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: del,
|
||||
}]
|
||||
: []
|
||||
)]
|
||||
.filter(x => x !== undefined);
|
||||
} else {
|
||||
menu = [{
|
||||
menuItems.push({
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.details,
|
||||
action: openDetail,
|
||||
|
@ -457,35 +321,194 @@ export function getNoteMenu(props: {
|
|||
icon: 'ti ti-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink),
|
||||
(appearNote.url || appearNote.uri) ? {
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||
|
||||
if (appearNote.url || appearNote.uri) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
|
||||
}
|
||||
|
||||
if (isSupportShare()) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
});
|
||||
}
|
||||
|
||||
if ($i.policies.canUseTranslator && instance.translatorAvailable) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-language-hiragana',
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ type: 'divider' });
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'ti ti-star-off',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
} : {
|
||||
icon: 'ti ti-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
}));
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-paperclip',
|
||||
text: i18n.ts.clip,
|
||||
children: () => getNoteClipMenu(props),
|
||||
});
|
||||
|
||||
menuItems.push(statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
} : {
|
||||
icon: 'ti ti-message-off',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
}));
|
||||
|
||||
if (appearNote.userId === $i.id) {
|
||||
if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => togglePin(false),
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => togglePin(true),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-user',
|
||||
text: i18n.ts.user,
|
||||
children: async () => {
|
||||
const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
|
||||
const { menu, cleanup } = getUserMenu(user);
|
||||
cleanups.push(cleanup);
|
||||
return menu;
|
||||
},
|
||||
} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)]
|
||||
.filter(x => x !== undefined);
|
||||
});
|
||||
|
||||
if (appearNote.userId !== $i.id) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse));
|
||||
}
|
||||
|
||||
if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-device-tv',
|
||||
text: i18n.ts.channel,
|
||||
children: async () => {
|
||||
const channelChildMenu = [] as MenuItem[];
|
||||
|
||||
const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
|
||||
|
||||
if (channel.pinnedNoteIds.includes(appearNote.id)) {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
channelChildMenu.push({
|
||||
icon: 'ti ti-pin',
|
||||
text: i18n.ts.pin,
|
||||
action: () => os.apiWithDialog('channels/update', {
|
||||
channelId: appearNote.channel!.id,
|
||||
pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
|
||||
}),
|
||||
});
|
||||
}
|
||||
return channelChildMenu;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
if (appearNote.userId === $i.id) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-edit',
|
||||
text: i18n.ts.deleteAndEdit,
|
||||
action: delEdit,
|
||||
});
|
||||
}
|
||||
menuItems.push({
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: del,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-info-circle',
|
||||
text: i18n.ts.details,
|
||||
action: openDetail,
|
||||
}, {
|
||||
icon: 'ti ti-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent,
|
||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||
|
||||
if (appearNote.url || appearNote.uri) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
|
||||
}
|
||||
}
|
||||
|
||||
if (noteActions.length > 0) {
|
||||
menu = menu.concat([{ type: 'divider' }, ...noteActions.map(action => ({
|
||||
menuItems.push({ type: 'divider' });
|
||||
|
||||
menuItems.push(...noteActions.map(action => ({
|
||||
icon: 'ti ti-plug',
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(appearNote);
|
||||
},
|
||||
}))]);
|
||||
})));
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyNoteId,
|
||||
action: () => {
|
||||
copyToClipboard(appearNote.id);
|
||||
os.success();
|
||||
},
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
|
@ -496,7 +519,7 @@ export function getNoteMenu(props: {
|
|||
};
|
||||
|
||||
return {
|
||||
menu,
|
||||
menu: menuItems,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { IRouter } from '@/nirax.js';
|
|||
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
|
||||
const meId = $i ? $i.id : null;
|
||||
|
@ -148,133 +148,154 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
});
|
||||
}
|
||||
|
||||
let menu: MenuItem[] = [{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-at',
|
||||
text: i18n.ts.copyUsername,
|
||||
action: () => {
|
||||
copyToClipboard(`@${user.username}@${user.host ?? host}`);
|
||||
},
|
||||
}, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.searchThisUsersNotes,
|
||||
action: () => {
|
||||
router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
|
||||
},
|
||||
}] : [])
|
||||
, ...(iAmModerator ? [{
|
||||
icon: 'ti ti-user-exclamation',
|
||||
text: i18n.ts.moderation,
|
||||
action: () => {
|
||||
router.push(`/admin/user/${user.id}`);
|
||||
},
|
||||
}] : []), {
|
||||
});
|
||||
|
||||
if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.searchThisUsersNotes,
|
||||
action: () => {
|
||||
router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (iAmModerator) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-user-exclamation',
|
||||
text: i18n.ts.moderation,
|
||||
action: () => {
|
||||
router.push(`/admin/user/${user.id}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-rss',
|
||||
text: i18n.ts.copyRSS,
|
||||
action: () => {
|
||||
copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
|
||||
},
|
||||
}, ...(user.host != null && user.url != null ? [{
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
if (user.url == null) return;
|
||||
window.open(user.url, '_blank', 'noopener');
|
||||
},
|
||||
}] : [{
|
||||
icon: 'ti ti-code',
|
||||
text: i18n.ts.genEmbedCode,
|
||||
type: 'parent' as const,
|
||||
children: [{
|
||||
text: i18n.ts.noteOfThisUser,
|
||||
});
|
||||
|
||||
if (user.host != null && user.url != null) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
genEmbedCode('user-timeline', user.id);
|
||||
if (user.url == null) return;
|
||||
window.open(user.url, '_blank', 'noopener');
|
||||
},
|
||||
}], // TODO: ユーザーカードの埋め込みなど
|
||||
}]), {
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-code',
|
||||
text: i18n.ts.genEmbedCode,
|
||||
type: 'parent',
|
||||
children: [{
|
||||
text: i18n.ts.noteOfThisUser,
|
||||
action: () => {
|
||||
genEmbedCode('user-timeline', user.id);
|
||||
},
|
||||
}], // TODO: ユーザーカードの埋め込みなど
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-share',
|
||||
text: i18n.ts.copyProfileUrl,
|
||||
action: () => {
|
||||
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: () => {
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
|
||||
os.post({ specified: user, initialText: `${canonical} ` });
|
||||
},
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editMemo,
|
||||
action: () => {
|
||||
editMemo();
|
||||
},
|
||||
}, {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-list',
|
||||
text: i18n.ts.addToList,
|
||||
children: async () => {
|
||||
const lists = await userListsCache.fetch();
|
||||
return lists.map(list => {
|
||||
const isListed = ref(list.userIds.includes(user.id));
|
||||
cleanups.push(watch(isListed, () => {
|
||||
if (isListed.value) {
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
list.userIds.push(user.id);
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
list.userIds.splice(list.userIds.indexOf(user.id), 1);
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'switch',
|
||||
text: list.name,
|
||||
ref: isListed,
|
||||
};
|
||||
});
|
||||
},
|
||||
}, {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-antenna',
|
||||
text: i18n.ts.addToAntenna,
|
||||
children: async () => {
|
||||
const antennas = await antennasCache.fetch();
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||
return antennas.filter((a) => a.src === 'users').map(antenna => ({
|
||||
text: antenna.name,
|
||||
action: async () => {
|
||||
await os.apiWithDialog('antennas/update', {
|
||||
antennaId: antenna.id,
|
||||
name: antenna.name,
|
||||
keywords: antenna.keywords,
|
||||
excludeKeywords: antenna.excludeKeywords,
|
||||
src: antenna.src,
|
||||
userListId: antenna.userListId,
|
||||
users: [...antenna.users, canonical],
|
||||
caseSensitive: antenna.caseSensitive,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
notify: antenna.notify,
|
||||
});
|
||||
antennasCache.delete();
|
||||
},
|
||||
}));
|
||||
},
|
||||
}] : [])] as any;
|
||||
if ($i) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-mail',
|
||||
text: i18n.ts.sendMessage,
|
||||
action: () => {
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
|
||||
os.post({ specified: user, initialText: `${canonical} ` });
|
||||
},
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editMemo,
|
||||
action: editMemo,
|
||||
}, {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-list',
|
||||
text: i18n.ts.addToList,
|
||||
children: async () => {
|
||||
const lists = await userListsCache.fetch();
|
||||
return lists.map(list => {
|
||||
const isListed = ref(list.userIds?.includes(user.id) ?? false);
|
||||
cleanups.push(watch(isListed, () => {
|
||||
if (isListed.value) {
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
list.userIds?.push(user.id);
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
list.userIds?.splice(list.userIds?.indexOf(user.id), 1);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
type: 'switch',
|
||||
text: list.name,
|
||||
ref: isListed,
|
||||
};
|
||||
});
|
||||
},
|
||||
}, {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-antenna',
|
||||
text: i18n.ts.addToAntenna,
|
||||
children: async () => {
|
||||
const antennas = await antennasCache.fetch();
|
||||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||
return antennas.filter((a) => a.src === 'users').map(antenna => ({
|
||||
text: antenna.name,
|
||||
action: async () => {
|
||||
await os.apiWithDialog('antennas/update', {
|
||||
antennaId: antenna.id,
|
||||
name: antenna.name,
|
||||
keywords: antenna.keywords,
|
||||
excludeKeywords: antenna.excludeKeywords,
|
||||
src: antenna.src,
|
||||
userListId: antenna.userListId,
|
||||
users: [...antenna.users, canonical],
|
||||
caseSensitive: antenna.caseSensitive,
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
notify: antenna.notify,
|
||||
});
|
||||
antennasCache.delete();
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ($i && meId !== user.id) {
|
||||
if (iAmModerator) {
|
||||
menu = menu.concat([{
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-badges',
|
||||
text: i18n.ts.roles,
|
||||
|
@ -312,13 +333,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
},
|
||||
}));
|
||||
},
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
|
||||
//if (user.isFollowing) {
|
||||
const withRepliesRef = ref(user.withReplies);
|
||||
menu = menu.concat([{
|
||||
const withRepliesRef = ref(user.withReplies ?? false);
|
||||
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
icon: 'ti ti-messages',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
|
@ -327,7 +349,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
|
||||
text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
|
||||
action: toggleNotify,
|
||||
}]);
|
||||
});
|
||||
|
||||
watch(withRepliesRef, (withReplies) => {
|
||||
misskeyApi('following/update', {
|
||||
userId: user.id,
|
||||
|
@ -338,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
});
|
||||
//}
|
||||
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||
action: toggleMute,
|
||||
|
@ -350,70 +373,68 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
icon: 'ti ti-ban',
|
||||
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
||||
action: toggleBlock,
|
||||
}]);
|
||||
});
|
||||
|
||||
if (user.isFollowed) {
|
||||
menu = menu.concat([{
|
||||
menuItems.push({
|
||||
icon: 'ti ti-link-off',
|
||||
text: i18n.ts.breakFollow,
|
||||
action: invalidateFollow,
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: reportAbuse,
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
if (user.host !== null) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.updateRemoteUser,
|
||||
action: userInfoUpdate,
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultStore.state.devMode) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
text: i18n.ts.copyUserId,
|
||||
action: () => {
|
||||
copyToClipboard(user.id);
|
||||
},
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
if ($i && meId === user.id) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.editProfile,
|
||||
action: () => {
|
||||
router.push('/settings/profile');
|
||||
},
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
if (userActions.length > 0) {
|
||||
menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({
|
||||
menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
|
||||
icon: 'ti ti-plug',
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(user);
|
||||
},
|
||||
}))]);
|
||||
})));
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (_DEV_) console.log('user menu cleanup', cleanups);
|
||||
for (const cl of cleanups) {
|
||||
cl();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
menu,
|
||||
cleanup,
|
||||
menu: menuItems,
|
||||
cleanup: () => {
|
||||
if (_DEV_) console.log('user menu cleanup', cleanups);
|
||||
for (const cl of cleanups) {
|
||||
cl();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,7 +41,9 @@ function toolsMenuItems(): MenuItem[] {
|
|||
}
|
||||
|
||||
export function openInstanceMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
text: instance.name ?? host,
|
||||
type: 'label',
|
||||
}, {
|
||||
|
@ -69,12 +71,18 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
text: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
to: '/ads',
|
||||
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
||||
type: 'link',
|
||||
to: '/invite',
|
||||
text: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
} : undefined, {
|
||||
});
|
||||
|
||||
if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) {
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
to: '/invite',
|
||||
text: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
text: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
|
@ -84,43 +92,69 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
text: i18n.ts.inquiry,
|
||||
icon: 'ti ti-help-circle',
|
||||
to: '/contact',
|
||||
}, (instance.impressumUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
href: instance.impressumUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (instance.tosUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.termsOfService,
|
||||
icon: 'ti ti-notebook',
|
||||
href: instance.tosUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (instance.privacyPolicyUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.privacyPolicy,
|
||||
icon: 'ti ti-shield-lock',
|
||||
href: instance.privacyPolicyUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
|
||||
});
|
||||
|
||||
if (instance.impressumUrl) {
|
||||
menuItems.push({
|
||||
type: 'a',
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
href: instance.impressumUrl,
|
||||
target: '_blank',
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.tosUrl) {
|
||||
menuItems.push({
|
||||
type: 'a',
|
||||
text: i18n.ts.termsOfService,
|
||||
icon: 'ti ti-notebook',
|
||||
href: instance.tosUrl,
|
||||
target: '_blank',
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.privacyPolicyUrl) {
|
||||
menuItems.push({
|
||||
type: 'a',
|
||||
text: i18n.ts.privacyPolicy,
|
||||
icon: 'ti ti-shield-lock',
|
||||
href: instance.privacyPolicyUrl,
|
||||
target: '_blank',
|
||||
});
|
||||
}
|
||||
|
||||
if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'a',
|
||||
text: i18n.ts.document,
|
||||
icon: 'ti ti-bulb',
|
||||
href: 'https://misskey-hub.net/docs/for-users/',
|
||||
target: '_blank',
|
||||
}, ($i) ? {
|
||||
text: i18n.ts._initialTutorial.launchTutorial,
|
||||
icon: 'ti ti-presentation',
|
||||
action: () => {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
} : undefined, {
|
||||
});
|
||||
|
||||
if ($i) {
|
||||
menuItems.push({
|
||||
text: i18n.ts._initialTutorial.launchTutorial,
|
||||
icon: 'ti ti-presentation',
|
||||
action: () => {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.aboutMisskey,
|
||||
to: '/about-misskey',
|
||||
}], ev.currentTarget ?? ev.target, {
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
|||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { antennasCache } from '@/cache.js';
|
||||
import { SoundStore } from '@/store.js';
|
||||
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
|
||||
|
|
|
@ -29,7 +29,7 @@ import * as os from '@/os.js';
|
|||
import { favoritedChannelsCache } from '@/cache.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { SoundStore } from '@/store.js';
|
||||
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
|
|
|
@ -46,7 +46,7 @@ import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed }
|
|||
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
provide('shouldHeaderThin', true);
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
@ -104,7 +104,27 @@ function toggleActive() {
|
|||
}
|
||||
|
||||
function getMenu() {
|
||||
let items: MenuItem[] = [{
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (props.menu) {
|
||||
menuItems.push(...props.menu, {
|
||||
type: 'divider',
|
||||
});
|
||||
}
|
||||
|
||||
if (props.refresher) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
action: () => {
|
||||
if (props.refresher) {
|
||||
props.refresher();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-settings',
|
||||
text: i18n.ts._deck.configureColumn,
|
||||
action: async () => {
|
||||
|
@ -129,74 +149,73 @@ function getMenu() {
|
|||
if (canceled) return;
|
||||
updateColumn(props.column.id, result);
|
||||
},
|
||||
});
|
||||
|
||||
const moveToMenuItems: MenuItem[] = [];
|
||||
|
||||
moveToMenuItems.push({
|
||||
icon: 'ti ti-arrow-left',
|
||||
text: i18n.ts._deck.swapLeft,
|
||||
action: () => {
|
||||
swapLeftColumn(props.column.id);
|
||||
},
|
||||
}, {
|
||||
type: 'parent',
|
||||
text: i18n.ts.move + '...',
|
||||
icon: 'ti ti-arrows-move',
|
||||
children: [{
|
||||
icon: 'ti ti-arrow-left',
|
||||
text: i18n.ts._deck.swapLeft,
|
||||
action: () => {
|
||||
swapLeftColumn(props.column.id);
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-arrow-right',
|
||||
text: i18n.ts._deck.swapRight,
|
||||
action: () => {
|
||||
swapRightColumn(props.column.id);
|
||||
},
|
||||
}, props.isStacked ? {
|
||||
icon: 'ti ti-arrow-right',
|
||||
text: i18n.ts._deck.swapRight,
|
||||
action: () => {
|
||||
swapRightColumn(props.column.id);
|
||||
},
|
||||
});
|
||||
|
||||
if (props.isStacked) {
|
||||
moveToMenuItems.push({
|
||||
icon: 'ti ti-arrow-up',
|
||||
text: i18n.ts._deck.swapUp,
|
||||
action: () => {
|
||||
swapUpColumn(props.column.id);
|
||||
},
|
||||
} : undefined, props.isStacked ? {
|
||||
}, {
|
||||
icon: 'ti ti-arrow-down',
|
||||
text: i18n.ts._deck.swapDown,
|
||||
action: () => {
|
||||
swapDownColumn(props.column.id);
|
||||
},
|
||||
} : undefined],
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
text: i18n.ts.move + '...',
|
||||
icon: 'ti ti-arrows-move',
|
||||
children: moveToMenuItems,
|
||||
}, {
|
||||
icon: 'ti ti-stack-2',
|
||||
text: i18n.ts._deck.stackLeft,
|
||||
action: () => {
|
||||
stackLeftColumn(props.column.id);
|
||||
},
|
||||
}, props.isStacked ? {
|
||||
icon: 'ti ti-window-maximize',
|
||||
text: i18n.ts._deck.popRight,
|
||||
action: () => {
|
||||
popRightColumn(props.column.id);
|
||||
},
|
||||
} : undefined, { type: 'divider' }, {
|
||||
});
|
||||
|
||||
if (props.isStacked) {
|
||||
menuItems.push({
|
||||
icon: 'ti ti-window-maximize',
|
||||
text: i18n.ts._deck.popRight,
|
||||
action: () => {
|
||||
popRightColumn(props.column.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ type: 'divider' }, {
|
||||
icon: 'ti ti-trash',
|
||||
text: i18n.ts.remove,
|
||||
danger: true,
|
||||
action: () => {
|
||||
removeColumn(props.column.id);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
if (props.menu) {
|
||||
items.unshift({ type: 'divider' });
|
||||
items = props.menu.concat(items);
|
||||
}
|
||||
|
||||
if (props.refresher) {
|
||||
items = [{
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
action: () => {
|
||||
if (props.refresher) {
|
||||
props.refresher();
|
||||
}
|
||||
},
|
||||
}, ...items];
|
||||
}
|
||||
|
||||
return items;
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function showSettingsMenu(ev: MouseEvent) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { SoundStore } from '@/store.js';
|
||||
import { userListsCache } from '@/cache.js';
|
||||
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
|
||||
|
|
|
@ -21,7 +21,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { SoundStore } from '@/store.js';
|
||||
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
|
|
|
@ -113,29 +113,41 @@ function onNote() {
|
|||
sound.playMisskeySfxFile(soundSetting.value);
|
||||
}
|
||||
|
||||
const menu = computed<MenuItem[]>(() => [{
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.timeline,
|
||||
action: setType,
|
||||
}, {
|
||||
icon: 'ti ti-bell',
|
||||
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||
action: () => soundSettingsButton(soundSetting),
|
||||
}, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: withRenotes,
|
||||
}, hasWithReplies(props.column.tl) ? {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: withReplies,
|
||||
disabled: onlyFiles,
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
disabled: hasWithReplies(props.column.tl) ? withReplies : false,
|
||||
}]);
|
||||
const menu = computed<MenuItem[]>(() => {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.timeline,
|
||||
action: setType,
|
||||
}, {
|
||||
icon: 'ti ti-bell',
|
||||
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||
action: () => soundSettingsButton(soundSetting),
|
||||
}, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: withRenotes,
|
||||
});
|
||||
|
||||
if (hasWithReplies(props.column.tl)) {
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: withReplies,
|
||||
disabled: onlyFiles,
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
disabled: hasWithReplies(props.column.tl) ? withReplies : false,
|
||||
});
|
||||
|
||||
return menuItems;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -40,6 +40,7 @@ import MkContainer from '@/components/MkContainer.vue';
|
|||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const name = 'timeline';
|
||||
|
||||
|
@ -109,11 +110,26 @@ const choose = async (ev) => {
|
|||
setSrc('list');
|
||||
},
|
||||
}));
|
||||
os.popupMenu([...availableBasicTimelines().map(tl => ({
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push(...availableBasicTimelines().map(tl => ({
|
||||
text: i18n.ts._timelines[tl],
|
||||
icon: basicTimelineIconClass(tl),
|
||||
action: () => { setSrc(tl); },
|
||||
})), antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
|
||||
})));
|
||||
|
||||
if (antennaItems.length > 0) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
menuItems.push(...antennaItems);
|
||||
}
|
||||
|
||||
if (listItems.length > 0) {
|
||||
menuItems.push({ type: 'divider' });
|
||||
menuItems.push(...listItems);
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => {
|
||||
menuOpened.value = false;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -358,6 +358,9 @@ type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']
|
|||
// @public (undocumented)
|
||||
type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1308,6 +1311,7 @@ declare namespace entities {
|
|||
AdminSystemWebhookShowResponse,
|
||||
AdminSystemWebhookUpdateRequest,
|
||||
AdminSystemWebhookUpdateResponse,
|
||||
AdminSystemWebhookTestRequest,
|
||||
AnnouncementsRequest,
|
||||
AnnouncementsResponse,
|
||||
AnnouncementsShowRequest,
|
||||
|
@ -1567,6 +1571,7 @@ declare namespace entities {
|
|||
IWebhooksShowResponse,
|
||||
IWebhooksUpdateRequest,
|
||||
IWebhooksDeleteRequest,
|
||||
IWebhooksTestRequest,
|
||||
InviteCreateResponse,
|
||||
InviteDeleteRequest,
|
||||
InviteListRequest,
|
||||
|
@ -2369,6 +2374,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co
|
|||
// @public (undocumented)
|
||||
type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
|
@ -960,6 +960,18 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||
*/
|
||||
request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
@ -2819,6 +2831,18 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -117,6 +117,7 @@ import type {
|
|||
AdminSystemWebhookShowResponse,
|
||||
AdminSystemWebhookUpdateRequest,
|
||||
AdminSystemWebhookUpdateResponse,
|
||||
AdminSystemWebhookTestRequest,
|
||||
AnnouncementsRequest,
|
||||
AnnouncementsResponse,
|
||||
AnnouncementsShowRequest,
|
||||
|
@ -376,6 +377,7 @@ import type {
|
|||
IWebhooksShowResponse,
|
||||
IWebhooksUpdateRequest,
|
||||
IWebhooksDeleteRequest,
|
||||
IWebhooksTestRequest,
|
||||
InviteCreateResponse,
|
||||
InviteDeleteRequest,
|
||||
InviteListRequest,
|
||||
|
@ -660,6 +662,7 @@ export type Endpoints = {
|
|||
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
|
||||
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
|
||||
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
|
||||
'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
|
||||
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
|
||||
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
|
||||
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
|
||||
|
@ -826,6 +829,7 @@ export type Endpoints = {
|
|||
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
|
||||
'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
|
||||
'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
|
||||
'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
|
||||
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
|
||||
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
|
||||
'invite/list': { req: InviteListRequest; res: InviteListResponse };
|
||||
|
|
|
@ -120,6 +120,7 @@ export type AdminSystemWebhookShowRequest = operations['admin___system-webhook__
|
|||
export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
|
||||
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
|
||||
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
|
||||
export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
|
||||
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
|
||||
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
|
||||
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
|
||||
|
@ -379,6 +380,7 @@ export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBod
|
|||
export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
|
||||
export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
|
||||
export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
|
||||
export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
|
||||
export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
|
||||
export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
|
||||
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -797,6 +797,16 @@ export type paths = {
|
|||
*/
|
||||
post: operations['admin___system-webhook___update'];
|
||||
};
|
||||
'/admin/system-webhook/test': {
|
||||
/**
|
||||
* admin/system-webhook/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||
*/
|
||||
post: operations['admin___system-webhook___test'];
|
||||
};
|
||||
'/announcements': {
|
||||
/**
|
||||
* announcements
|
||||
|
@ -2436,6 +2446,16 @@ export type paths = {
|
|||
*/
|
||||
post: operations['i___webhooks___delete'];
|
||||
};
|
||||
'/i/webhooks/test': {
|
||||
/**
|
||||
* i/webhooks/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
post: operations['i___webhooks___test'];
|
||||
};
|
||||
'/invite/create': {
|
||||
/**
|
||||
* invite/create
|
||||
|
@ -10327,6 +10347,71 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/system-webhook/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
|
||||
*/
|
||||
'admin___system-webhook___test': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
webhookId: string;
|
||||
/** @enum {string} */
|
||||
type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
|
||||
override?: {
|
||||
url?: string;
|
||||
secret?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* announcements
|
||||
* @description No description provided.
|
||||
|
@ -20146,6 +20231,71 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* i/webhooks/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:account*
|
||||
*/
|
||||
i___webhooks___test: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
webhookId: string;
|
||||
/** @enum {string} */
|
||||
type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction';
|
||||
override?: {
|
||||
url?: string;
|
||||
secret?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* invite/create
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in New Issue