Compare commits
27 Commits
525b30a609
...
1c9eeb3708
Author | SHA1 | Date |
---|---|---|
|
1c9eeb3708 | |
|
4d562e7439 | |
|
30df768d26 | |
|
ebd06becbf | |
|
cf35208777 | |
|
d1eddf0d88 | |
|
d5ad953c9e | |
|
e339293673 | |
|
a1ca68aadd | |
|
420365c17f | |
|
dc608aada0 | |
|
231c2c2e54 | |
|
2f8d02024a | |
|
4b98b446be | |
|
026ec40b3b | |
|
54fc232a23 | |
|
a3cc865e11 | |
|
d7b443d1f0 | |
|
4eca4e072b | |
|
e454c2acf8 | |
|
03a61ea3a6 | |
|
07ace0ccb8 | |
|
cda219fe09 | |
|
554e112864 | |
|
067f5762e3 | |
|
9c78649ab2 | |
|
3d11c4e940 |
|
@ -201,6 +201,12 @@ id: 'aidx'
|
||||||
# deliverJobMaxAttempts: 12
|
# deliverJobMaxAttempts: 12
|
||||||
# inboxJobMaxAttempts: 8
|
# inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
#deliverLocalUser:
|
||||||
|
# postNoteCreated:
|
||||||
|
# # Changes the way post-processing (notifications, sending webhooks, delivery to each timeline, delivery to ActivityPub) is called immediately after creating a note.
|
||||||
|
# # The configurable values are "immediate" and "worker" (the default is "immediate").
|
||||||
|
# runOn: 'immediate'
|
||||||
|
|
||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
|
|
@ -287,6 +287,13 @@ id: 'aidx'
|
||||||
#deliverJobMaxAttempts: 12
|
#deliverJobMaxAttempts: 12
|
||||||
#inboxJobMaxAttempts: 8
|
#inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
# deliver for LocalUsers
|
||||||
|
#deliverLocalUser:
|
||||||
|
# postNoteCreated:
|
||||||
|
# # Changes the way post-processing (notifications, sending webhooks, delivery to each timeline, delivery to ActivityPub) is called immediately after creating a note.
|
||||||
|
# # The configurable values are "immediate" and "worker" (the default is "immediate").
|
||||||
|
# runOn: 'immediate'
|
||||||
|
|
||||||
# Local address used for outgoing requests
|
# Local address used for outgoing requests
|
||||||
#outgoingAddress: 127.0.0.1
|
#outgoingAddress: 127.0.0.1
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "22.11.0"
|
"version": "22.11.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
|
"ghcr.io/devcontainers-extra/features/corepack:1": {
|
||||||
|
"version": "0.31.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
|
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
|
|
||||||
name: Ok To Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ok-to-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Only run for PRs, not issue comments
|
|
||||||
if: ${{ github.event.issue.pull_request }}
|
|
||||||
steps:
|
|
||||||
# Generate a GitHub App installation access token from an App ID and private key
|
|
||||||
# To create a new GitHub App:
|
|
||||||
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
|
|
||||||
# See app.yml for an example app manifest
|
|
||||||
- name: Generate token
|
|
||||||
id: generate_token
|
|
||||||
uses: tibdex/github-app-token@v2
|
|
||||||
with:
|
|
||||||
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
|
|
||||||
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: Slash Command Dispatch
|
|
||||||
uses: peter-evans/slash-command-dispatch@v4
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
|
||||||
with:
|
|
||||||
token: ${{ env.TOKEN }} # GitHub App installation access token
|
|
||||||
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
|
|
||||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-type: pull-request
|
|
||||||
commands: deploy
|
|
||||||
named-args: true
|
|
||||||
permission: write
|
|
|
@ -1,92 +0,0 @@
|
||||||
# Run secret-dependent integration tests only after /deploy approval
|
|
||||||
on:
|
|
||||||
repository_dispatch:
|
|
||||||
types: [deploy-command]
|
|
||||||
|
|
||||||
name: Deploy preview environment
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Repo owner has commented /deploy on a (fork-based) pull request
|
|
||||||
deploy-preview-environment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if:
|
|
||||||
github.event.client_payload.slash_command.sha != '' &&
|
|
||||||
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v7.0.1
|
|
||||||
id: check-id
|
|
||||||
env:
|
|
||||||
number: ${{ github.event.client_payload.pull_request.number }}
|
|
||||||
job: ${{ github.job }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const { data: pull } = await github.rest.pulls.get({
|
|
||||||
...context.repo,
|
|
||||||
pull_number: process.env.number
|
|
||||||
});
|
|
||||||
const ref = pull.head.sha;
|
|
||||||
|
|
||||||
const { data: checks } = await github.rest.checks.listForRef({
|
|
||||||
...context.repo,
|
|
||||||
ref
|
|
||||||
});
|
|
||||||
|
|
||||||
const check = checks.check_runs.filter(c => c.name === process.env.job);
|
|
||||||
|
|
||||||
return check[0].id;
|
|
||||||
|
|
||||||
- uses: actions/github-script@v7.0.1
|
|
||||||
env:
|
|
||||||
check_id: ${{ steps.check-id.outputs.result }}
|
|
||||||
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.checks.update({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: process.env.check_id,
|
|
||||||
status: 'in_progress',
|
|
||||||
details_url: process.env.details_url
|
|
||||||
});
|
|
||||||
|
|
||||||
# Check out merge commit
|
|
||||||
- name: Fork based /deploy checkout
|
|
||||||
uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
|
||||||
|
|
||||||
# <insert integration tests needing secrets>
|
|
||||||
- name: Context
|
|
||||||
uses: okteto/context@latest
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.OKTETO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy preview environment
|
|
||||||
uses: ikuradon/deploy-preview@latest
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
|
|
||||||
timeout: 15m
|
|
||||||
|
|
||||||
# Update check run called "integration-fork"
|
|
||||||
- uses: actions/github-script@v7.0.1
|
|
||||||
id: update-check-run
|
|
||||||
if: ${{ always() }}
|
|
||||||
env:
|
|
||||||
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
|
|
||||||
conclusion: ${{ job.status }}
|
|
||||||
check_id: ${{ steps.check-id.outputs.result }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { data: result } = await github.rest.checks.update({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: process.env.check_id,
|
|
||||||
status: 'completed',
|
|
||||||
conclusion: process.env.conclusion
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
|
@ -1,54 +0,0 @@
|
||||||
# file: .github/workflows/preview-closed.yaml
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
|
|
||||||
name: Destroy preview environment
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
destroy-preview-environment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v7.0.1
|
|
||||||
id: check-conclusion
|
|
||||||
env:
|
|
||||||
number: ${{ github.event.number }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const { data: pull } = await github.rest.pulls.get({
|
|
||||||
...context.repo,
|
|
||||||
pull_number: process.env.number
|
|
||||||
});
|
|
||||||
const ref = pull.head.sha;
|
|
||||||
|
|
||||||
const { data: checks } = await github.rest.checks.listForRef({
|
|
||||||
...context.repo,
|
|
||||||
ref
|
|
||||||
});
|
|
||||||
|
|
||||||
const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
|
|
||||||
|
|
||||||
if (check.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: result } = await github.rest.checks.get({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: check[0].id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.conclusion;
|
|
||||||
- name: Context
|
|
||||||
if: steps.check-conclusion.outputs.result == 'success'
|
|
||||||
uses: okteto/context@latest
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.OKTETO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Destroy preview environment
|
|
||||||
if: steps.check-conclusion.outputs.result == 'success'
|
|
||||||
uses: okteto/destroy-preview@latest
|
|
||||||
with:
|
|
||||||
name: pr-${{ github.event.number }}-syuilo
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,14 +1,21 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Feat: アクセストークン発行時に通知するように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: 投稿フォームで画像をプレビュー可能に
|
||||||
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
|
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
|
||||||
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
|
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
|
||||||
|
- Enhance: クライアントエラー画面の多言語対応
|
||||||
|
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
|
||||||
|
- Enhance: ノートに埋め込まれたメディアのコンテキストメニューから管理者用のファイル管理画面を開けるように ( #15440 )
|
||||||
|
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`
|
||||||
|
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
|
||||||
|
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2025.2.0
|
## 2025.2.0
|
||||||
|
@ -92,6 +99,7 @@
|
||||||
- Enhance: チャート更新時にDBに同時接続しないように
|
- Enhance: チャート更新時にDBに同時接続しないように
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830)
|
||||||
- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 )
|
- Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 )
|
||||||
|
- Enhance: ノート作成後の後処理をワーカープロセスで実行するオプションを追加( #15052 )
|
||||||
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
|
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
|
||||||
- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
|
- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
|
||||||
|
|
|
@ -9472,6 +9472,14 @@ export interface Locale extends ILocale {
|
||||||
* ログインがありました
|
* ログインがありました
|
||||||
*/
|
*/
|
||||||
"login": string;
|
"login": string;
|
||||||
|
/**
|
||||||
|
* アクセストークンが作成されました
|
||||||
|
*/
|
||||||
|
"createToken": string;
|
||||||
|
/**
|
||||||
|
* 心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。
|
||||||
|
*/
|
||||||
|
"createTokenDescription": ParameterizedString<"text">;
|
||||||
"_types": {
|
"_types": {
|
||||||
/**
|
/**
|
||||||
* すべて
|
* すべて
|
||||||
|
@ -10944,6 +10952,52 @@ export interface Locale extends ILocale {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"_bootErrors": {
|
||||||
|
/**
|
||||||
|
* 読み込みに失敗しました
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。
|
||||||
|
*/
|
||||||
|
"serverError": string;
|
||||||
|
/**
|
||||||
|
* 以下を行うと解決する可能性があります。
|
||||||
|
*/
|
||||||
|
"solution": string;
|
||||||
|
/**
|
||||||
|
* ブラウザおよびOSを最新バージョンに更新する
|
||||||
|
*/
|
||||||
|
"solution1": string;
|
||||||
|
/**
|
||||||
|
* アドブロッカーを無効にする
|
||||||
|
*/
|
||||||
|
"solution2": string;
|
||||||
|
/**
|
||||||
|
* ブラウザのキャッシュをクリアする
|
||||||
|
*/
|
||||||
|
"solution3": string;
|
||||||
|
/**
|
||||||
|
* (Tor Browser) dom.webaudio.enabledをtrueに設定する
|
||||||
|
*/
|
||||||
|
"solution4": string;
|
||||||
|
/**
|
||||||
|
* その他のオプション
|
||||||
|
*/
|
||||||
|
"otherOption": string;
|
||||||
|
/**
|
||||||
|
* クライアント設定とキャッシュを削除
|
||||||
|
*/
|
||||||
|
"otherOption1": string;
|
||||||
|
/**
|
||||||
|
* 簡易クライアントを起動
|
||||||
|
*/
|
||||||
|
"otherOption2": string;
|
||||||
|
/**
|
||||||
|
* 修復ツールを起動
|
||||||
|
*/
|
||||||
|
"otherOption3": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -2500,6 +2500,8 @@ _notification:
|
||||||
flushNotification: "通知の履歴をリセットする"
|
flushNotification: "通知の履歴をリセットする"
|
||||||
exportOfXCompleted: "{x}のエクスポートが完了しました"
|
exportOfXCompleted: "{x}のエクスポートが完了しました"
|
||||||
login: "ログインがありました"
|
login: "ログインがありました"
|
||||||
|
createToken: "アクセストークンが作成されました"
|
||||||
|
createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。"
|
||||||
|
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
|
@ -2927,3 +2929,16 @@ _captcha:
|
||||||
_unknown:
|
_unknown:
|
||||||
title: "CAPTCHAエラー"
|
title: "CAPTCHAエラー"
|
||||||
text: "想定外のエラーが発生しました。"
|
text: "想定外のエラーが発生しました。"
|
||||||
|
|
||||||
|
_bootErrors:
|
||||||
|
title: "読み込みに失敗しました"
|
||||||
|
serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
|
||||||
|
solution: "以下を行うと解決する可能性があります。"
|
||||||
|
solution1: "ブラウザおよびOSを最新バージョンに更新する"
|
||||||
|
solution2: "アドブロッカーを無効にする"
|
||||||
|
solution3: "ブラウザのキャッシュをクリアする"
|
||||||
|
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
|
||||||
|
otherOption: "その他のオプション"
|
||||||
|
otherOption1: "クライアント設定とキャッシュを削除"
|
||||||
|
otherOption2: "簡易クライアントを起動"
|
||||||
|
otherOption3: "修復ツールを起動"
|
||||||
|
|
|
@ -92,6 +92,12 @@ type Source = {
|
||||||
deliverJobMaxAttempts?: number;
|
deliverJobMaxAttempts?: number;
|
||||||
inboxJobMaxAttempts?: number;
|
inboxJobMaxAttempts?: number;
|
||||||
|
|
||||||
|
deliverLocalUser?: {
|
||||||
|
postNoteCreated?: {
|
||||||
|
runOn?: 'immediate' | 'worker';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mediaProxy?: string;
|
mediaProxy?: string;
|
||||||
proxyRemoteFiles?: boolean;
|
proxyRemoteFiles?: boolean;
|
||||||
videoThumbnailGenerator?: string;
|
videoThumbnailGenerator?: string;
|
||||||
|
@ -162,6 +168,11 @@ export type Config = {
|
||||||
relationshipJobPerSec: number | undefined;
|
relationshipJobPerSec: number | undefined;
|
||||||
deliverJobMaxAttempts: number | undefined;
|
deliverJobMaxAttempts: number | undefined;
|
||||||
inboxJobMaxAttempts: number | undefined;
|
inboxJobMaxAttempts: number | undefined;
|
||||||
|
deliverLocalUser?: {
|
||||||
|
postNoteCreated?: {
|
||||||
|
runOn?: 'immediate' | 'worker';
|
||||||
|
}
|
||||||
|
};
|
||||||
proxyRemoteFiles: boolean | undefined;
|
proxyRemoteFiles: boolean | undefined;
|
||||||
signToActivityPubGet: boolean | undefined;
|
signToActivityPubGet: boolean | undefined;
|
||||||
logging?: {
|
logging?: {
|
||||||
|
@ -299,6 +310,7 @@ export function loadConfig(): Config {
|
||||||
relationshipJobPerSec: config.relationshipJobPerSec,
|
relationshipJobPerSec: config.relationshipJobPerSec,
|
||||||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||||
|
deliverLocalUser: config.deliverLocalUser,
|
||||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||||
signToActivityPubGet: config.signToActivityPubGet ?? true,
|
signToActivityPubGet: config.signToActivityPubGet ?? true,
|
||||||
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
mediaProxy: externalMediaProxy ?? internalMediaProxy,
|
||||||
|
|
|
@ -173,7 +173,8 @@ export class DriveService {
|
||||||
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`;
|
||||||
|
|
||||||
// for original
|
// for original
|
||||||
const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
const prefix = this.meta.objectStoragePrefix ? `${this.meta.objectStoragePrefix}/` : '';
|
||||||
|
const key = `${prefix}${randomUUID()}${ext}`;
|
||||||
const url = `${ baseUrl }/${ key }`;
|
const url = `${ baseUrl }/${ key }`;
|
||||||
|
|
||||||
// for alts
|
// for alts
|
||||||
|
@ -190,7 +191,7 @@ export class DriveService {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (alts.webpublic) {
|
if (alts.webpublic) {
|
||||||
webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||||
|
@ -198,7 +199,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alts.thumbnail) {
|
if (alts.thumbnail) {
|
||||||
thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||||
|
|
|
@ -8,6 +8,8 @@ import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
|
@ -123,6 +125,8 @@ type MinimumUser = {
|
||||||
uri: MiUser['uri'];
|
uri: MiUser['uri'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PostNoteUser = Pick<MiUser, 'id' | 'host' | 'username' | 'isBot'>
|
||||||
|
|
||||||
type Option = {
|
type Option = {
|
||||||
createdAt?: Date | null;
|
createdAt?: Date | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
|
@ -145,10 +149,26 @@ type Option = {
|
||||||
app?: MiApp | null;
|
app?: MiApp | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PostNoteCreateData = {
|
||||||
|
note: MiNote;
|
||||||
|
user: PostNoteUser;
|
||||||
|
data: Option;
|
||||||
|
silent: boolean;
|
||||||
|
tags: string[];
|
||||||
|
mentionedUsers: MinimumUser[];
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
|
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
|
||||||
|
/**
|
||||||
|
* @see NoteCreateService#postNoteCreated
|
||||||
|
* @see QueueService#localUserDeliver
|
||||||
|
* @see LocalUserDeliverProcessorService
|
||||||
|
*/
|
||||||
|
private readonly postNoteCreatedCaller: (p: PostNoteCreateData) => void | Promise<void>;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
|
@ -219,8 +239,32 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
this.logger = this.loggerService.getLogger('note-create-service');
|
||||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||||
|
|
||||||
|
const runOnImmediate = (p: PostNoteCreateData) => {
|
||||||
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
|
() => this.postNoteCreated(p),
|
||||||
|
() => { /* aborted, ignore this */ },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runOnWorker = async (p: PostNoteCreateData) => {
|
||||||
|
await this.queueService.localUserDeliver({
|
||||||
|
type: 'postNoteCreated',
|
||||||
|
data: p,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.postNoteCreatedCaller = this.config.deliverLocalUser?.postNoteCreated?.runOn === 'worker'
|
||||||
|
? runOnWorker
|
||||||
|
: runOnImmediate;
|
||||||
|
const runOnName = this.config.deliverLocalUser?.postNoteCreated?.runOn
|
||||||
|
? this.config.deliverLocalUser.postNoteCreated.runOn
|
||||||
|
: 'immediate';
|
||||||
|
this.logger.info(`postNoteCreatedCaller: ${runOnName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -394,10 +438,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
this.postNoteCreatedCaller({ note, user, data, silent, tags, mentionedUsers });
|
||||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
|
||||||
() => { /* aborted, ignore this */ },
|
|
||||||
);
|
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
@ -500,13 +541,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queueから呼ぶためにpublicにしているが、通常は直接呼ばないこと
|
||||||
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async postNoteCreated(note: MiNote, user: {
|
public async postNoteCreated(params: PostNoteCreateData) {
|
||||||
id: MiUser['id'];
|
const { note, user, data, silent, tags, mentionedUsers } = params;
|
||||||
username: MiUser['username'];
|
|
||||||
host: MiUser['host'];
|
|
||||||
isBot: MiUser['isBot'];
|
|
||||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
|
||||||
this.notesChart.update(note, true);
|
this.notesChart.update(note, true);
|
||||||
if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
|
if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) {
|
||||||
this.perUserNotesChart.update(user, note, true);
|
this.perUserNotesChart.update(user, note, true);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
RelationshipJobData,
|
RelationshipJobData,
|
||||||
UserWebhookDeliverJobData,
|
UserWebhookDeliverJobData,
|
||||||
SystemWebhookDeliverJobData,
|
SystemWebhookDeliverJobData,
|
||||||
|
LocalUserDeliverJobData,
|
||||||
} from '../queue/types.js';
|
} from '../queue/types.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
|
||||||
export type ObjectStorageQueue = Bull.Queue;
|
export type ObjectStorageQueue = Bull.Queue;
|
||||||
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||||
|
export type LocalUserDeliverQueue = Bull.Queue<LocalUserDeliverJobData>;
|
||||||
|
|
||||||
const $system: Provider = {
|
const $system: Provider = {
|
||||||
provide: 'queue:system',
|
provide: 'queue:system',
|
||||||
|
@ -83,6 +85,12 @@ const $systemWebhookDeliver: Provider = {
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $localUserDeliver: Provider = {
|
||||||
|
provide: 'queue:localUserDeliver',
|
||||||
|
useFactory: (config: Config) => new Bull.Queue(QUEUE.LOCAL_USER_DELIVER, baseQueueOptions(config, QUEUE.LOCAL_USER_DELIVER)),
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
],
|
],
|
||||||
|
@ -96,6 +104,7 @@ const $systemWebhookDeliver: Provider = {
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$userWebhookDeliver,
|
$userWebhookDeliver,
|
||||||
$systemWebhookDeliver,
|
$systemWebhookDeliver,
|
||||||
|
$localUserDeliver,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$system,
|
$system,
|
||||||
|
@ -107,6 +116,7 @@ const $systemWebhookDeliver: Provider = {
|
||||||
$objectStorage,
|
$objectStorage,
|
||||||
$userWebhookDeliver,
|
$userWebhookDeliver,
|
||||||
$systemWebhookDeliver,
|
$systemWebhookDeliver,
|
||||||
|
$localUserDeliver,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class QueueModule implements OnApplicationShutdown {
|
export class QueueModule implements OnApplicationShutdown {
|
||||||
|
@ -120,6 +130,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
|
@Inject('queue:localUserDeliver') public localUserDeliverQueue: LocalUserDeliverQueue,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
|
@ -136,6 +147,7 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
this.objectStorageQueue.close(),
|
this.objectStorageQueue.close(),
|
||||||
this.userWebhookDeliverQueue.close(),
|
this.userWebhookDeliverQueue.close(),
|
||||||
this.systemWebhookDeliverQueue.close(),
|
this.systemWebhookDeliverQueue.close(),
|
||||||
|
this.localUserDeliverQueue.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||||
import type {
|
import type {
|
||||||
DbJobData,
|
DbJobData,
|
||||||
DeliverJobData,
|
DeliverJobData,
|
||||||
|
LocalUserDeliverJobData,
|
||||||
RelationshipJobData,
|
RelationshipJobData,
|
||||||
SystemWebhookDeliverJobData,
|
SystemWebhookDeliverJobData,
|
||||||
ThinUser,
|
ThinUser,
|
||||||
|
@ -29,6 +30,7 @@ import type {
|
||||||
DeliverQueue,
|
DeliverQueue,
|
||||||
EndedPollNotificationQueue,
|
EndedPollNotificationQueue,
|
||||||
InboxQueue,
|
InboxQueue,
|
||||||
|
LocalUserDeliverQueue,
|
||||||
ObjectStorageQueue,
|
ObjectStorageQueue,
|
||||||
RelationshipQueue,
|
RelationshipQueue,
|
||||||
SystemQueue,
|
SystemQueue,
|
||||||
|
@ -53,6 +55,7 @@ export class QueueService {
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
|
@Inject('queue:localUserDeliver') public localUserDeliverQueue: LocalUserDeliverQueue,
|
||||||
) {
|
) {
|
||||||
this.systemQueue.add('tickCharts', {
|
this.systemQueue.add('tickCharts', {
|
||||||
}, {
|
}, {
|
||||||
|
@ -528,6 +531,21 @@ export class QueueService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public localUserDeliver(
|
||||||
|
data: LocalUserDeliverJobData,
|
||||||
|
opts?: { attempts?: number },
|
||||||
|
) {
|
||||||
|
return this.localUserDeliverQueue.add('localUserDeliver-' + data.type, data, {
|
||||||
|
attempts: opts?.attempts ?? 1,
|
||||||
|
backoff: {
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.deliverQueue.once('cleaned', (jobs, status) => {
|
this.deliverQueue.once('cleaned', (jobs, status) => {
|
||||||
|
|
|
@ -90,6 +90,10 @@ export type MiNotification = {
|
||||||
type: 'login';
|
type: 'login';
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
} | {
|
||||||
|
type: 'createToken';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
} | {
|
} | {
|
||||||
type: 'app';
|
type: 'app';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -332,6 +332,16 @@ export const packedNotificationSchema = {
|
||||||
enum: ['login'],
|
enum: ['login'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['createToken'],
|
||||||
|
},
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { LocalUserDeliverProcessorService } from '@/queue/processors/LocalUserDeliverProcessorService.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
|
@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
AggregateRetentionProcessorService,
|
AggregateRetentionProcessorService,
|
||||||
CheckExpiredMutingsProcessorService,
|
CheckExpiredMutingsProcessorService,
|
||||||
CheckModeratorsActivityProcessorService,
|
CheckModeratorsActivityProcessorService,
|
||||||
|
LocalUserDeliverProcessorService,
|
||||||
QueueProcessorService,
|
QueueProcessorService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
import { QUEUE, baseQueueOptions } from './const.js';
|
import { QUEUE, baseQueueOptions } from './const.js';
|
||||||
|
import { LocalUserDeliverProcessorService } from './processors/LocalUserDeliverProcessorService.js';
|
||||||
|
|
||||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||||
function httpRelatedBackoff(attemptsMade: number) {
|
function httpRelatedBackoff(attemptsMade: number) {
|
||||||
|
@ -84,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private relationshipQueueWorker: Bull.Worker;
|
private relationshipQueueWorker: Bull.Worker;
|
||||||
private objectStorageQueueWorker: Bull.Worker;
|
private objectStorageQueueWorker: Bull.Worker;
|
||||||
private endedPollNotificationQueueWorker: Bull.Worker;
|
private endedPollNotificationQueueWorker: Bull.Worker;
|
||||||
|
private localUserDeliverQueueWorker: Bull.Worker;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
|
@ -123,6 +125,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||||
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
|
private localUserDeliverProcessorService: LocalUserDeliverProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
|
|
||||||
|
@ -517,6 +520,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region LocalUser deliver
|
||||||
|
{
|
||||||
|
this.localUserDeliverQueueWorker = new Bull.Worker(QUEUE.LOCAL_USER_DELIVER, (job) => {
|
||||||
|
if (this.config.sentryForBackend) {
|
||||||
|
return Sentry.startSpan({ name: 'Queue: LocalUserDeliver' }, () => this.localUserDeliverProcessorService.process(job));
|
||||||
|
} else {
|
||||||
|
return this.localUserDeliverProcessorService.process(job);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...baseQueueOptions(this.config, QUEUE.LOCAL_USER_DELIVER),
|
||||||
|
autorun: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -531,6 +549,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.relationshipQueueWorker.run(),
|
this.relationshipQueueWorker.run(),
|
||||||
this.objectStorageQueueWorker.run(),
|
this.objectStorageQueueWorker.run(),
|
||||||
this.endedPollNotificationQueueWorker.run(),
|
this.endedPollNotificationQueueWorker.run(),
|
||||||
|
this.localUserDeliverQueueWorker.run(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,6 +565,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
this.relationshipQueueWorker.close(),
|
this.relationshipQueueWorker.close(),
|
||||||
this.objectStorageQueueWorker.close(),
|
this.objectStorageQueueWorker.close(),
|
||||||
this.endedPollNotificationQueueWorker.close(),
|
this.endedPollNotificationQueueWorker.close(),
|
||||||
|
this.localUserDeliverQueueWorker.close(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const QUEUE = {
|
||||||
OBJECT_STORAGE: 'objectStorage',
|
OBJECT_STORAGE: 'objectStorage',
|
||||||
USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
|
USER_WEBHOOK_DELIVER: 'userWebhookDeliver',
|
||||||
SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
|
SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver',
|
||||||
|
LOCAL_USER_DELIVER: 'localUserDeliver',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import type { LocalUserDeliverJobData } from '@/queue/types.js';
|
||||||
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalUserDeliverProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
|
private noteCreateService: NoteCreateService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('clean');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<LocalUserDeliverJobData>): Promise<void> {
|
||||||
|
try {
|
||||||
|
switch (job.data.type) {
|
||||||
|
case 'postNoteCreated': {
|
||||||
|
const data = job.data;
|
||||||
|
await this.noteCreateService.postNoteCreated(data.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this.logger.error(`Failed to process job. ${job.data}`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Failed to process. ${job.data}`, new Error(`unknown: ${e}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { PostNoteCreateData } from '@/core/NoteCreateService.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
@ -130,6 +131,14 @@ export type UserWebhookDeliverJobData<T extends WebhookEventTypes = WebhookEvent
|
||||||
eventId: string;
|
eventId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 他のも増えそうな余地あるので
|
||||||
|
export type LocalUserDeliverJobData = LocalUserDeliverPostNoteCreatedJobData;
|
||||||
|
|
||||||
|
type LocalUserDeliverPostNoteCreatedJobData = {
|
||||||
|
type: 'postNoteCreated';
|
||||||
|
data: PostNoteCreateData;
|
||||||
|
}
|
||||||
|
|
||||||
export type ThinUser = {
|
export type ThinUser = {
|
||||||
id: MiUser['id'];
|
id: MiUser['id'];
|
||||||
};
|
};
|
||||||
|
|
|
@ -512,6 +512,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
federation: {
|
federation: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
enum: ['all', 'specified', 'none'],
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
federationHosts: {
|
federationHosts: {
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
await this.userFollowingService.unfollow(follower, followee);
|
await this.userFollowingService.unfollow(follower, followee);
|
||||||
|
|
||||||
return await this.userEntityService.pack(followee.id, me);
|
return await this.userEntityService.pack(follower.id, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AccessTokensRepository } from '@/models/_.js';
|
import type { AccessTokensRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private accessTokensRepository: AccessTokensRepository,
|
private accessTokensRepository: AccessTokensRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Generate access token
|
// Generate access token
|
||||||
|
@ -71,6 +73,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
permission: ps.permission,
|
permission: ps.permission,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// アクセストークンが生成されたことを通知
|
||||||
|
this.notificationService.createNotification(me.id, 'createToken', {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: accessToken,
|
token: accessToken,
|
||||||
};
|
};
|
||||||
|
|
|
@ -210,9 +210,15 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
||||||
|
|
||||||
spec.paths['/' + endpoint.name] = {
|
spec.paths['/' + endpoint.name] = {
|
||||||
...(endpoint.meta.allowGet ? {
|
...(endpoint.meta.allowGet ? {
|
||||||
get: info,
|
get: {
|
||||||
|
...info,
|
||||||
|
operationId: 'get___' + info.operationId,
|
||||||
|
},
|
||||||
} : {}),
|
} : {}),
|
||||||
post: info,
|
post: {
|
||||||
|
...info,
|
||||||
|
operationId: 'post___' + info.operationId,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import type {
|
||||||
SystemQueue,
|
SystemQueue,
|
||||||
UserWebhookDeliverQueue,
|
UserWebhookDeliverQueue,
|
||||||
SystemWebhookDeliverQueue,
|
SystemWebhookDeliverQueue,
|
||||||
|
LocalUserDeliverQueue,
|
||||||
} from '@/core/QueueModule.js';
|
} from '@/core/QueueModule.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
@ -143,6 +144,7 @@ export class ClientServerService {
|
||||||
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
|
||||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||||
|
@Inject('queue:localUserDeliver') public localUserDeliverQueue: LocalUserDeliverQueue,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -271,6 +273,7 @@ export class ClientServerService {
|
||||||
this.objectStorageQueue,
|
this.objectStorageQueue,
|
||||||
this.userWebhookDeliverQueue,
|
this.userWebhookDeliverQueue,
|
||||||
this.systemWebhookDeliverQueue,
|
this.systemWebhookDeliverQueue,
|
||||||
|
this.localUserDeliverQueue,
|
||||||
].map(q => new BullMQAdapter(q)),
|
].map(q => new BullMQAdapter(q)),
|
||||||
serverAdapter: bullBoardServerAdapter,
|
serverAdapter: bullBoardServerAdapter,
|
||||||
});
|
});
|
||||||
|
|
|
@ -114,13 +114,17 @@
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
||||||
|
|
||||||
|
const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
|
||||||
|
const reload = locale?.reload || 'Reload';
|
||||||
|
|
||||||
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
|
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
|
||||||
<div class="message">読み込みに失敗しました</div>
|
<div class="message">${title}</div>
|
||||||
<div class="submessage">Failed to initialize Misskey</div>
|
|
||||||
<div class="submessage">Error Code: ${code}</div>
|
<div class="submessage">Error Code: ${code}</div>
|
||||||
<button onclick="location.reload(!0)">
|
<button onclick="location.reload(!0)">
|
||||||
<div>リロード</div>
|
<div>${reload}</div>
|
||||||
<div><small>Reload</small></div>
|
|
||||||
</button>`;
|
</button>`;
|
||||||
addStyle(`
|
addStyle(`
|
||||||
#misskey_app,
|
#misskey_app,
|
||||||
|
|
|
@ -151,6 +151,22 @@
|
||||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
||||||
|
|
||||||
|
const messages = Object.assign({
|
||||||
|
title: 'Failed to initialize Misskey',
|
||||||
|
solution: 'The following actions may solve the problem.',
|
||||||
|
solution1: 'Update your os and browser',
|
||||||
|
solution2: 'Disable an adblocker',
|
||||||
|
solution3: 'Clear the browser cache',
|
||||||
|
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
|
||||||
|
otherOption: 'Other options',
|
||||||
|
otherOption1: 'Clear preferences and cache',
|
||||||
|
otherOption2: 'Start the simple client',
|
||||||
|
otherOption3: 'Start the repair tool',
|
||||||
|
}, locale?._bootErrors || {});
|
||||||
|
const reload = locale?.reload || 'Reload';
|
||||||
|
|
||||||
let errorsElement = document.getElementById('errors');
|
let errorsElement = document.getElementById('errors');
|
||||||
|
|
||||||
if (!errorsElement) {
|
if (!errorsElement) {
|
||||||
|
@ -160,32 +176,32 @@
|
||||||
<path d="M12 9v2m0 4v.01"></path>
|
<path d="M12 9v2m0 4v.01"></path>
|
||||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>Failed to load<br>読み込みに失敗しました</h1>
|
<h1>${messages.title}</h1>
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
<button class="button-big" onclick="location.reload(true);">
|
||||||
<span class="button-label-big">Reload / リロード</span>
|
<span class="button-label-big">${reload}</span>
|
||||||
</button>
|
</button>
|
||||||
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
|
<p><b>${messages.solution}</b></p>
|
||||||
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
|
<p>${messages.solution1}</p>
|
||||||
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
<p>${messages.solution2}</p>
|
||||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
<p>${messages.solution3}</p>
|
||||||
<p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
|
<p>${messages.solution4}</p>
|
||||||
<details style="color: #86b300;">
|
<details style="color: #86b300;">
|
||||||
<summary>Other options / その他のオプション</summary>
|
<summary>${messages.otherOption}</summary>
|
||||||
<a href="/flush">
|
<a href="/flush">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Clear preferences and cache</span>
|
<span class="button-label-small">${messages.otherOption1}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="/cli">
|
<a href="/cli">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Start the simple client</span>
|
<span class="button-label-small">${messages.otherOption2}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="/bios">
|
<a href="/bios">
|
||||||
<button class="button-small">
|
<button class="button-small">
|
||||||
<span class="button-label-small">Start the repair tool</span>
|
<span class="button-label-small">${messages.otherOption3}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* exportCompleted - エクスポートが完了
|
* exportCompleted - エクスポートが完了
|
||||||
* login - ログイン
|
* login - ログイン
|
||||||
|
* createToken - トークン作成
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
|
@ -36,6 +37,7 @@ export const notificationTypes = [
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
'exportCompleted',
|
'exportCompleted',
|
||||||
'login',
|
'login',
|
||||||
|
'createToken',
|
||||||
'app',
|
'app',
|
||||||
'test',
|
'test',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/swcrc",
|
"$schema": "https://swc.rs/schema.json",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
"parser": {
|
"parser": {
|
||||||
"syntax": "typescript",
|
"syntax": "typescript",
|
||||||
|
|
|
@ -69,6 +69,7 @@ export const notificationTypes = [
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
'exportCompleted',
|
'exportCompleted',
|
||||||
'login',
|
'login',
|
||||||
|
'createToken',
|
||||||
'test',
|
'test',
|
||||||
'app',
|
'app',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -414,6 +414,7 @@ function toStories(component: string): Promise<string> {
|
||||||
glob('src/components/MkSignupServerRules.vue'),
|
glob('src/components/MkSignupServerRules.vue'),
|
||||||
glob('src/components/MkUserSetupDialog.vue'),
|
glob('src/components/MkUserSetupDialog.vue'),
|
||||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||||
|
glob('src/components/MkImgPreviewDialog.vue'),
|
||||||
glob('src/components/MkInstanceCardMini.vue'),
|
glob('src/components/MkInstanceCardMini.vue'),
|
||||||
glob('src/components/MkInviteCode.vue'),
|
glob('src/components/MkInviteCode.vue'),
|
||||||
glob('src/components/MkTagItem.vue'),
|
glob('src/components/MkTagItem.vue'),
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { file } from '../../.storybook/fakes.js';
|
||||||
|
import MkImgPreviewDialog from './MkImgPreviewDialog.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkImgPreviewDialog,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkImgPreviewDialog v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
file: file(),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
chromatic: {
|
||||||
|
// NOTE: ロードが終わるまで待つ
|
||||||
|
delay: 3000,
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkImgPreviewDialog>;
|
|
@ -0,0 +1,58 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="modal"
|
||||||
|
:width="1800"
|
||||||
|
:height="900"
|
||||||
|
@close="close"
|
||||||
|
@esc="close"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<template #header>{{ file.name }}</template>
|
||||||
|
<div :class="$style.container">
|
||||||
|
<img :src="file.url" :alt="file.comment ?? file.name" :class="$style.img"/>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, ref } from 'vue';
|
||||||
|
import MkModalWindow from './MkModalWindow.vue';
|
||||||
|
import type * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
file: Misskey.entities.DriveFile;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = ref<typeof MkModalWindow | null>(null);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background-color: var(--MI_THEME-bg);
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -91,10 +91,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
|
import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import type { Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import type { Keymap } from '@/scripts/hotkey.js';
|
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
import MkMediaRange from '@/components/MkMediaRange.vue';
|
import MkMediaRange from '@/components/MkMediaRange.vue';
|
||||||
|
@ -216,10 +217,9 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const details: MenuItem[] = [];
|
||||||
if ($i?.id === props.audio.userId) {
|
if ($i?.id === props.audio.userId) {
|
||||||
menu.push({
|
details.push({
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts._fileViewer.title,
|
text: i18n.ts._fileViewer.title,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
|
@ -227,6 +227,29 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iAmModerator) {
|
||||||
|
details.push({
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.moderation,
|
||||||
|
icon: 'ti ti-photo-exclamation',
|
||||||
|
to: `/admin/file/${props.audio.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.length > 0) {
|
||||||
|
menu.push({ type: 'divider' }, ...details);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultStore.state.devMode) {
|
||||||
|
menu.push({ type: 'divider' }, {
|
||||||
|
icon: 'ti ti-id',
|
||||||
|
text: i18n.ts.copyFileId,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(props.audio.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menuShowing.value = true;
|
menuShowing.value = true;
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
|
|
|
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { watch, ref, computed } from 'vue';
|
import { watch, ref, computed } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
|
@ -132,10 +133,9 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const details: MenuItem[] = [];
|
||||||
if ($i?.id === props.image.userId) {
|
if ($i?.id === props.image.userId) {
|
||||||
menuItems.push({
|
details.push({
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts._fileViewer.title,
|
text: i18n.ts._fileViewer.title,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
|
@ -143,6 +143,29 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iAmModerator) {
|
||||||
|
details.push({
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.moderation,
|
||||||
|
icon: 'ti ti-photo-exclamation',
|
||||||
|
to: `/admin/file/${props.image.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.length > 0) {
|
||||||
|
menuItems.push({ type: 'divider' }, ...details);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultStore.state.devMode) {
|
||||||
|
menuItems.push({ type: 'divider' }, {
|
||||||
|
icon: 'ti ti-id',
|
||||||
|
text: i18n.ts.copyFileId,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(props.image.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { Keymap } from '@/scripts/hotkey.js';
|
import type { Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { hms } from '@/filters/hms.js';
|
import { hms } from '@/filters/hms.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
@ -241,10 +242,9 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const details: MenuItem[] = [];
|
||||||
if ($i?.id === props.video.userId) {
|
if ($i?.id === props.video.userId) {
|
||||||
menu.push({
|
details.push({
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts._fileViewer.title,
|
text: i18n.ts._fileViewer.title,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
|
@ -252,6 +252,29 @@ function showMenu(ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iAmModerator) {
|
||||||
|
details.push({
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.moderation,
|
||||||
|
icon: 'ti ti-photo-exclamation',
|
||||||
|
to: `/admin/file/${props.video.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.length > 0) {
|
||||||
|
menu.push({ type: 'divider' }, ...details);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultStore.state.devMode) {
|
||||||
|
menu.push({ type: 'divider' }, {
|
||||||
|
icon: 'ti ti-id',
|
||||||
|
text: i18n.ts.copyFileId,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(props.video.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
menuShowing.value = true;
|
menuShowing.value = true;
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
os.popupMenu(menu, ev.currentTarget ?? ev.target, {
|
||||||
align: 'right',
|
align: 'right',
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||||
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
|
||||||
[$style.t_login]: notification.type === 'login',
|
[$style.t_login]: notification.type === 'login',
|
||||||
|
[$style.t_createToken]: notification.type === 'createToken',
|
||||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
|
@ -41,6 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
|
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
|
||||||
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
|
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
|
||||||
|
<i v-else-if="notification.type === 'createToken'" class="ti ti-key"></i>
|
||||||
<template v-else-if="notification.type === 'roleAssigned'">
|
<template v-else-if="notification.type === 'roleAssigned'">
|
||||||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||||
<i v-else class="ti ti-badges"></i>
|
<i v-else class="ti ti-badges"></i>
|
||||||
|
@ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
|
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
|
||||||
|
<span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
|
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
|
||||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
|
@ -107,6 +110,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
||||||
{{ i18n.ts.showFile }}
|
{{ i18n.ts.showFile }}
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<MkA v-else-if="notification.type === 'createToken'" :class="$style.text" to="/settings/apps">
|
||||||
|
<Mfm :text="i18n.tsx._notification.createTokenDescription({ text: i18n.ts.manageAccessTokens })"/>
|
||||||
|
</MkA>
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -357,6 +363,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_createToken {
|
||||||
|
padding: 3px;
|
||||||
|
background: var(--eventOther);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tail {
|
.tail {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -22,20 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Sortable>
|
</Sortable>
|
||||||
<p :class="[$style.remain, {
|
<p
|
||||||
[$style.exceeded]: props.modelValue.length > 16,
|
:class="[$style.remain, {
|
||||||
}]">{{ 16 - props.modelValue.length }}/16</p>
|
[$style.exceeded]: props.modelValue.length > 16,
|
||||||
|
}]"
|
||||||
|
>
|
||||||
|
{{ 16 - props.modelValue.length }}/16
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, inject } from 'vue';
|
import { defineAsyncComponent, inject } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { MenuItem } from '@/types/menu';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
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 type { MenuItem } from '@/types/menu.js';
|
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -168,6 +174,14 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
||||||
text: i18n.ts.cropImage,
|
text: i18n.ts.cropImage,
|
||||||
icon: 'ti ti-crop',
|
icon: 'ti ti-crop',
|
||||||
action: () : void => { crop(file); },
|
action: () : void => { crop(file); },
|
||||||
|
}, {
|
||||||
|
text: i18n.ts.preview,
|
||||||
|
icon: 'ti ti-photo-search',
|
||||||
|
action: () => {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkImgPreviewDialog.vue')), {
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,6 +198,16 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
||||||
action: () => { detachAndDeleteMedia(file); },
|
action: () => { detachAndDeleteMedia(file); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (defaultStore.state.devMode) {
|
||||||
|
menuItems.push({ type: 'divider' }, {
|
||||||
|
icon: 'ti ti-id',
|
||||||
|
text: i18n.ts.copyFileId,
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(file.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
|
||||||
menuShowing = true;
|
menuShowing = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
|
||||||
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-storage-access-by-user-activation allow-same-origin"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
|
||||||
:class="$style.playerIframe"
|
:class="$style.playerIframe"
|
||||||
|
|
|
@ -109,6 +109,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div>
|
<div>
|
||||||
<a style="display: inline-block;" class="pepabo" title="GMO Pepabo" href="https://pepabo.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/gmo_pepabo.svg" alt="GMO Pepabo"></a>
|
<a style="display: inline-block;" class="pepabo" title="GMO Pepabo" href="https://pepabo.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/gmo_pepabo.svg" alt="GMO Pepabo"></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<a style="display: inline-block;" class="purpledotdigital" title="Purple Dot Digital" href="https://purpledotdigital.com/" target="_blank"><img style="width: 100%;" src="https://assets.misskey-hub.net/sponsors/purple-dot-digital.jpg" alt="Purple Dot Digital"></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
|
|
|
@ -418,7 +418,7 @@ async function deleteAccount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assignRole() {
|
async function assignRole() {
|
||||||
const roles = await misskeyApi('admin/roles/list');
|
const roles = await misskeyApi('admin/roles/list').then(it => it.filter(r => r.target === 'manual'));
|
||||||
|
|
||||||
const { canceled, result: roleId } = await os.select({
|
const { canceled, result: roleId } = await os.select({
|
||||||
title: i18n.ts._role.chooseRoleToAssign,
|
title: i18n.ts._role.chooseRoleToAssign,
|
||||||
|
|
|
@ -81,7 +81,7 @@ const $i = signinRequired();
|
||||||
|
|
||||||
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
|
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
|
||||||
|
|
||||||
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
|
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken'] satisfies (typeof notificationTypes[number])[] as string[];
|
||||||
|
|
||||||
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||||
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
||||||
|
|
|
@ -98,7 +98,7 @@ describe('MkUrlPreview', () => {
|
||||||
assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1');
|
assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
iframe?.sandbox.toString(),
|
iframe?.sandbox.toString(),
|
||||||
'allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin',
|
'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-storage-access-by-user-activation allow-same-origin',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/swcrc",
|
"$schema": "https://swc.rs/schema.json",
|
||||||
"jsc": {
|
"jsc": {
|
||||||
"parser": {
|
"parser": {
|
||||||
"syntax": "typescript",
|
"syntax": "typescript",
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { mkdir, writeFile } from 'fs/promises';
|
import assert from 'assert';
|
||||||
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||||
import { OpenAPIV3_1 } from 'openapi-types';
|
import { OpenAPIV3_1 } from 'openapi-types';
|
||||||
import { toPascal } from 'ts-case-convert';
|
import { toPascal } from 'ts-case-convert';
|
||||||
import OpenAPIParser from '@readme/openapi-parser';
|
import OpenAPIParser from '@readme/openapi-parser';
|
||||||
import openapiTS from 'openapi-typescript';
|
import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
|
||||||
|
|
||||||
async function generateBaseTypes(
|
async function generateBaseTypes(
|
||||||
openApiDocs: OpenAPIV3_1.Document,
|
openApiDocs: OpenAPIV3_1.Document,
|
||||||
|
@ -20,7 +21,29 @@ async function generateBaseTypes(
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
const generatedTypes = await openapiTS(openApiJsonPath, {
|
// NOTE: Align `operationId` of GET and POST to avoid duplication of type definitions
|
||||||
|
const openApi = JSON.parse(await readFile(openApiJsonPath, 'utf8')) as OpenAPI3;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
for (const [key, item] of Object.entries(openApi.paths!)) {
|
||||||
|
assert('post' in item);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
openApi.paths![key] = {
|
||||||
|
...('get' in item ? {
|
||||||
|
get: {
|
||||||
|
...item.get,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
|
post: {
|
||||||
|
...item.post,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
operationId: ((item as PathItemObject).post as OperationObject).operationId!.replaceAll('post___', ''),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedTypes = await openapiTS(openApi, {
|
||||||
exportType: true,
|
exportType: true,
|
||||||
transform(schemaObject) {
|
transform(schemaObject) {
|
||||||
if ('format' in schemaObject && schemaObject.format === 'binary') {
|
if ('format' in schemaObject && schemaObject.format === 'binary') {
|
||||||
|
@ -78,7 +101,7 @@ async function generateEndpoints(
|
||||||
for (const operation of postPathItems) {
|
for (const operation of postPathItems) {
|
||||||
const path = operation._path_;
|
const path = operation._path_;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const operationId = operation.operationId!;
|
const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
|
||||||
const endpoint = new Endpoint(path);
|
const endpoint = new Endpoint(path);
|
||||||
endpoints.push(endpoint);
|
endpoints.push(endpoint);
|
||||||
|
|
||||||
|
@ -195,7 +218,7 @@ async function generateApiClientJSDoc(
|
||||||
|
|
||||||
for (const operation of postPathItems) {
|
for (const operation of postPathItems) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const operationId = operation.operationId!;
|
const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
|
||||||
|
|
||||||
if (operation.description) {
|
if (operation.description) {
|
||||||
endpoints.push({
|
endpoints.push({
|
||||||
|
|
|
@ -4355,6 +4355,13 @@ export type components = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'login';
|
type: 'login';
|
||||||
|
} | {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'createToken';
|
||||||
} | ({
|
} | ({
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -8349,7 +8356,8 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength: boolean;
|
urlPreviewRequireContentLength: boolean;
|
||||||
urlPreviewUserAgent: string | null;
|
urlPreviewUserAgent: string | null;
|
||||||
urlPreviewSummaryProxyUrl: string | null;
|
urlPreviewSummaryProxyUrl: string | null;
|
||||||
federation: string;
|
/** @enum {string} */
|
||||||
|
federation: 'all' | 'specified' | 'none';
|
||||||
federationHosts: string[];
|
federationHosts: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19916,8 +19924,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19984,8 +19992,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue