Compare commits
27 Commits
eb1e9b9140
...
9e8068457c
Author | SHA1 | Date |
---|---|---|
Aleteoryx | 9e8068457c | |
かっこかり | 2c47f2eb4d | |
かっこかり | f5563c8304 | |
おさむのひと | 4ac8aad50a | |
かっこかり | ceb4640669 | |
かっこかり | 3bf63dd9c5 | |
かっこかり | ce95323e49 | |
FineArchs | daf9ae5d4a | |
syuilo | a5e61b8c19 | |
syuilo | cacdf9d939 | |
syuilo | 0134e6e420 | |
かっこかり | 6bd6af440f | |
かっこかり | 7d7a12d7d6 | |
dependabot[bot] | 887c709647 | |
かっこかり | 0e4b6d1dad | |
Juan Aguilar Santillana | 07f26bc8dd | |
syuilo | 366b79e459 | |
Kisaragi | 6b2072f4b1 | |
かっこかり | 1544ba9153 | |
かっこかり | be0906a6c7 | |
かっこかり | e0f54d6a68 | |
かっこかり | 837a8e15d8 | |
KanariKanaru | 0c2cfe31a3 | |
かっこかり | 05c944c2cc | |
かっこかり | f393b6b898 | |
Aleteoryx | b59bc50c48 | |
Aleteoryx | ed894efa1a |
|
@ -0,0 +1,203 @@
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# Misskey configuration
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
# ┌─────┐
|
||||||
|
#───┘ URL └─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Final accessible URL seen by a user.
|
||||||
|
url: 'http://misskey.local'
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
# ┌───────────────────────┐
|
||||||
|
#───┘ Port and TLS settings └───────────────────────────────────
|
||||||
|
|
||||||
|
#
|
||||||
|
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||||
|
#
|
||||||
|
# +----- https://example.tld/ ------------+
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# +---------------------------------------+
|
||||||
|
#
|
||||||
|
# You need to set up a reverse proxy. (e.g. nginx)
|
||||||
|
# An encrypted connection with HTTPS is highly recommended
|
||||||
|
# because tokens may be transferred in GET requests.
|
||||||
|
|
||||||
|
# The port that your Misskey server should listen on.
|
||||||
|
port: 61812
|
||||||
|
|
||||||
|
# ┌──────────────────────────┐
|
||||||
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
|
|
||||||
|
db:
|
||||||
|
host: db
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
# Database name
|
||||||
|
db: misskey
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
|
||||||
|
# Whether disable Caching queries
|
||||||
|
#disableCache: true
|
||||||
|
|
||||||
|
# Extra Connection options
|
||||||
|
#extra:
|
||||||
|
# ssl: true
|
||||||
|
|
||||||
|
dbReplications: false
|
||||||
|
|
||||||
|
# You can configure any number of replicas here
|
||||||
|
#dbSlaves:
|
||||||
|
# -
|
||||||
|
# host:
|
||||||
|
# port:
|
||||||
|
# db:
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
# -
|
||||||
|
# host:
|
||||||
|
# port:
|
||||||
|
# db:
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
#pass: example-pass
|
||||||
|
#prefix: example-prefix
|
||||||
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
# ┌───────────────────────────┐
|
||||||
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
#meilisearch:
|
||||||
|
# host: meilisearch
|
||||||
|
# port: 7700
|
||||||
|
# apiKey: ''
|
||||||
|
# ssl: true
|
||||||
|
# index: ''
|
||||||
|
|
||||||
|
# ┌───────────────┐
|
||||||
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
||||||
|
# You can select the ID generation method.
|
||||||
|
# You don't usually need to change this setting, but you can
|
||||||
|
# change it according to your preferences.
|
||||||
|
|
||||||
|
# Available methods:
|
||||||
|
# aid ... Short, Millisecond accuracy
|
||||||
|
# aidx ... Millisecond accuracy
|
||||||
|
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||||
|
# ulid ... Millisecond accuracy
|
||||||
|
# objectid ... This is left for backward compatibility
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# ID SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
id: 'aidx'
|
||||||
|
|
||||||
|
# ┌────────────────┐
|
||||||
|
#───┘ Error tracking └──────────────────────────────────────────
|
||||||
|
|
||||||
|
# Sentry is available for error tracking.
|
||||||
|
# See the Sentry documentation for more details on options.
|
||||||
|
|
||||||
|
#sentryForBackend:
|
||||||
|
# enableNodeProfiling: true
|
||||||
|
# options:
|
||||||
|
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||||
|
|
||||||
|
#sentryForFrontend:
|
||||||
|
# options:
|
||||||
|
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# Whether disable HSTS
|
||||||
|
#disableHsts: true
|
||||||
|
|
||||||
|
# Number of worker processes
|
||||||
|
#clusterLimit: 1
|
||||||
|
|
||||||
|
# Job concurrency per worker
|
||||||
|
# deliverJobConcurrency: 128
|
||||||
|
# inboxJobConcurrency: 16
|
||||||
|
|
||||||
|
# Job rate limiter
|
||||||
|
# deliverJobPerSec: 128
|
||||||
|
# inboxJobPerSec: 32
|
||||||
|
|
||||||
|
# Job attempts
|
||||||
|
# deliverJobMaxAttempts: 12
|
||||||
|
# inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Proxy for HTTP/HTTPS
|
||||||
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
proxyBypassHosts:
|
||||||
|
- api.deepl.com
|
||||||
|
- api-free.deepl.com
|
||||||
|
- www.recaptcha.net
|
||||||
|
- hcaptcha.com
|
||||||
|
- challenges.cloudflare.com
|
||||||
|
|
||||||
|
# Proxy for SMTP/SMTPS
|
||||||
|
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||||
|
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||||
|
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||||
|
|
||||||
|
# Media Proxy
|
||||||
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
|
# Proxy remote files (default: true)
|
||||||
|
proxyRemoteFiles: true
|
||||||
|
|
||||||
|
# Sign to ActivityPub GET request (default: true)
|
||||||
|
signToActivityPubGet: true
|
||||||
|
|
||||||
|
allowedPrivateNetworks: [
|
||||||
|
'127.0.0.1/32'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Upload or download file size limits (bytes)
|
||||||
|
#maxFileSize: 262144000
|
|
@ -3,6 +3,8 @@
|
||||||
set -xe
|
set -xe
|
||||||
|
|
||||||
sudo chown node node_modules
|
sudo chown node node_modules
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||||
git config --global --add safe.directory /workspace
|
git config --global --add safe.directory /workspace
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
corepack install
|
corepack install
|
||||||
|
@ -12,3 +14,4 @@ pnpm install --frozen-lockfile
|
||||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||||
pnpm build
|
pnpm build
|
||||||
pnpm migrate
|
pnpm migrate
|
||||||
|
pnpm exec cypress install
|
||||||
|
|
|
@ -35,6 +35,7 @@ coverage
|
||||||
!/.config/example.yml
|
!/.config/example.yml
|
||||||
!/.config/docker_example.yml
|
!/.config/docker_example.yml
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
|
!/.config/cypress-devcontainer.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
compose.yml
|
compose.yml
|
||||||
.devcontainer/compose.yml
|
.devcontainer/compose.yml
|
||||||
|
@ -64,6 +65,10 @@ temp
|
||||||
tsdoc-metadata.json
|
tsdoc-metadata.json
|
||||||
misskey-assets
|
misskey-assets
|
||||||
|
|
||||||
|
# Vite temporary files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# blender backups
|
# blender backups
|
||||||
*.blend1
|
*.blend1
|
||||||
*.blend2
|
*.blend2
|
||||||
|
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,18 +1,28 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
|
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
|
||||||
- サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
||||||
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
||||||
|
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
|
||||||
|
- Enhance: ScratchpadにUIインスペクターを追加
|
||||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||||
|
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
||||||
|
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
|
||||||
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
|
||||||
|
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
|
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
|
||||||
|
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
|
||||||
|
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
|
||||||
|
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
|
||||||
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
|
||||||
|
- Fix: Continue importing from file if single emoji import fails
|
||||||
|
|
||||||
## 2024.8.0
|
## 2024.8.0
|
||||||
|
|
||||||
|
|
|
@ -3121,7 +3121,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"narrow": string;
|
"narrow": string;
|
||||||
/**
|
/**
|
||||||
* 設定はページリロード後に反映されます。今すぐリロードしますか?
|
* 設定はページリロード後に反映されます。
|
||||||
*/
|
*/
|
||||||
"reloadToApplySetting": string;
|
"reloadToApplySetting": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9477,6 +9477,10 @@ export interface Locale extends ILocale {
|
||||||
* Webhookを削除しますか?
|
* Webhookを削除しますか?
|
||||||
*/
|
*/
|
||||||
"deleteConfirm": string;
|
"deleteConfirm": string;
|
||||||
|
/**
|
||||||
|
* スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
|
||||||
|
*/
|
||||||
|
"testRemarks": string;
|
||||||
};
|
};
|
||||||
"_abuseReport": {
|
"_abuseReport": {
|
||||||
"_notificationRecipient": {
|
"_notificationRecipient": {
|
||||||
|
|
|
@ -592,6 +592,8 @@ ascendingOrder: "昇順"
|
||||||
descendingOrder: "降順"
|
descendingOrder: "降順"
|
||||||
scratchpad: "スクラッチパッド"
|
scratchpad: "スクラッチパッド"
|
||||||
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
|
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
|
||||||
|
uiInspector: "UIインスペクター"
|
||||||
|
uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
|
||||||
output: "出力"
|
output: "出力"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||||
|
@ -776,7 +778,7 @@ left: "左"
|
||||||
center: "中央"
|
center: "中央"
|
||||||
wide: "広い"
|
wide: "広い"
|
||||||
narrow: "狭い"
|
narrow: "狭い"
|
||||||
reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
|
reloadToApplySetting: "設定はページリロード後に反映されます。"
|
||||||
needReloadToApply: "反映には再起動が必要です。"
|
needReloadToApply: "反映には再起動が必要です。"
|
||||||
showTitlebar: "タイトルバーを表示する"
|
showTitlebar: "タイトルバーを表示する"
|
||||||
clearCache: "キャッシュをクリア"
|
clearCache: "キャッシュをクリア"
|
||||||
|
@ -2512,6 +2514,7 @@ _webhookSettings:
|
||||||
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
abuseReportResolved: "ユーザーからの通報を処理したとき"
|
||||||
userCreated: "ユーザーが作成されたとき"
|
userCreated: "ユーザーが作成されたとき"
|
||||||
deleteConfirm: "Webhookを削除しますか?"
|
deleteConfirm: "Webhookを削除しますか?"
|
||||||
|
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
|
||||||
|
|
||||||
_abuseReport:
|
_abuseReport:
|
||||||
_notificationRecipient:
|
_notificationRecipient:
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||||
"cy:run": "pnpm cypress run",
|
"cy:run": "pnpm cypress run",
|
||||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
|
"e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"jest": "cd packages/backend && pnpm jest",
|
"jest": "cd packages/backend && pnpm jest",
|
||||||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||||
"test": "pnpm -r test",
|
"test": "pnpm -r test",
|
||||||
|
|
|
@ -100,7 +100,7 @@
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"bullmq": "5.10.4",
|
"bullmq": "5.10.4",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.4.2",
|
"got": "14.4.2",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "15.6.1",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"htmlescape": "1.1.1",
|
"htmlescape": "1.1.1",
|
||||||
"http-link-header": "1.1.3",
|
"http-link-header": "1.1.3",
|
||||||
|
|
|
@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
if (antenna.userListId == null) return false;
|
||||||
userListId: antenna.userListId!,
|
const exists = await this.userListMembershipsRepository.exists({
|
||||||
})).map(x => x.userId);
|
where: {
|
||||||
|
userListId: antenna.userListId,
|
||||||
if (!listUsers.includes(note.userId)) return false;
|
userId: note.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!exists) return false;
|
||||||
} else if (antenna.src === 'users') {
|
} else if (antenna.src === 'users') {
|
||||||
const accts = antenna.users.map(x => {
|
const accts = antenna.users.map(x => {
|
||||||
const { username, host } = Acct.parse(x);
|
const { username, host } = Acct.parse(x);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||||
|
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.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 $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||||
|
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
|
@ -359,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -503,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
WebhookTestService,
|
||||||
UtilityService,
|
UtilityService,
|
||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -791,6 +796,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
$WebhookTestService,
|
||||||
$UtilityService,
|
$UtilityService,
|
||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
|
|
|
@ -452,10 +452,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see UserWebhookDeliverJobData
|
* @see UserWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see UserWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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 = {
|
const data: UserWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -468,7 +473,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
return this.userWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
@ -479,10 +484,15 @@ export class QueueService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see SystemWebhookDeliverJobData
|
* @see SystemWebhookDeliverJobData
|
||||||
* @see WebhookDeliverProcessorService
|
* @see SystemWebhookDeliverProcessorService
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
|
public systemWebhookDeliver(
|
||||||
|
webhook: MiSystemWebhook,
|
||||||
|
type: SystemWebhookEventType,
|
||||||
|
content: unknown,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
const data: SystemWebhookDeliverJobData = {
|
const data: SystemWebhookDeliverJobData = {
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
@ -494,7 +504,7 @@ export class QueueService {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
|
||||||
attempts: 4,
|
attempts: opts?.attempts ?? 4,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
},
|
},
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
* SystemWebhook の一覧を取得する.
|
* SystemWebhook の一覧を取得する.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchSystemWebhooks(params?: {
|
public fetchSystemWebhooks(params?: {
|
||||||
ids?: MiSystemWebhook['id'][];
|
ids?: MiSystemWebhook['id'][];
|
||||||
isActive?: MiSystemWebhook['isActive'];
|
isActive?: MiSystemWebhook['isActive'];
|
||||||
on?: MiSystemWebhook['on'];
|
on?: MiSystemWebhook['on'];
|
||||||
|
@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
||||||
/**
|
/**
|
||||||
* SystemWebhook をWebhook配送キューに追加する
|
* SystemWebhook をWebhook配送キューに追加する
|
||||||
* @see QueueService.systemWebhookDeliver
|
* @see QueueService.systemWebhookDeliver
|
||||||
|
* // TODO: contentの型を厳格化する
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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'
|
const webhookEntity = typeof webhook === 'string'
|
||||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||||
: webhook;
|
: webhook;
|
||||||
if (!webhookEntity || !webhookEntity.isActive) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!webhookEntity.on.includes(type)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
import { type WebhooksRepository } from '@/models/_.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
|
||||||
return this.activeWebhooks;
|
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
|
@bindThis
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
const obj = JSON.parse(data);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -207,16 +207,41 @@ export class ApRequestService {
|
||||||
|
|
||||||
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
|
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const window = new Window();
|
const window = new Window({
|
||||||
|
settings: {
|
||||||
|
disableJavaScriptEvaluation: true,
|
||||||
|
disableJavaScriptFileLoading: true,
|
||||||
|
disableCSSFileLoading: true,
|
||||||
|
disableComputedStyleRendering: true,
|
||||||
|
handleDisabledFileLoadingAsSuccess: true,
|
||||||
|
navigation: {
|
||||||
|
disableMainFrameNavigation: true,
|
||||||
|
disableChildFrameNavigation: true,
|
||||||
|
disableChildPageNavigation: true,
|
||||||
|
disableFallbackToSetURL: true,
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
maxTimeout: 0,
|
||||||
|
maxIntervalTime: 0,
|
||||||
|
maxIntervalIterations: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
const document = window.document;
|
const document = window.document;
|
||||||
document.documentElement.innerHTML = html;
|
try {
|
||||||
|
document.documentElement.innerHTML = html;
|
||||||
|
|
||||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||||
if (alternate) {
|
if (alternate) {
|
||||||
const href = alternate.getAttribute('href');
|
const href = alternate.getAttribute('href');
|
||||||
if (href) {
|
if (href) {
|
||||||
return await this.signedGet(href, user, false);
|
return await this.signedGet(href, user, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// something went wrong parsing the HTML, ignore the whole thing
|
||||||
|
} finally {
|
||||||
|
window.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
* The getter will return a .bind version of the function
|
* The getter will return a .bind version of the function
|
||||||
* and memoize the result against a symbol on the instance
|
* and memoize the result against a symbol on the instance
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function bindThis(target: any, key: string, descriptor: any) {
|
export function bindThis(target: any, key: string, descriptor: any) {
|
||||||
let fn = descriptor.value;
|
const fn = descriptor.value;
|
||||||
|
|
||||||
if (typeof fn !== 'function') {
|
if (typeof fn !== 'function') {
|
||||||
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
|
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
|
||||||
|
@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
get() {
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (this === target.prototype || this.hasOwnProperty(key) ||
|
if (this === target.prototype || this.hasOwnProperty(key)) {
|
||||||
typeof fn !== 'function') {
|
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundFn = fn.bind(this);
|
const boundFn = fn.bind(this);
|
||||||
Object.defineProperty(this, key, {
|
Reflect.defineProperty(this, key, {
|
||||||
|
value: boundFn,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
writable: true,
|
||||||
return boundFn;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
fn = value;
|
|
||||||
delete this[key];
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return boundFn;
|
return boundFn;
|
||||||
},
|
},
|
||||||
set(value: any) {
|
|
||||||
fn = value;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||||
|
export type WebhookEventTypes = typeof webhookEventTypes[number];
|
||||||
|
|
||||||
@Entity('webhook')
|
@Entity('webhook')
|
||||||
export class MiWebhook {
|
export class MiWebhook {
|
||||||
|
|
|
@ -87,23 +87,30 @@ export class ImportCustomEmojisProcessorService {
|
||||||
await this.emojisRepository.delete({
|
await this.emojisRepository.delete({
|
||||||
name: emojiInfo.name,
|
name: emojiInfo.name,
|
||||||
});
|
});
|
||||||
const driveFile = await this.driveService.addFile({
|
try {
|
||||||
user: null,
|
const driveFile = await this.driveService.addFile({
|
||||||
path: emojiPath,
|
user: null,
|
||||||
name: record.fileName,
|
path: emojiPath,
|
||||||
force: true,
|
name: record.fileName,
|
||||||
});
|
force: true,
|
||||||
await this.customEmojiService.add({
|
});
|
||||||
name: emojiInfo.name,
|
await this.customEmojiService.add({
|
||||||
category: emojiInfo.category,
|
name: emojiInfo.name,
|
||||||
host: null,
|
category: emojiInfo.category,
|
||||||
aliases: emojiInfo.aliases,
|
host: null,
|
||||||
driveFile,
|
aliases: emojiInfo.aliases,
|
||||||
license: emojiInfo.license,
|
driveFile,
|
||||||
isSensitive: emojiInfo.isSensitive,
|
license: emojiInfo.license,
|
||||||
localOnly: emojiInfo.localOnly,
|
isSensitive: emojiInfo.isSensitive,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
localOnly: emojiInfo.localOnly,
|
||||||
});
|
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error || typeof e === 'string') {
|
||||||
|
this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
|
@ -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_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_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_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 from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.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_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.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_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_create from './endpoints/invite/create.js';
|
||||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||||
import * as ep___invite_list from './endpoints/invite/list.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_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_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_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: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||||
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.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 };
|
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_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_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_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_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_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.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_list,
|
||||||
$admin_systemWebhook_show,
|
$admin_systemWebhook_show,
|
||||||
$admin_systemWebhook_update,
|
$admin_systemWebhook_update,
|
||||||
|
$admin_systemWebhook_test,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
@ -1028,6 +1033,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$i_webhooks_test,
|
||||||
$invite_create,
|
$invite_create,
|
||||||
$invite_delete,
|
$invite_delete,
|
||||||
$invite_list,
|
$invite_list,
|
||||||
|
@ -1243,6 +1249,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_systemWebhook_list,
|
$admin_systemWebhook_list,
|
||||||
$admin_systemWebhook_show,
|
$admin_systemWebhook_show,
|
||||||
$admin_systemWebhook_update,
|
$admin_systemWebhook_update,
|
||||||
|
$admin_systemWebhook_test,
|
||||||
$announcements,
|
$announcements,
|
||||||
$announcements_show,
|
$announcements_show,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
|
@ -1409,6 +1416,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$i_webhooks_test,
|
||||||
$invite_create,
|
$invite_create,
|
||||||
$invite_delete,
|
$invite_delete,
|
||||||
$invite_list,
|
$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_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_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_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 from './endpoints/announcements.js';
|
||||||
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
import * as ep___announcements_show from './endpoints/announcements/show.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.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_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.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_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_create from './endpoints/invite/create.js';
|
||||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||||
import * as ep___invite_list from './endpoints/invite/list.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/list', ep___admin_systemWebhook_list],
|
||||||
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
['admin/system-webhook/show', ep___admin_systemWebhook_show],
|
||||||
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
['admin/system-webhook/update', ep___admin_systemWebhook_update],
|
||||||
|
['admin/system-webhook/test', ep___admin_systemWebhook_test],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['announcements/show', ep___announcements_show],
|
['announcements/show', ep___announcements_show],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
|
@ -645,6 +648,7 @@ const eps = [
|
||||||
['i/webhooks/show', ep___i_webhooks_show],
|
['i/webhooks/show', ep___i_webhooks_show],
|
||||||
['i/webhooks/update', ep___i_webhooks_update],
|
['i/webhooks/update', ep___i_webhooks_update],
|
||||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||||
|
['i/webhooks/test', ep___i_webhooks_test],
|
||||||
['invite/create', ep___invite_create],
|
['invite/create', ep___invite_create],
|
||||||
['invite/delete', ep___invite_delete],
|
['invite/delete', ep___invite_delete],
|
||||||
['invite/list', ep___invite_list],
|
['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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,12 @@ export const meta = {
|
||||||
code: 'TOO_MANY_ANTENNAS',
|
code: 'TOO_MANY_ANTENNAS',
|
||||||
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
|
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emptyKeyword: {
|
||||||
|
message: 'Either keywords or excludeKeywords is required.',
|
||||||
|
code: 'EMPTY_KEYWORD',
|
||||||
|
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
@ -87,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
||||||
throw new Error('either keywords or excludeKeywords is required.');
|
throw new ApiError(meta.errors.emptyKeyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentAntennasCount = await this.antennasRepository.countBy({
|
const currentAntennasCount = await this.antennasRepository.countBy({
|
||||||
|
|
|
@ -32,6 +32,12 @@ export const meta = {
|
||||||
code: 'NO_SUCH_USER_LIST',
|
code: 'NO_SUCH_USER_LIST',
|
||||||
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emptyKeyword: {
|
||||||
|
message: 'Either keywords or excludeKeywords is required.',
|
||||||
|
code: 'EMPTY_KEYWORD',
|
||||||
|
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
@ -85,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
if (ps.keywords && ps.excludeKeywords) {
|
if (ps.keywords && ps.excludeKeywords) {
|
||||||
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
|
||||||
throw new Error('either keywords or excludeKeywords is required.');
|
throw new ApiError(meta.errors.emptyKeyword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fetch the antenna
|
// Fetch the antenna
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks'],
|
tags: ['webhooks'],
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
|
||||||
import type { WebhooksRepository } from '@/models/_.js';
|
import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks', 'account'],
|
tags: ['webhooks', 'account'],
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
// TODO: UserWebhook schemaの適用
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['webhooks'],
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -228,6 +228,17 @@ describe('アンテナ', () => {
|
||||||
assert.deepStrictEqual(response, expected);
|
assert.deepStrictEqual(response, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('を作成する時キーワードが指定されていないとエラーになる', async () => {
|
||||||
|
await failedApiCall({
|
||||||
|
endpoint: 'antennas/create',
|
||||||
|
parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] },
|
||||||
|
user: alice
|
||||||
|
}, {
|
||||||
|
status: 400,
|
||||||
|
code: 'EMPTY_KEYWORD',
|
||||||
|
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a'
|
||||||
|
})
|
||||||
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region 更新(antennas/update)
|
//#region 更新(antennas/update)
|
||||||
|
|
||||||
|
@ -255,6 +266,18 @@ describe('アンテナ', () => {
|
||||||
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('を変更する時キーワードが指定されていないとエラーになる', async () => {
|
||||||
|
const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
|
||||||
|
await failedApiCall({
|
||||||
|
endpoint: 'antennas/update',
|
||||||
|
parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] },
|
||||||
|
user: alice
|
||||||
|
}, {
|
||||||
|
status: 400,
|
||||||
|
code: 'EMPTY_KEYWORD',
|
||||||
|
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region 表示(antennas/show)
|
//#region 表示(antennas/show)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -6,6 +7,7 @@
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { randomString } from '../utils.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||||
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { randomString } from '../utils.js';
|
|
||||||
|
|
||||||
describe('SystemWebhookService', () => {
|
describe('SystemWebhookService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
|
@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
on: ['abuseReport'],
|
on: ['abuseReport'],
|
||||||
});
|
});
|
||||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
|
|
||||||
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
on: ['abuseReport'],
|
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();
|
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
on: ['abuseReportResolved'],
|
on: ['abuseReportResolved'],
|
||||||
});
|
});
|
||||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
|
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
|
||||||
|
|
||||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,23 +10,42 @@ import '@tabler/icons-webfont/dist/tabler-icons.scss';
|
||||||
|
|
||||||
import '@/style.scss';
|
import '@/style.scss';
|
||||||
import { createApp, defineAsyncComponent } from 'vue';
|
import { createApp, defineAsyncComponent } from 'vue';
|
||||||
import lightTheme from '@@/themes/l-light.json5';
|
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||||
import darkTheme from '@@/themes/d-dark.json5';
|
import defaultDarkTheme from '@@/themes/d-dark.json5';
|
||||||
import { MediaProxy } from '@@/js/media-proxy.js';
|
import { MediaProxy } from '@@/js/media-proxy.js';
|
||||||
import { applyTheme } from './theme.js';
|
import { applyTheme, assertIsTheme } from '@/theme.js';
|
||||||
import { fetchCustomEmojis } from './custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
import { DI } from './di.js';
|
import { DI } from '@/di.js';
|
||||||
import { serverMetadata } from './server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url } from './config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import { parseEmbedParams } from '@@/js/embed-page.js';
|
import { parseEmbedParams } from '@@/js/embed-page.js';
|
||||||
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
||||||
|
|
||||||
console.info('Misskey Embed');
|
import type { Theme } from '@/theme.js';
|
||||||
|
|
||||||
|
console.log('Misskey Embed');
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const embedParams = parseEmbedParams(params);
|
const embedParams = parseEmbedParams(params);
|
||||||
|
|
||||||
console.info(embedParams);
|
if (_DEV_) console.log(embedParams);
|
||||||
|
|
||||||
|
function parseThemeOrNull(theme: string | null): Theme | null {
|
||||||
|
if (theme == null) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(theme);
|
||||||
|
if (assertIsTheme(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lightTheme = parseThemeOrNull(serverMetadata.defaultLightTheme) ?? defaultLightTheme;
|
||||||
|
const darkTheme = parseThemeOrNull(serverMetadata.defaultDarkTheme) ?? defaultDarkTheme;
|
||||||
|
|
||||||
if (embedParams.colorMode === 'dark') {
|
if (embedParams.colorMode === 'dark') {
|
||||||
applyTheme(darkTheme);
|
applyTheme(darkTheme);
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
import { host as hostRaw } from '@/config.js';
|
import { host as hostRaw } from '@@/js/config.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
user: Misskey.entities.UserLite;
|
user: Misskey.entities.UserLite;
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||||
import { WorkerMultiDispatch } from '@/to-be-shared/worker-multi-dispatch.js';
|
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
|
||||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||||
|
|
||||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import EmA from './EmA.vue';
|
import EmA from './EmA.vue';
|
||||||
import { url as local } from '@/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { host as localHost } from '@/config.js';
|
import { host as localHost } from '@@/js/config.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
username: string;
|
username: string;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import EmMention from '@/components/EmMention.vue';
|
||||||
import EmEmoji from '@/components/EmEmoji.vue';
|
import EmEmoji from '@/components/EmEmoji.vue';
|
||||||
import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
|
import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
|
||||||
import EmA from '@/components/EmA.vue';
|
import EmA from '@/components/EmA.vue';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
|
|
||||||
function safeParseFloat(str: unknown): number | null {
|
function safeParseFloat(str: unknown): number | null {
|
||||||
if (typeof str !== 'string' || str === '') return null;
|
if (typeof str !== 'string' || str === '') return null;
|
||||||
|
|
|
@ -121,8 +121,8 @@ import EmUserName from '@/components/EmUserName.vue';
|
||||||
import EmTime from '@/components/EmTime.vue';
|
import EmTime from '@/components/EmTime.vue';
|
||||||
import { userPage } from '@/utils.js';
|
import { userPage } from '@/utils.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { shouldCollapsed } from '@/to-be-shared/collapsed.js';
|
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
|
|
||||||
function getAppearNote(note: Misskey.entities.Note) {
|
function getAppearNote(note: Misskey.entities.Note) {
|
||||||
return Misskey.note.isPureRenote(note) ? note.renote : note;
|
return Misskey.note.isPureRenote(note) ? note.renote : note;
|
||||||
|
|
|
@ -142,9 +142,9 @@ import EmAcct from '@/components/EmAcct.vue';
|
||||||
import { userPage } from '@/utils.js';
|
import { userPage } from '@/utils.js';
|
||||||
import { notePage } from '@/utils.js';
|
import { notePage } from '@/utils.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { shouldCollapsed } from '@/to-be-shared/collapsed.js';
|
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||||
import { serverMetadata } from '@/server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import EmMfm from '@/components/EmMfm.js';
|
import EmMfm from '@/components/EmMfm.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
@ -35,8 +35,8 @@ import * as Misskey from 'misskey-js';
|
||||||
import EmMediaList from '@/components/EmMediaList.vue';
|
import EmMediaList from '@/components/EmMediaList.vue';
|
||||||
import EmPoll from '@/components/EmPoll.vue';
|
import EmPoll from '@/components/EmPoll.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import { shouldCollapsed } from '@/to-be-shared/collapsed.js';
|
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||||
import EmA from '@/components/EmA.vue';
|
import EmA from '@/components/EmA.vue';
|
||||||
import EmMfm from '@/components/EmMfm.js';
|
import EmMfm from '@/components/EmMfm.js';
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref, computed } from 'vue';
|
import { onMounted, onUnmounted, ref, computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { dateTimeFormat } from '@/to-be-shared/intl-const.js';
|
import { dateTimeFormat } from '@@/js/intl-const.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
time: Date | string | number | null;
|
time: Date | string | number | null;
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { toUnicode as decodePunycode } from 'punycode/';
|
import { toUnicode as decodePunycode } from 'punycode/';
|
||||||
import EmA from './EmA.vue';
|
import EmA from './EmA.vue';
|
||||||
import { url as local } from '@/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
|
|
||||||
function safeURIDecode(str: string): string {
|
function safeURIDecode(str: string): string {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import { I18n } from '@@/js/i18n.js';
|
import { I18n } from '@@/js/i18n.js';
|
||||||
import type { Locale } from '../../../locales/index.js';
|
import type { Locale } from '../../../locales/index.js';
|
||||||
import { locale } from '@/config.js';
|
import { locale } from '@@/js/config.js';
|
||||||
|
|
||||||
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@@/js/config.js';
|
||||||
|
|
||||||
export const pendingApiRequestsCount = ref(0);
|
export const pendingApiRequestsCount = ref(0);
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,8 @@ import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
|
||||||
import { misskeyApi } from '@/misskey-api.js';
|
import { misskeyApi } from '@/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { serverMetadata } from '@/server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url, instanceName } from '@/config.js';
|
import { url, instanceName } from '@@/js/config.js';
|
||||||
import { isLink } from '@/to-be-shared/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ misskeyApi('clips/show', {
|
||||||
|
|
||||||
.instanceIcon {
|
.instanceIcon {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -46,8 +46,8 @@ import XNotFound from '@/pages/not-found.vue';
|
||||||
import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
|
import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { serverMetadata } from '@/server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url, instanceName } from '@/config.js';
|
import { url, instanceName } from '@@/js/config.js';
|
||||||
import { isLink } from '@/to-be-shared/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ function top(ev: MouseEvent) {
|
||||||
|
|
||||||
.instanceIcon {
|
.instanceIcon {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -59,7 +59,7 @@ import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
|
||||||
import { misskeyApi } from '@/misskey-api.js';
|
import { misskeyApi } from '@/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { serverMetadata } from '@/server-metadata.js';
|
import { serverMetadata } from '@/server-metadata.js';
|
||||||
import { url, instanceName } from '@/config.js';
|
import { url, instanceName } from '@@/js/config.js';
|
||||||
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
import { defaultEmbedParams } from '@@/js/embed-page.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ misskeyApi('users/show', {
|
||||||
|
|
||||||
.instanceIcon {
|
.instanceIcon {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApi } from '@/misskey-api.js';
|
import { misskeyApi } from '@/misskey-api.js';
|
||||||
|
|
||||||
const providedMetaEl = document.getElementById('misskey_meta');
|
const providedMetaEl = document.getElementById('misskey_meta');
|
||||||
|
|
||||||
const _serverMetadata = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
||||||
|
|
||||||
// NOTE: devモードのときしか _serverMetadata が null になることは無い
|
// NOTE: devモードのときしか _serverMetadata が null になることは無い
|
||||||
export const serverMetadata = _serverMetadata ?? await misskeyApi('meta', {
|
export const serverMetadata: Misskey.entities.MetaDetailed = _serverMetadata ?? await misskeyApi('meta', {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,10 @@ export type Theme = {
|
||||||
|
|
||||||
let timeout: number | null = null;
|
let timeout: number | null = null;
|
||||||
|
|
||||||
|
export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
||||||
|
return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
|
||||||
|
}
|
||||||
|
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
|
|
||||||
|
@ -35,8 +39,6 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
document.documentElement.classList.remove('_themeChanging_');
|
document.documentElement.classList.remove('_themeChanging_');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
|
||||||
|
|
||||||
// Deep copy
|
// Deep copy
|
||||||
const _theme = JSON.parse(JSON.stringify(theme));
|
const _theme = JSON.parse(JSON.stringify(theme));
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||||
}
|
}
|
||||||
|
|
||||||
function compile(theme: Theme): Record<string, string> {
|
function compile(theme: Theme): Record<string, string> {
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
|
|
||||||
return prev + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
|
||||||
private symbol = Symbol('WorkerMultiDispatch');
|
|
||||||
private workers: Worker[] = [];
|
|
||||||
private terminated = false;
|
|
||||||
private prevWorkerNumber = 0;
|
|
||||||
private getUseWorkerNumber = defaultUseWorkerNumber;
|
|
||||||
private finalizationRegistry: FinalizationRegistry<symbol>;
|
|
||||||
|
|
||||||
constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
|
|
||||||
this.getUseWorkerNumber = getUseWorkerNumber;
|
|
||||||
for (let i = 0; i < concurrency; i++) {
|
|
||||||
this.workers.push(workerConstructor());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.finalizationRegistry = new FinalizationRegistry(() => {
|
|
||||||
this.terminate();
|
|
||||||
});
|
|
||||||
this.finalizationRegistry.register(this, this.symbol);
|
|
||||||
|
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
|
|
||||||
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
|
||||||
workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
|
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
|
|
||||||
this.prevWorkerNumber = workerNumber;
|
|
||||||
|
|
||||||
// 不毛だがunionをoverloadに突っ込めない
|
|
||||||
// https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error
|
|
||||||
// https://github.com/microsoft/TypeScript/issues/14107
|
|
||||||
if (Array.isArray(options)) {
|
|
||||||
this.workers[workerNumber].postMessage(message, options);
|
|
||||||
} else {
|
|
||||||
this.workers[workerNumber].postMessage(message, options);
|
|
||||||
}
|
|
||||||
return workerNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
|
||||||
this.workers.forEach(worker => {
|
|
||||||
worker.addEventListener('message', callback, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
|
||||||
this.workers.forEach(worker => {
|
|
||||||
worker.removeEventListener('message', callback, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public terminate() {
|
|
||||||
this.terminated = true;
|
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
|
|
||||||
this.workers.forEach(worker => {
|
|
||||||
worker.terminate();
|
|
||||||
});
|
|
||||||
this.workers = [];
|
|
||||||
this.finalizationRegistry.unregister(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isTerminated() {
|
|
||||||
return this.terminated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getWorkers() {
|
|
||||||
return this.workers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSymbol() {
|
|
||||||
return this.symbol;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,7 +40,7 @@ import XNotFound from '@/pages/not-found.vue';
|
||||||
|
|
||||||
const page = location.pathname.split('/')[2];
|
const page = location.pathname.split('/')[2];
|
||||||
const contentId = location.pathname.split('/')[3];
|
const contentId = location.pathname.split('/')[3];
|
||||||
console.log(page, contentId);
|
if (_DEV_) console.log(page, contentId);
|
||||||
|
|
||||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
|
|
||||||
export const acct = (user: Misskey.Acct) => {
|
export const acct = (user: Misskey.Acct) => {
|
||||||
return Misskey.acct.toString(user);
|
return Misskey.acct.toString(user);
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type FIXME = any;
|
||||||
|
|
||||||
|
declare const _LANGS_: string[][];
|
||||||
|
declare const _VERSION_: string;
|
||||||
|
declare const _ENV_: string;
|
||||||
|
declare const _DEV_: boolean;
|
||||||
|
declare const _PERF_PREFIX_: string;
|
||||||
|
declare const _DATA_TRANSFER_DRIVE_FILE_: string;
|
||||||
|
declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
|
||||||
|
declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||||
|
|
||||||
|
// for dev-mode
|
||||||
|
declare const _LANGS_FULL_: string[][];
|
||||||
|
|
||||||
|
// TagCanvas
|
||||||
|
interface Window {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
TagCanvas: any;
|
||||||
|
}
|
|
@ -14,7 +14,11 @@ export default [
|
||||||
},
|
},
|
||||||
...pluginVue.configs['flat/recommended'],
|
...pluginVue.configs['flat/recommended'],
|
||||||
{
|
{
|
||||||
files: ['js/**/*.{ts,vue}', '**/*.vue'],
|
files: [
|
||||||
|
'@types/**/*.ts',
|
||||||
|
'js/**/*.ts',
|
||||||
|
'**/*.vue',
|
||||||
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
|
...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
|
||||||
|
|
|
@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
|
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
|
||||||
const collapsed = note.cw == null && (
|
const collapsed = note.cw == null && (
|
||||||
note.text != null && (
|
(note.text != null && (
|
||||||
(note.text.includes('$[x2')) ||
|
(note.text.includes('$[x2')) ||
|
||||||
(note.text.includes('$[x3')) ||
|
(note.text.includes('$[x3')) ||
|
||||||
(note.text.includes('$[x4')) ||
|
(note.text.includes('$[x4')) ||
|
||||||
|
@ -15,7 +15,7 @@ export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): bo
|
||||||
(note.text.split('\n').length > 9) ||
|
(note.text.split('\n').length > 9) ||
|
||||||
(note.text.length > 500) ||
|
(note.text.length > 500) ||
|
||||||
(urls.length >= 4)
|
(urls.length >= 4)
|
||||||
) || note.files.length >= 5
|
)) || (note.files != null && note.files.length >= 5)
|
||||||
);
|
);
|
||||||
|
|
||||||
return collapsed;
|
return collapsed;
|
|
@ -3,6 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Locale } from '../../../locales/index.js';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
||||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||||
|
|
||||||
|
@ -10,9 +13,16 @@ export const host = address.host;
|
||||||
export const hostname = address.hostname;
|
export const hostname = address.hostname;
|
||||||
export const url = address.origin;
|
export const url = address.origin;
|
||||||
export const apiUrl = location.origin + '/api';
|
export const apiUrl = location.origin + '/api';
|
||||||
|
export const wsOrigin = location.origin;
|
||||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||||
export const langs = _LANGS_;
|
export const langs = _LANGS_;
|
||||||
const preParseLocale = localStorage.getItem('locale');
|
const preParseLocale = localStorage.getItem('locale');
|
||||||
export const locale = preParseLocale ? JSON.parse(preParseLocale) : null;
|
export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null;
|
||||||
export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName;
|
export const version = _VERSION_;
|
||||||
|
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
|
||||||
|
export const ui = localStorage.getItem('ui');
|
||||||
export const debug = localStorage.getItem('debug') === 'true';
|
export const debug = localStorage.getItem('debug') === 'true';
|
||||||
|
|
||||||
|
export function updateLocale(newLocale: Locale): void {
|
||||||
|
locale = newLocale;
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ export function char2fluentEmojiFilePath(char: string): string {
|
||||||
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
|
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
|
||||||
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
|
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
|
||||||
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
|
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
|
||||||
codes = codes.filter(x => x && x.length);
|
codes = codes.filter(x => x != null && x.length > 0);
|
||||||
const fileName = codes.map(x => x!.padStart(4, '0')).join('-');
|
const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-');
|
||||||
return `${fluentEmojiPngBase}/${fileName}.png`;
|
return `${fluentEmojiPngBase}/${fileName}.png`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { lang } from '@/config.js';
|
import { lang } from '@@/js/config.js';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
|
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
|
||||||
|
|
||||||
let _dateTimeFormat: Intl.DateTimeFormat;
|
let _dateTimeFormat: Intl.DateTimeFormat;
|
|
@ -3,16 +3,18 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
|
function defaultUseWorkerNumber(prev: number) {
|
||||||
return prev + 1;
|
return prev + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
type WorkerNumberGetter = (prev: number, totalWorkers: number) => number;
|
||||||
|
|
||||||
|
export class WorkerMultiDispatch<POST = unknown, RETURN = unknown> {
|
||||||
private symbol = Symbol('WorkerMultiDispatch');
|
private symbol = Symbol('WorkerMultiDispatch');
|
||||||
private workers: Worker[] = [];
|
private workers: Worker[] = [];
|
||||||
private terminated = false;
|
private terminated = false;
|
||||||
private prevWorkerNumber = 0;
|
private prevWorkerNumber = 0;
|
||||||
private getUseWorkerNumber = defaultUseWorkerNumber;
|
private getUseWorkerNumber: WorkerNumberGetter;
|
||||||
private finalizationRegistry: FinalizationRegistry<symbol>;
|
private finalizationRegistry: FinalizationRegistry<symbol>;
|
||||||
|
|
||||||
constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
|
constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
|
||||||
|
@ -29,7 +31,7 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
|
if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
|
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) {
|
||||||
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
||||||
workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
|
workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
|
||||||
if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
|
if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
|
||||||
|
@ -46,12 +48,14 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||||
return workerNumber;
|
return workerNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||||
this.workers.forEach(worker => {
|
this.workers.forEach(worker => {
|
||||||
worker.addEventListener('message', callback, options);
|
worker.addEventListener('message', callback, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||||
this.workers.forEach(worker => {
|
this.workers.forEach(worker => {
|
||||||
worker.removeEventListener('message', callback, options);
|
worker.removeEventListener('message', callback, options);
|
|
@ -16,7 +16,13 @@
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"],
|
||||||
|
"@@/*": ["./*"]
|
||||||
|
},
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
|
"./@types",
|
||||||
"./node_modules/@types"
|
"./node_modules/@types"
|
||||||
],
|
],
|
||||||
"lib": [
|
"lib": [
|
||||||
|
@ -25,6 +31,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"@types/**/*.ts",
|
||||||
"js/**/*"
|
"js/**/*"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { MenuButton } from '@/types/menu.js';
|
import { MenuButton } from '@/types/menu.js';
|
||||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@@/js/config.js';
|
||||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { compareVersions } from 'compare-versions';
|
||||||
import widgets from '@/widgets/index.js';
|
import widgets from '@/widgets/index.js';
|
||||||
import directives from '@/directives/index.js';
|
import directives from '@/directives/index.js';
|
||||||
import components from '@/components/index.js';
|
import components from '@/components/index.js';
|
||||||
import { version, lang, updateLocale, locale } from '@/config.js';
|
import { version, lang, updateLocale, locale } from '@@/js/config.js';
|
||||||
import { applyTheme } from '@/scripts/theme.js';
|
import { applyTheme } from '@/scripts/theme.js';
|
||||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
|
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
|
||||||
import { updateI18n } from '@/i18n.js';
|
import { updateI18n } from '@/i18n.js';
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||||
import { common } from './common.js';
|
import { common } from './common.js';
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'misskey-js';
|
||||||
import { ui } from '@/config.js';
|
import { ui } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkMention from './MkMention.vue';
|
import MkMention from './MkMention.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host as localHost } from '@/config.js';
|
import { host as localHost } from '@@/js/config.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
const user = ref<Misskey.entities.UserLite>();
|
const user = ref<Misskey.entities.UserLite>();
|
||||||
|
|
|
@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,9 @@ export default defineComponent({
|
||||||
setup(props, { slots, expose }) {
|
setup(props, { slots, expose }) {
|
||||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||||
|
|
||||||
function getDateText(time: string) {
|
function getDateText(dateInstance: Date) {
|
||||||
const date = new Date(time).getDate();
|
const date = dateInstance.getDate();
|
||||||
const month = new Date(time).getMonth() + 1;
|
const month = dateInstance.getMonth() + 1;
|
||||||
return i18n.tsx.monthAndDay({
|
return i18n.tsx.monthAndDay({
|
||||||
month: month.toString(),
|
month: month.toString(),
|
||||||
day: date.toString(),
|
day: date.toString(),
|
||||||
|
@ -62,9 +62,16 @@ export default defineComponent({
|
||||||
})[0];
|
})[0];
|
||||||
if (el.key == null && item.id) el.key = item.id;
|
if (el.key == null && item.id) el.key = item.id;
|
||||||
|
|
||||||
|
const date = new Date(item.createdAt);
|
||||||
|
const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
i !== props.items.length - 1 &&
|
i !== props.items.length - 1 &&
|
||||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
nextDate != null && (
|
||||||
|
date.getFullYear() !== nextDate.getFullYear() ||
|
||||||
|
date.getMonth() !== nextDate.getMonth() ||
|
||||||
|
date.getDate() !== nextDate.getDate()
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const separator = h('div', {
|
const separator = h('div', {
|
||||||
class: $style['separator'],
|
class: $style['separator'],
|
||||||
|
@ -78,12 +85,12 @@ export default defineComponent({
|
||||||
h('i', {
|
h('i', {
|
||||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||||
}),
|
}),
|
||||||
getDateText(item.createdAt),
|
getDateText(date),
|
||||||
]),
|
]),
|
||||||
h('span', {
|
h('span', {
|
||||||
class: $style['date-2'],
|
class: $style['date-2'],
|
||||||
}, [
|
}, [
|
||||||
getDateText(props.items[i + 1].createdAt),
|
getDateText(nextDate),
|
||||||
h('i', {
|
h('i', {
|
||||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
|
@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="thumbnail" :class="$style.root">
|
<div
|
||||||
|
ref="thumbnail"
|
||||||
|
:class="[
|
||||||
|
$style.root,
|
||||||
|
{ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
|
||||||
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
|
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
|
||||||
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
|
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
|
||||||
|
@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
file: Misskey.entities.DriveFile;
|
file: Misskey.entities.DriveFile;
|
||||||
fit: 'cover' | 'contain';
|
fit: 'cover' | 'contain';
|
||||||
|
highlightWhenSensitive?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const is = computed(() => {
|
const is = computed(() => {
|
||||||
|
@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sensitiveHighlight::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: inset 0 0 0 4px var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
.iconSub {
|
.iconSub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts._embedCodeGen.title }}</template>
|
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
||||||
|
|
||||||
<div :class="$style.embedCodeGenRoot">
|
<div :class="$style.embedCodeGenRoot">
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -103,7 +103,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
|
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
|
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
|
||||||
|
|
|
@ -611,6 +611,7 @@ defineExpose({
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@ -717,7 +718,7 @@ defineExpose({
|
||||||
|
|
||||||
> .item {
|
> .item {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0 3px;
|
||||||
width: var(--eachSize);
|
width: var(--eachSize);
|
||||||
height: var(--eachSize);
|
height: var(--eachSize);
|
||||||
contain: strict;
|
contain: strict;
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
class="file _button"
|
class="file _button"
|
||||||
>
|
>
|
||||||
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
|
||||||
<div v-if="viewMode === 'list'" class="body">
|
<div v-if="viewMode === 'list'" class="body">
|
||||||
<div>
|
<div>
|
||||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||||
|
|
|
@ -43,7 +43,7 @@ import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||||
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
|
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
|
||||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||||
|
|
||||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { instanceName } from '@/config.js';
|
import { instanceName } from '@@/js/config.js';
|
||||||
import { instance as Instance } from '@/instance.js';
|
import { instance as Instance } from '@/instance.js';
|
||||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref } from 'vue';
|
import { defineAsyncComponent, ref } from 'vue';
|
||||||
import { url as local } from '@/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { host as localHost } from '@/config.js';
|
import { host as localHost } from '@@/js/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
|
@ -163,6 +163,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
|
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
@ -195,8 +196,8 @@ import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
import { type Keymap } from '@/scripts/hotkey.js';
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
|
@ -506,16 +507,6 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLink = (el: HTMLElement): boolean => {
|
|
||||||
if (el.tagName === 'A') return true;
|
|
||||||
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
|
|
||||||
if (el.tagName === 'AUDIO') return true;
|
|
||||||
if (el.parentElement) {
|
|
||||||
return isLink(el.parentElement);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||||
if (window.getSelection()?.toString() !== '') return;
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
|
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||||
|
@ -222,7 +223,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
@ -468,14 +469,6 @@ function toggleReact() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement): boolean => {
|
|
||||||
if (el.tagName === 'A') return true;
|
|
||||||
if (el.parentElement) {
|
|
||||||
return isLink(el.parentElement);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||||
if (window.getSelection()?.toString() !== '') return;
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withTooltip="true"
|
:withTooltip="true"
|
||||||
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||||
:noStyle="true"
|
:noStyle="true"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100% !important; object-fit: contain;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withTooltip="true"
|
:withTooltip="true"
|
||||||
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||||
:noStyle="true"
|
:noStyle="true"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100% !important; object-fit: contain;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,7 @@ import RouterView from '@/components/global/RouterView.vue';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import { popout as _popout } from '@/scripts/popout.js';
|
import { popout as _popout } from '@/scripts/popout.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import { useScrollPositionManager } from '@/nirax.js';
|
import { useScrollPositionManager } from '@/nirax.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
@ -109,7 +109,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
import { host, url } from '@/config.js';
|
import { host, url } from '@@/js/config.js';
|
||||||
import { erase, unique } from '@/scripts/array.js';
|
import { erase, unique } from '@/scripts/array.js';
|
||||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||||
import { formatTimeString } from '@/scripts/format-time-string.js';
|
import { formatTimeString } from '@/scripts/format-time-string.js';
|
||||||
|
|
|
@ -63,7 +63,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
|
text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
|
@ -42,7 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkRadio from '@/components/MkRadio.vue';
|
import MkRadio from '@/components/MkRadio.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import * as config from '@/config.js';
|
import * as config from '@@/js/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const text = ref('');
|
const text = ref('');
|
||||||
|
|
|
@ -36,6 +36,7 @@ const emit = defineEmits<{
|
||||||
.icon {
|
.icon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
max-height: 60px;
|
||||||
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
|
@ -63,6 +63,7 @@ function getReactionName(reaction: string): string {
|
||||||
.reactionIcon {
|
.reactionIcon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
|
max-height: 60px;
|
||||||
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -72,7 +72,7 @@ import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { host as configHost } from '@/config.js';
|
import { host as configHost } from '@@/js/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { login } from '@/account.js';
|
import { login } from '@/account.js';
|
||||||
|
|
|
@ -84,7 +84,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkInput from './MkInput.vue';
|
import MkInput from './MkInput.vue';
|
||||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||||
import * as config from '@/config.js';
|
import * as config from '@@/js/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { login } from '@/account.js';
|
import { login } from '@/account.js';
|
||||||
|
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
|
@ -35,7 +35,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import MkMediaList from '@/components/MkMediaList.vue';
|
import MkMediaList from '@/components/MkMediaList.vue';
|
||||||
import MkPoll from '@/components/MkPoll.vue';
|
import MkPoll from '@/components/MkPoll.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@@/js/collapsed.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
|
export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
|
||||||
|
|
||||||
export type MkSystemWebhookEditorProps = {
|
export type MkSystemWebhookEditorProps = {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
|
|
|
@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFolder :defaultOpen="true">
|
<MkFolder :defaultOpen="true">
|
||||||
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
<div class="_gaps_s">
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
<div :class="$style.switchBox">
|
||||||
</MkSwitch>
|
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
|
||||||
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
|
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
|
</MkSwitch>
|
||||||
</MkSwitch>
|
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
|
||||||
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
|
</div>
|
||||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
|
<div :class="$style.switchBox">
|
||||||
</MkSwitch>
|
<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>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import {
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadingScope(async () => {
|
await loadingScope(async () => {
|
||||||
switch (mode.value) {
|
switch (mode.value) {
|
||||||
|
@ -235,4 +266,29 @@ onMounted(async () => {
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
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>
|
</style>
|
||||||
|
|
|
@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
|
||||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkSparkle from '@/components/MkSparkle.vue';
|
import MkSparkle from '@/components/MkSparkle.vue';
|
||||||
import { version } from '@/config.js';
|
import { version } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { confetti } from '@/scripts/confetti.js';
|
import { confetti } from '@/scripts/confetti.js';
|
||||||
|
|
||||||
|
|
|
@ -85,12 +85,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
||||||
import type { summaly } from '@misskey-dev/summaly';
|
import type { summaly } from '@misskey-dev/summaly';
|
||||||
import { url as local } from '@/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { versatileLang } from '@/scripts/intl-const.js';
|
import { versatileLang } from '@@/js/intl-const.js';
|
||||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { host as currentHost, hostname } from '@/config.js';
|
import { host as currentHost, hostname } from '@@/js/config.js';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
|
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
|
||||||
|
|
|
@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
|
||||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
|
@ -58,7 +58,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { instanceName } from '@/config.js';
|
import { instanceName } from '@@/js/config.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
|
@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import { widgets as widgetDefs } from '@/widgets/index.js';
|
import { widgets as widgetDefs } from '@/widgets/index.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { isLink } from '@@/js/is-link.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -98,13 +99,6 @@ const updateWidget = (id, data) => {
|
||||||
|
|
||||||
function onContextmenu(widget: Widget, ev: MouseEvent) {
|
function onContextmenu(widget: Widget, ev: MouseEvent) {
|
||||||
const element = ev.target as HTMLElement | null;
|
const element = ev.target as HTMLElement | null;
|
||||||
const isLink = (el: HTMLElement): boolean => {
|
|
||||||
if (el.tagName === 'A') return true;
|
|
||||||
if (el.parentElement) {
|
|
||||||
return isLink(el.parentElement);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if (element && isLink(element)) return;
|
if (element && isLink(element)) return;
|
||||||
if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
|
if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
|
||||||
if (window.getSelection()?.toString() !== '') return;
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue