Merge upstream
This commit is contained in:
commit
4b6cbdab39
@ -72,7 +72,7 @@ dbReplications: false
|
|||||||
#───┘ Redis configuration └─────────────────────────────────────
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: keydb
|
host: dragonfly
|
||||||
port: 6379
|
port: 6379
|
||||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
@ -80,7 +80,7 @@ redis:
|
|||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
#redisForPubsub:
|
#redisForPubsub:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
@ -88,7 +88,7 @@ redis:
|
|||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForJobQueue:
|
#redisForJobQueue:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
@ -96,7 +96,7 @@ redis:
|
|||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForTimelines:
|
#redisForTimelines:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
@ -72,7 +72,7 @@ dbReplications: false
|
|||||||
#───┘ Redis configuration └─────────────────────────────────────
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: keydb
|
host: dragonfly
|
||||||
port: 6379
|
port: 6379
|
||||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
@ -80,7 +80,7 @@ redis:
|
|||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
#redisForPubsub:
|
#redisForPubsub:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
@ -88,7 +88,7 @@ redis:
|
|||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForJobQueue:
|
#redisForJobQueue:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
@ -96,7 +96,7 @@ redis:
|
|||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForTimelines:
|
#redisForTimelines:
|
||||||
# host: keydb
|
# host: dragonfly
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
@ -15,17 +15,25 @@ services:
|
|||||||
- internal_network
|
- internal_network
|
||||||
- external_network
|
- external_network
|
||||||
|
|
||||||
keydb:
|
dragonfly:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
ulimits:
|
||||||
|
memlock: -1
|
||||||
|
environment:
|
||||||
|
DFLY_snapshot_cron: '* * * * *'
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
volumes:
|
volumes:
|
||||||
- keydb-data:/data
|
- dragonfly-data:/data
|
||||||
healthcheck:
|
|
||||||
test: "keydb-cli ping"
|
|
||||||
interval: 5s
|
|
||||||
retries: 20
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@ -45,7 +53,7 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
keydb-data:
|
dragonfly-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal_network:
|
internal_network:
|
||||||
|
@ -11,7 +11,6 @@ docker-compose.yml
|
|||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules
|
packages/*/node_modules
|
||||||
redis/
|
redis/
|
||||||
keydb/
|
|
||||||
files/
|
files/
|
||||||
fluent-emojis/
|
fluent-emojis/
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
28
.github/unused/test-backend.yml
vendored
28
.github/unused/test-backend.yml
vendored
@ -30,8 +30,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
keydb:
|
dragonfly:
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
options: --ulimit "memlock=-1"
|
||||||
|
env:
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
@ -82,8 +92,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
keydb:
|
dragonfly:
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
options: --ulimit "memlock=-1"
|
||||||
|
env:
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,7 +53,6 @@ run.bat
|
|||||||
api-docs.json
|
api-docs.json
|
||||||
*.log
|
*.log
|
||||||
/redis
|
/redis
|
||||||
/keydb
|
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/files
|
/files
|
||||||
|
@ -37,8 +37,25 @@ spec:
|
|||||||
value: "misskey"
|
value: "misskey"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5432
|
- containerPort: 5432
|
||||||
- name: keydb
|
- name: dragonfly
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
env:
|
||||||
|
- name: DFLY_version_check
|
||||||
|
value: false
|
||||||
|
- name: DFLY_tcp_backlog
|
||||||
|
value: 2048
|
||||||
|
- name: DFLY_default_lua_flags
|
||||||
|
value: allow-undeclared-keys
|
||||||
|
- name: DFLY_pipeline_squash
|
||||||
|
value: 0
|
||||||
|
- name: DFLY_multi_exec_squash
|
||||||
|
value: false
|
||||||
|
- name: DFLY_conn_io_threads
|
||||||
|
value: 4
|
||||||
|
- name: DFLY_epoll_file_threads
|
||||||
|
value: 4
|
||||||
|
- name: DFLY_proactor_threads
|
||||||
|
value: 4
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 6379
|
- containerPort: 6379
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -3,17 +3,25 @@ version: "3"
|
|||||||
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
||||||
|
|
||||||
services:
|
services:
|
||||||
keydb:
|
dragonfly:
|
||||||
restart: always
|
restart: always
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
ulimits:
|
||||||
|
memlock: -1
|
||||||
|
environment:
|
||||||
|
DFLY_snapshot_cron: '* * * * *'
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- ./keydb:/data
|
- ./redis:/data
|
||||||
healthcheck:
|
|
||||||
test: "keydb-cli ping"
|
|
||||||
interval: 5s
|
|
||||||
retries: 20
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -6,13 +6,13 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
links:
|
links:
|
||||||
- db
|
- db
|
||||||
- keydb
|
- dragonfly
|
||||||
# - mcaptcha
|
# - mcaptcha
|
||||||
# - meilisearch
|
# - meilisearch
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
keydb:
|
dragonfly:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
@ -23,17 +23,25 @@ services:
|
|||||||
- ./files:/misskey/files
|
- ./files:/misskey/files
|
||||||
- ./.config:/misskey/.config:ro
|
- ./.config:/misskey/.config:ro
|
||||||
|
|
||||||
keydb:
|
dragonfly:
|
||||||
restart: always
|
restart: always
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
ulimits:
|
||||||
|
memlock: -1
|
||||||
|
environment:
|
||||||
|
DFLY_snapshot_cron: '* * * * *'
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
volumes:
|
volumes:
|
||||||
- ./keydb:/data
|
- ./redis:/data
|
||||||
healthcheck:
|
|
||||||
test: "keydb-cli ping"
|
|
||||||
interval: 5s
|
|
||||||
retries: 20
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -442,6 +442,10 @@ moderation: "Moderation"
|
|||||||
moderationNote: "Moderation note"
|
moderationNote: "Moderation note"
|
||||||
addModerationNote: "Add moderation note"
|
addModerationNote: "Add moderation note"
|
||||||
moderationLogs: "Moderation logs"
|
moderationLogs: "Moderation logs"
|
||||||
|
userAccountMoveLogs: "Account migration logs"
|
||||||
|
userAccountMoveLogsTitle: "{from} migrated the account to {to}"
|
||||||
|
movedToId: "ID of the account migrated to"
|
||||||
|
moveFromId: "ID of the account migrated from"
|
||||||
nUsersMentioned: "Mentioned by {n} users"
|
nUsersMentioned: "Mentioned by {n} users"
|
||||||
securityKeyAndPasskey: "Security- and passkeys"
|
securityKeyAndPasskey: "Security- and passkeys"
|
||||||
securityKey: "Security key"
|
securityKey: "Security key"
|
||||||
@ -910,6 +914,7 @@ followingVisibility: "Visibility of follows"
|
|||||||
followersVisibility: "Visibility of followers"
|
followersVisibility: "Visibility of followers"
|
||||||
continueThread: "View thread continuation"
|
continueThread: "View thread continuation"
|
||||||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||||
|
deleteAccountConfirmAndWarn: "This will irreversibly delete your account.\nPlease note that re-logging in after a deletion request will interrupt the deletion of your account.\nProceed?"
|
||||||
incorrectPassword: "Incorrect password."
|
incorrectPassword: "Incorrect password."
|
||||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||||
hide: "Hide"
|
hide: "Hide"
|
||||||
@ -1089,6 +1094,7 @@ audioFiles: "Audio"
|
|||||||
dataSaver: "Data Saver"
|
dataSaver: "Data Saver"
|
||||||
accountMigration: "Account Migration"
|
accountMigration: "Account Migration"
|
||||||
accountMoved: "This user has moved to a new account:"
|
accountMoved: "This user has moved to a new account:"
|
||||||
|
accountMovedFrom: "This user has been migrated from the following account:"
|
||||||
accountMovedShort: "This account has been migrated."
|
accountMovedShort: "This account has been migrated."
|
||||||
operationForbidden: "Operation forbidden"
|
operationForbidden: "Operation forbidden"
|
||||||
forceShowAds: "Always show ads"
|
forceShowAds: "Always show ads"
|
||||||
@ -1837,6 +1843,7 @@ _accountDelete:
|
|||||||
sendEmail: "Once account deletion has been completed, an email will be sent to the email address registered to this account."
|
sendEmail: "Once account deletion has been completed, an email will be sent to the email address registered to this account."
|
||||||
requestAccountDelete: "Request to delete my account"
|
requestAccountDelete: "Request to delete my account"
|
||||||
started: "Deletion has been started."
|
started: "Deletion has been started."
|
||||||
|
dontLogin: "We recommend that you do not log in to your account, as this will interrupt the deletion process."
|
||||||
inProgress: "Your account is currently being deleted"
|
inProgress: "Your account is currently being deleted"
|
||||||
youCantUseThisTime: "You can't request account deletion for now."
|
youCantUseThisTime: "You can't request account deletion for now."
|
||||||
youAreRootAndCantUseThisTime: "You can't request account deletion 'cause you are root."
|
youAreRootAndCantUseThisTime: "You can't request account deletion 'cause you are root."
|
||||||
|
31
locales/index.d.ts
vendored
31
locales/index.d.ts
vendored
@ -1796,6 +1796,22 @@ export interface Locale extends ILocale {
|
|||||||
* モデログ
|
* モデログ
|
||||||
*/
|
*/
|
||||||
"moderationLogs": string;
|
"moderationLogs": string;
|
||||||
|
/**
|
||||||
|
* アカウント移行使用ログ
|
||||||
|
*/
|
||||||
|
"userAccountMoveLogs": string;
|
||||||
|
/**
|
||||||
|
* {from} が {to} にアカウントを移行しました
|
||||||
|
*/
|
||||||
|
"userAccountMoveLogsTitle": ParameterizedString<"from" | "to">;
|
||||||
|
/**
|
||||||
|
* 移行先のアカウントのID
|
||||||
|
*/
|
||||||
|
"movedToId": string;
|
||||||
|
/**
|
||||||
|
* 移行元のアカウントのID
|
||||||
|
*/
|
||||||
|
"moveFromId": string;
|
||||||
/**
|
/**
|
||||||
* {n}人が投稿
|
* {n}人が投稿
|
||||||
*/
|
*/
|
||||||
@ -3668,6 +3684,13 @@ export interface Locale extends ILocale {
|
|||||||
* ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?
|
* ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?
|
||||||
*/
|
*/
|
||||||
"truncateAccountConfirm": string;
|
"truncateAccountConfirm": string;
|
||||||
|
/**
|
||||||
|
* アカウントが削除されます。
|
||||||
|
* 削除リクエスト後に再ログインすると
|
||||||
|
* アカウントの削除が中断されてしまいますのでご注意ください。
|
||||||
|
* よろしいですか?
|
||||||
|
*/
|
||||||
|
"deleteAccountConfirmAndWarn": string;
|
||||||
/**
|
/**
|
||||||
* パスワードが間違っています。
|
* パスワードが間違っています。
|
||||||
*/
|
*/
|
||||||
@ -4396,6 +4419,10 @@ export interface Locale extends ILocale {
|
|||||||
* このユーザーは新しいアカウントに移行しました:
|
* このユーザーは新しいアカウントに移行しました:
|
||||||
*/
|
*/
|
||||||
"accountMoved": string;
|
"accountMoved": string;
|
||||||
|
/**
|
||||||
|
* このユーザーは次のアカウントから移行されました:
|
||||||
|
*/
|
||||||
|
"accountMovedFrom": string;
|
||||||
/**
|
/**
|
||||||
* このアカウントは移行されています
|
* このアカウントは移行されています
|
||||||
*/
|
*/
|
||||||
@ -7331,6 +7358,10 @@ export interface Locale extends ILocale {
|
|||||||
* あなたは最高管理者であるため、アカウントを削除することはできません。
|
* あなたは最高管理者であるため、アカウントを削除することはできません。
|
||||||
*/
|
*/
|
||||||
"youAreRootAndCantUseThisTime": string;
|
"youAreRootAndCantUseThisTime": string;
|
||||||
|
/**
|
||||||
|
* 削除が中断されてしまいますので、アカウントにログインしないことをおすすめします。
|
||||||
|
*/
|
||||||
|
"dontLogin": string;
|
||||||
};
|
};
|
||||||
"_accountTruncate": {
|
"_accountTruncate": {
|
||||||
/**
|
/**
|
||||||
|
@ -445,6 +445,10 @@ moderation: "モデレーション"
|
|||||||
moderationNote: "モデレーションノート"
|
moderationNote: "モデレーションノート"
|
||||||
addModerationNote: "モデレーションノートを追加する"
|
addModerationNote: "モデレーションノートを追加する"
|
||||||
moderationLogs: "モデログ"
|
moderationLogs: "モデログ"
|
||||||
|
userAccountMoveLogs: "アカウント移行使用ログ"
|
||||||
|
userAccountMoveLogsTitle: "{from} が {to} にアカウントを移行しました"
|
||||||
|
movedToId: "移行先のアカウントのID"
|
||||||
|
moveFromId: "移行元のアカウントのID"
|
||||||
nUsersMentioned: "{n}人が投稿"
|
nUsersMentioned: "{n}人が投稿"
|
||||||
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
||||||
securityKey: "セキュリティキー"
|
securityKey: "セキュリティキー"
|
||||||
@ -913,6 +917,7 @@ followersVisibility: "フォロワーの公開範囲"
|
|||||||
continueThread: "さらにスレッドを見る"
|
continueThread: "さらにスレッドを見る"
|
||||||
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
|
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
|
||||||
truncateAccountConfirm: "ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?"
|
truncateAccountConfirm: "ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?"
|
||||||
|
deleteAccountConfirmAndWarn: "アカウントが削除されます。\n削除リクエスト後に再ログインすると\nアカウントの削除が中断されてしまいますのでご注意ください。\nよろしいですか?"
|
||||||
incorrectPassword: "パスワードが間違っています。"
|
incorrectPassword: "パスワードが間違っています。"
|
||||||
voteConfirm: "「{choice}」に投票しますか?"
|
voteConfirm: "「{choice}」に投票しますか?"
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
@ -1095,6 +1100,7 @@ audioFiles: "音声"
|
|||||||
dataSaver: "データセーバー"
|
dataSaver: "データセーバー"
|
||||||
accountMigration: "アカウントの移行"
|
accountMigration: "アカウントの移行"
|
||||||
accountMoved: "このユーザーは新しいアカウントに移行しました:"
|
accountMoved: "このユーザーは新しいアカウントに移行しました:"
|
||||||
|
accountMovedFrom: "このユーザーは次のアカウントから移行されました:"
|
||||||
accountMovedShort: "このアカウントは移行されています"
|
accountMovedShort: "このアカウントは移行されています"
|
||||||
operationForbidden: "この操作はできません"
|
operationForbidden: "この操作はできません"
|
||||||
forceShowAds: "常に広告を表示する"
|
forceShowAds: "常に広告を表示する"
|
||||||
@ -1898,6 +1904,7 @@ _accountDelete:
|
|||||||
inProgress: "削除が進行中"
|
inProgress: "削除が進行中"
|
||||||
youCantUseThisTime: "現在、アカウントの削除はできません。"
|
youCantUseThisTime: "現在、アカウントの削除はできません。"
|
||||||
youAreRootAndCantUseThisTime: "あなたは最高管理者であるため、アカウントを削除することはできません。"
|
youAreRootAndCantUseThisTime: "あなたは最高管理者であるため、アカウントを削除することはできません。"
|
||||||
|
dontLogin: "削除が中断されてしまいますので、アカウントにログインしないことをおすすめします。"
|
||||||
|
|
||||||
_accountTruncate:
|
_accountTruncate:
|
||||||
accountDelete: "アカウントの整理"
|
accountDelete: "アカウントの整理"
|
||||||
|
@ -441,6 +441,10 @@ moderation: "중재"
|
|||||||
moderationNote: "중재 기록"
|
moderationNote: "중재 기록"
|
||||||
addModerationNote: "중재 기록 추가하기"
|
addModerationNote: "중재 기록 추가하기"
|
||||||
moderationLogs: "모더레이션 로그"
|
moderationLogs: "모더레이션 로그"
|
||||||
|
userAccountMoveLogs: "계정 이사 사용 로그"
|
||||||
|
userAccountMoveLogsTitle: "{from} 가 {to} 로 계정을 이사했습니다"
|
||||||
|
movedToId: "이사 후 계정의 ID"
|
||||||
|
moveFromId: "이사 전 계정의 ID"
|
||||||
nUsersMentioned: "{n}명이 언급함"
|
nUsersMentioned: "{n}명이 언급함"
|
||||||
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
||||||
securityKey: "보안 키"
|
securityKey: "보안 키"
|
||||||
@ -910,6 +914,7 @@ followersVisibility: "내 팔로워를 볼 수 있는 사람"
|
|||||||
continueThread: "글타래 더 보기"
|
continueThread: "글타래 더 보기"
|
||||||
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
|
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
|
||||||
truncateAccountConfirm: "다이렉트 및 프로필 상단에 고정된 노트를 제외한 모든 노트가 (드라이브 정리 옵션을 켠 경우 모든 파일도) 삭제되고 이는 복구할 수 없습니다. 그래도 계속하시겠습니까?"
|
truncateAccountConfirm: "다이렉트 및 프로필 상단에 고정된 노트를 제외한 모든 노트가 (드라이브 정리 옵션을 켠 경우 모든 파일도) 삭제되고 이는 복구할 수 없습니다. 그래도 계속하시겠습니까?"
|
||||||
|
deleteAccountConfirmAndWarn: "계정이 삭제됩니다.\n삭제 요청 후 다시 로그인하면 계정 삭제가 중단되어 버립니다.\n계속하시겠습니까?"
|
||||||
incorrectPassword: "비밀번호가 올바르지 않습니다."
|
incorrectPassword: "비밀번호가 올바르지 않습니다."
|
||||||
voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
|
voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
|
||||||
hide: "숨기기"
|
hide: "숨기기"
|
||||||
@ -1092,6 +1097,7 @@ audioFiles: "소리"
|
|||||||
dataSaver: "데이터 절약 모드"
|
dataSaver: "데이터 절약 모드"
|
||||||
accountMigration: "계정 이동"
|
accountMigration: "계정 이동"
|
||||||
accountMoved: "이 유저는 다음 계정으로 이사했습니다:"
|
accountMoved: "이 유저는 다음 계정으로 이사했습니다:"
|
||||||
|
accountMovedFrom: "이 유저는 다음 계정에서 이사했습니다:"
|
||||||
accountMovedShort: "이사한 계정입니다"
|
accountMovedShort: "이사한 계정입니다"
|
||||||
operationForbidden: "사용할 수 없습니다"
|
operationForbidden: "사용할 수 없습니다"
|
||||||
forceShowAds: "광고를 항상 표시"
|
forceShowAds: "광고를 항상 표시"
|
||||||
@ -1270,7 +1276,7 @@ youAreHidingSensitiveInformation: "'프라이빗 모드'에 의해 숨겨졌습
|
|||||||
temporarilySeeThis: "무시하고 표시하기"
|
temporarilySeeThis: "무시하고 표시하기"
|
||||||
sensitiveDoubleClickRequired: "민감한 내용의 미디어를 더블 클릭해서 표시"
|
sensitiveDoubleClickRequired: "민감한 내용의 미디어를 더블 클릭해서 표시"
|
||||||
prohibitSkippingInitialTutorial: "튜토리얼을 건너뛸 수 없도록 하기"
|
prohibitSkippingInitialTutorial: "튜토리얼을 건너뛸 수 없도록 하기"
|
||||||
prohibitSkippingInitialTutorialDescription: "신규 가입한 사용자에게 표시되는 튜토리얼을 건너뛸 수 없도록 합니다. 튜토리얼을 완료하지 않거나 튜토리얼 페이지를 우회하는 경우에도 강제로 리디렉션됩니다."
|
prohibitSkippingInitialTutorialDescription: "신규 가입한 유저에게 표시되는 튜토리얼을 건너뛸 수 없도록 합니다. 튜토리얼을 완료하지 않거나 튜토리얼 페이지를 우회하는 경우에도 강제로 리디렉션됩니다."
|
||||||
onboarding: "온보딩"
|
onboarding: "온보딩"
|
||||||
refreshMetadata: "서버 정보를 갱신하기"
|
refreshMetadata: "서버 정보를 갱신하기"
|
||||||
removeAllFollowings: "모든 팔로우 관계를 제거하기"
|
removeAllFollowings: "모든 팔로우 관계를 제거하기"
|
||||||
@ -1424,7 +1430,7 @@ _initialTutorial:
|
|||||||
description: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
|
description: "푸시 알림을 활성화하면 {name}의 알림을 나의 기기에서 받아볼 수 있게 됩니다."
|
||||||
_privacySettings:
|
_privacySettings:
|
||||||
title: "프라이버시 설정"
|
title: "프라이버시 설정"
|
||||||
description: "대부분의 초기 사용자가 설정하는 것을 권장하는 옵션들입니다. 입맛에 맞게 변경해보세요."
|
description: "대부분의 초기 유저가 설정하는 것을 권장하는 옵션들입니다. 입맛에 맞게 변경해보세요."
|
||||||
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
|
theseSettingsCanEditLater: "이 설정들은 나중에도 변경할 수 있습니다."
|
||||||
youCanEditMoreSettingsInSettingsPageLater: "이외의 옵션은 '설정' 페이지에 있으니, 나중에 꼭 확인해보시기 바랍니다."
|
youCanEditMoreSettingsInSettingsPageLater: "이외의 옵션은 '설정' 페이지에 있으니, 나중에 꼭 확인해보시기 바랍니다."
|
||||||
_profileSettings:
|
_profileSettings:
|
||||||
@ -1434,7 +1440,7 @@ _initialTutorial:
|
|||||||
youCanEditMoreSettingsInSettingsPageLater: "이외의 옵션은 '설정' 페이지에 있으니, 나중에 꼭 확인해보시기 바랍니다."
|
youCanEditMoreSettingsInSettingsPageLater: "이외의 옵션은 '설정' 페이지에 있으니, 나중에 꼭 확인해보시기 바랍니다."
|
||||||
mustBeSetBotOwner: "Bot 계정은 반드시 관리자를 기재해야 합니다. 아래에서 관리자 계정을 선택하세요."
|
mustBeSetBotOwner: "Bot 계정은 반드시 관리자를 기재해야 합니다. 아래에서 관리자 계정을 선택하세요."
|
||||||
_done:
|
_done:
|
||||||
title: "튜토리얼이 완료! 🎉"
|
title: "튜토리얼이 끝났습니다! 🎉"
|
||||||
description: "여기에서 소개한 기능은 극히 일부에 지나지 않습니다. Misskey의 사용 방법을 더 자세히 알아보려면 {link}를 확인해 주세요!"
|
description: "여기에서 소개한 기능은 극히 일부에 지나지 않습니다. Misskey의 사용 방법을 더 자세히 알아보려면 {link}를 확인해 주세요!"
|
||||||
haveFun: "{name}에서 즐거운 시간 보내시기 바랍니다!"
|
haveFun: "{name}에서 즐거운 시간 보내시기 바랍니다!"
|
||||||
youCanReferTutorialBy: "이 튜토리얼은 [더 보기! -> 정보 -> 튜토리얼 보기]에서 다시 볼 수 있습니다."
|
youCanReferTutorialBy: "이 튜토리얼은 [더 보기! -> 정보 -> 튜토리얼 보기]에서 다시 볼 수 있습니다."
|
||||||
@ -1450,7 +1456,7 @@ _initialTutorial:
|
|||||||
backToOriginalPathDescription: "튜토리얼에 진입하기 전에 보고 있던 페이지로 이동합니다."
|
backToOriginalPathDescription: "튜토리얼에 진입하기 전에 보고 있던 페이지로 이동합니다."
|
||||||
profile: "프로필 설정"
|
profile: "프로필 설정"
|
||||||
profileDescription: "더 많은 프로필 관련 설정 (배너, 추가 정보, 고양이...) 을 진행합니다."
|
profileDescription: "더 많은 프로필 관련 설정 (배너, 추가 정보, 고양이...) 을 진행합니다."
|
||||||
exploreDescription: "지금 여기에서 인기 있는 노트 또는 사용자를 더 탐색해봅니다."
|
exploreDescription: "지금 여기에서 인기 있는 노트 또는 유저를 더 탐색해봅니다."
|
||||||
goToTimeline: "타임라인으로"
|
goToTimeline: "타임라인으로"
|
||||||
goToTimelineDescription: "설정을 더 진행하지 않고, 홈 타임라인으로 돌아갑니다."
|
goToTimelineDescription: "설정을 더 진행하지 않고, 홈 타임라인으로 돌아갑니다."
|
||||||
_timelineDescription:
|
_timelineDescription:
|
||||||
@ -1867,6 +1873,7 @@ _accountDelete:
|
|||||||
inProgress: "삭제 진행 중"
|
inProgress: "삭제 진행 중"
|
||||||
youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다."
|
youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다."
|
||||||
youAreRootAndCantUseThisTime: "당신은 최고 관리자이므로, 지금은 계정 삭제를 진행할 수 없습니다."
|
youAreRootAndCantUseThisTime: "당신은 최고 관리자이므로, 지금은 계정 삭제를 진행할 수 없습니다."
|
||||||
|
dontLogin: "삭제가 중단되어 버릴 수 있으므로, 계정에 로그인하지 않는 것을 권장합니다."
|
||||||
_accountTruncate:
|
_accountTruncate:
|
||||||
accountTruncate: "계정 청소"
|
accountTruncate: "계정 청소"
|
||||||
purgeDriveFiles: "드라이브의 파일도 정리하기"
|
purgeDriveFiles: "드라이브의 파일도 정리하기"
|
||||||
@ -2175,7 +2182,7 @@ _permissions:
|
|||||||
"write:admin:suspend-user": "유저 정지하기"
|
"write:admin:suspend-user": "유저 정지하기"
|
||||||
"write:admin:unset-user-avatar": "유저 아바타 삭제하기"
|
"write:admin:unset-user-avatar": "유저 아바타 삭제하기"
|
||||||
"write:admin:unset-user-banner": "유저 배너 삭제하기"
|
"write:admin:unset-user-banner": "유저 배너 삭제하기"
|
||||||
"write:admin:unset-user-mutual-link": "사용자의 서로링크 삭제하기"
|
"write:admin:unset-user-mutual-link": "유저의 서로링크 삭제하기"
|
||||||
"write:admin:unsuspend-user": "유저 정지 해제하기"
|
"write:admin:unsuspend-user": "유저 정지 해제하기"
|
||||||
"write:admin:meta": "인스턴스 메타데이터 수정하기"
|
"write:admin:meta": "인스턴스 메타데이터 수정하기"
|
||||||
"write:admin:user-note": "중재 기록 수정하기"
|
"write:admin:user-note": "중재 기록 수정하기"
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
export class Useraccountmovelogs1724749627479 {
|
||||||
|
name = 'Useraccountmovelogs1724749627479'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_account_move_log" ("id" character varying(32) NOT NULL, "movedToId" character varying(32) NOT NULL, "movedFromId" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8ffd4ae965a5e3a0fbf4b084212" PRIMARY KEY ("id")); COMMENT ON COLUMN "user_account_move_log"."createdAt" IS 'The created date of the UserIp.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d5ee7d4d1b5e7a69d8855ab069" ON "user_account_move_log" ("movedToId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_82930731d6390e7bb429a1938f" ON "user_account_move_log" ("movedFromId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_account_move_log" ADD CONSTRAINT "FK_d5ee7d4d1b5e7a69d8855ab0696" FOREIGN KEY ("movedToId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_account_move_log" ADD CONSTRAINT "FK_82930731d6390e7bb429a1938f8" FOREIGN KEY ("movedFromId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_account_move_log" DROP CONSTRAINT "FK_82930731d6390e7bb429a1938f8"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_account_move_log" DROP CONSTRAINT "FK_d5ee7d4d1b5e7a69d8855ab0696"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_82930731d6390e7bb429a1938f"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d5ee7d4d1b5e7a69d8855ab069"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_account_move_log"`);
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UserAccountMoveLogRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
@ -48,6 +48,15 @@ export class AccountMoveService {
|
|||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userAccountMoveLogRepository)
|
||||||
|
private userAccountMoveLogRepository: UserAccountMoveLogRepository,
|
||||||
|
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
@ -119,6 +128,8 @@ export class AccountMoveService {
|
|||||||
this.copyBlocking(src, dst),
|
this.copyBlocking(src, dst),
|
||||||
this.copyMutings(src, dst),
|
this.copyMutings(src, dst),
|
||||||
this.updateLists(src, dst),
|
this.updateLists(src, dst),
|
||||||
|
this.mergeModerationNote(src, dst),
|
||||||
|
this.insertAccountMoveLog(src, dst),
|
||||||
]);
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
/* skip if any error happens */
|
/* skip if any error happens */
|
||||||
@ -256,6 +267,32 @@ export class AccountMoveService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async mergeModerationNote(src: ThinUser, dst: MiUser): Promise<void> {
|
||||||
|
const srcprofile = await this.userProfilesRepository.findOneBy({ userId: src.id });
|
||||||
|
const dstprofile = await this.userProfilesRepository.findOneBy({ userId: dst.id });
|
||||||
|
|
||||||
|
if (!srcprofile || !dstprofile) return;
|
||||||
|
|
||||||
|
await this.userProfilesRepository.update({ userId: dst.id }, {
|
||||||
|
moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.userProfilesRepository.update({ userId: src.id }, {
|
||||||
|
moderationNote: srcprofile.moderationNote + '\n' + dstprofile.moderationNote,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async insertAccountMoveLog(src: ThinUser, dst: MiUser): Promise<void> {
|
||||||
|
await this.userAccountMoveLogRepository.insert({
|
||||||
|
id: this.idService.gen(),
|
||||||
|
movedToId: dst.id,
|
||||||
|
movedFromId: src.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: MiUser): Promise<void> {
|
private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: MiUser): Promise<void> {
|
||||||
if (localFollowerIds.length === 0) return;
|
if (localFollowerIds.length === 0) return;
|
||||||
|
@ -138,7 +138,7 @@ export class AnnouncementService {
|
|||||||
limit: number,
|
limit: number,
|
||||||
offset: number,
|
offset: number,
|
||||||
moderator: MiUser,
|
moderator: MiUser,
|
||||||
): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number })[]> {
|
): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number, lastReadAt: Date | null })[]> {
|
||||||
const query = this.announcementsRepository.createQueryBuilder('announcement');
|
const query = this.announcementsRepository.createQueryBuilder('announcement');
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@ -157,13 +157,14 @@ export class AnnouncementService {
|
|||||||
.offset(offset)
|
.offset(offset)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
const reads = new Map<MiAnnouncement, number>();
|
const reads = announcements.length > 0
|
||||||
|
? await this.announcementReadsRepository.createQueryBuilder()
|
||||||
for (const announcement of announcements) {
|
.select('"announcementId", count(*) as "reads", max("id") as "lastReadId"')
|
||||||
reads.set(announcement, await this.announcementReadsRepository.countBy({
|
.where('"announcementId" IN (:...announcementIds)', { announcementIds: announcements.map(a => a.id) })
|
||||||
announcementId: announcement.id,
|
.groupBy('"announcementId"')
|
||||||
}));
|
.getRawMany<{ announcementId: string, reads: number, lastReadId: string | null }>()
|
||||||
}
|
.then(rs => new Map(rs.map(r => [r.announcementId, { reads: r.reads, lastReadAt: r.lastReadId ? this.idService.parse(r.lastReadId).date : null }])))
|
||||||
|
: new Map();
|
||||||
|
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(announcements.map(a => a.userId).filter(id => id != null)),
|
id: In(announcements.map(a => a.userId).filter(id => id != null)),
|
||||||
@ -174,8 +175,8 @@ export class AnnouncementService {
|
|||||||
|
|
||||||
return announcements.map(announcement => ({
|
return announcements.map(announcement => ({
|
||||||
...announcement,
|
...announcement,
|
||||||
|
...reads.get(announcement.id) ?? { reads: 0, lastReadAt: null },
|
||||||
userInfo: packedUsers.find(u => u.id === announcement.userId) ?? null,
|
userInfo: packedUsers.find(u => u.id === announcement.userId) ?? null,
|
||||||
reads: reads.get(announcement) ?? 0,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,18 +294,20 @@ export class AnnouncementService {
|
|||||||
'read.id IS NOT NULL as "isRead"',
|
'read.id IS NOT NULL as "isRead"',
|
||||||
]);
|
]);
|
||||||
query
|
query
|
||||||
.andWhere(
|
.andWhere(new Brackets((qb) => {
|
||||||
new Brackets((qb) => {
|
qb.orWhere(new Brackets((nqb) => {
|
||||||
qb.orWhere('announcement."userId" = :userId', { userId: me.id });
|
nqb.andWhere('announcement."userId" = :userId', { userId: me.id });
|
||||||
qb.orWhere('announcement."userId" IS NULL');
|
nqb.andWhere(isActive ? 'read.id IS NULL' : 'read.id IS NOT NULL');
|
||||||
}),
|
}));
|
||||||
)
|
qb.orWhere(new Brackets((nqb) => {
|
||||||
.andWhere(
|
nqb.andWhere('announcement."userId" IS NULL');
|
||||||
new Brackets((qb) => {
|
nqb.andWhere('announcement."isActive" = :isActive', { isActive });
|
||||||
|
}));
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets((qb) => {
|
||||||
qb.orWhere('announcement."forExistingUsers" = false');
|
qb.orWhere('announcement."forExistingUsers" = false');
|
||||||
qb.orWhere('announcement.id > :userId', { userId: me.id });
|
qb.orWhere('announcement.id > :userId', { userId: me.id });
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
query.select([
|
query.select([
|
||||||
'announcement.*',
|
'announcement.*',
|
||||||
@ -312,12 +315,9 @@ export class AnnouncementService {
|
|||||||
]);
|
]);
|
||||||
query.andWhere('announcement."userId" IS NULL');
|
query.andWhere('announcement."userId" IS NULL');
|
||||||
query.andWhere('announcement."forExistingUsers" = false');
|
query.andWhere('announcement."forExistingUsers" = false');
|
||||||
|
query.andWhere('announcement."isActive" = :isActive', { isActive });
|
||||||
}
|
}
|
||||||
|
|
||||||
query.andWhere('announcement."isActive" = :isActive', {
|
|
||||||
isActive: isActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
query.orderBy({
|
query.orderBy({
|
||||||
'"isRead"': 'ASC',
|
'"isRead"': 'ASC',
|
||||||
|
@ -168,11 +168,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);
|
||||||
|
@ -102,6 +102,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
|||||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
||||||
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
|
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
|
||||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
||||||
|
import { UserAccountMoveLogEntityService } from './entities/UserAccountMoveLogEntityService.js';
|
||||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||||
import { NoteEntityService } from './entities/NoteEntityService.js';
|
import { NoteEntityService } from './entities/NoteEntityService.js';
|
||||||
@ -244,6 +245,7 @@ const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useEx
|
|||||||
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
||||||
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
|
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
|
||||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
||||||
|
const $UserAccountMoveLogEntityService: Provider = { provide: 'UserAccountMoveLogEntityService', useExisting: UserAccountMoveLogEntityService };
|
||||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||||
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
||||||
@ -385,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
InviteCodeEntityService,
|
InviteCodeEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
|
UserAccountMoveLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
@ -522,6 +525,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
$InviteCodeEntityService,
|
$InviteCodeEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
|
$UserAccountMoveLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
@ -659,6 +663,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
InviteCodeEntityService,
|
InviteCodeEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
|
UserAccountMoveLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
@ -795,6 +800,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
$InviteCodeEntityService,
|
$InviteCodeEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
|
$UserAccountMoveLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
|
@ -59,6 +59,7 @@ import { isReply } from '@/misc/is-reply.js';
|
|||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@ -150,6 +151,7 @@ type Option = {
|
|||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@ -217,6 +219,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('note:create');
|
this.logger = this.loggerService.getLogger('note:create');
|
||||||
|
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -548,7 +551,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
// Register host
|
// Register host
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateNote(i.host, note, true);
|
this.instanceChart.updateNote(i.host, note, true);
|
||||||
}
|
}
|
||||||
@ -1093,12 +1096,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
private collapseNotesCount(oldValue: number, newValue: number) {
|
||||||
this.#shutdownController.abort();
|
return oldValue + newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined): void {
|
private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) {
|
||||||
this.dispose();
|
await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
|
this.#shutdownController.abort();
|
||||||
|
await this.updateNotesCountQueue.performAllNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||||
|
await this.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,6 +378,16 @@ export class QueueService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createUserSuspendJob(user: ThinUser) {
|
||||||
|
return this.dbQueue.add('userSuspend', {
|
||||||
|
user: { id: user.id },
|
||||||
|
}, {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createReportAbuseJob(report: MiAbuseUserReport) {
|
public createReportAbuseJob(report: MiAbuseUserReport) {
|
||||||
return this.dbQueue.add('reportAbuse',
|
return this.dbQueue.add('reportAbuse',
|
||||||
|
@ -9,16 +9,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type {
|
import type { FollowingsRepository } from '@/models/_.js';
|
||||||
AntennasRepository,
|
|
||||||
ClipNotesRepository,
|
|
||||||
ClipsRepository,
|
|
||||||
FollowingsRepository,
|
|
||||||
FollowRequestsRepository,
|
|
||||||
UserListMembershipsRepository,
|
|
||||||
UserListsRepository,
|
|
||||||
WebhooksRepository,
|
|
||||||
} from '@/models/_.js';
|
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
@ -36,27 +27,6 @@ export class UserSuspendService {
|
|||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
|
||||||
private antennasRepository: AntennasRepository,
|
|
||||||
|
|
||||||
@Inject(DI.webhooksRepository)
|
|
||||||
private webhooksRepository: WebhooksRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userListsRepository)
|
|
||||||
private userListsRepository: UserListsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.clipsRepository)
|
|
||||||
private clipsRepository: ClipsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.clipNotesRepository)
|
|
||||||
private clipNotesRepository: ClipNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userListMembershipsRepository)
|
|
||||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
@ -72,41 +42,7 @@ export class UserSuspendService {
|
|||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||||
|
|
||||||
const promises: Promise<unknown>[] = [];
|
await this.queueService.createUserSuspendJob(user);
|
||||||
|
|
||||||
let cursor = '';
|
|
||||||
while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
|
|
||||||
const clipNotes = await this.clipNotesRepository.createQueryBuilder('c')
|
|
||||||
.select('c.id')
|
|
||||||
.innerJoin('c.note', 'n')
|
|
||||||
.where('n.userId = :userId', { userId: user.id })
|
|
||||||
.andWhere('c.id > :cursor', { cursor })
|
|
||||||
.orderBy('c.id', 'ASC')
|
|
||||||
.limit(500)
|
|
||||||
.getRawMany<{ id: string }>();
|
|
||||||
|
|
||||||
if (clipNotes.length === 0) break;
|
|
||||||
|
|
||||||
cursor = clipNotes.at(-1)?.id ?? '';
|
|
||||||
|
|
||||||
promises.push(this.clipNotesRepository.createQueryBuilder()
|
|
||||||
.delete()
|
|
||||||
.where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) })
|
|
||||||
.execute());
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled([
|
|
||||||
this.followRequestsRepository.delete({ followeeId: user.id }),
|
|
||||||
this.followRequestsRepository.delete({ followerId: user.id }),
|
|
||||||
|
|
||||||
this.antennasRepository.delete({ userId: user.id }),
|
|
||||||
this.webhooksRepository.delete({ userId: user.id }),
|
|
||||||
this.userListsRepository.delete({ userId: user.id }),
|
|
||||||
this.clipsRepository.delete({ userId: user.id }),
|
|
||||||
|
|
||||||
...promises,
|
|
||||||
this.userListMembershipsRepository.delete({ userId: user.id }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにDelete配信
|
// 知り得る全SharedInboxにDelete配信
|
||||||
|
@ -682,10 +682,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
// まずサーバー内で検索して様子見
|
// まずサーバー内で検索して様子見
|
||||||
let dst = await this.fetchPerson(src.movedToUri);
|
let dst = await this.fetchPerson(src.movedToUri);
|
||||||
|
|
||||||
if (dst && this.userEntityService.isLocalUser(dst)) {
|
if (dst) {
|
||||||
// targetがローカルユーザーだった場合データベースから引っ張ってくる
|
|
||||||
dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser;
|
|
||||||
} else if (dst) {
|
|
||||||
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
||||||
|
|
||||||
// targetを見つけたことがあるならtargetをupdatePersonする
|
// targetを見つけたことがあるならtargetをupdatePersonする
|
||||||
@ -702,13 +699,15 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
dst = await this.resolvePerson(src.movedToUri);
|
dst = await this.resolvePerson(src.movedToUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
const dstUri = this.userEntityService.getUserUri(dst);
|
||||||
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
const srcUri = this.userEntityService.getUserUri(src);
|
||||||
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
if (dst.movedToUri === dstUri) return 'skip: movedTo itself (dst)'; // ???
|
||||||
|
if (src.movedToUri !== dstUri) return 'skip: missmatch uri'; // ???
|
||||||
|
if (dst.movedToUri === srcUri) return 'skip: dst.movedToUri === src.uri';
|
||||||
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
||||||
return 'skip: dst.alsoKnownAs is empty';
|
return 'skip: dst.alsoKnownAs is empty';
|
||||||
}
|
}
|
||||||
if (!dst.alsoKnownAs.includes(src.uri)) {
|
if (!dst.alsoKnownAs.includes(srcUri)) {
|
||||||
return 'skip: alsoKnownAs does not include from.uri';
|
return 'skip: alsoKnownAs does not include from.uri';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
|
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
@ -54,10 +55,30 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getChartUsers(span: 'hour' | 'day', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{
|
public async getUsersRanking(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ userId: string; count: number; }[]> {
|
||||||
userId: string;
|
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
|
||||||
count: number;
|
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
|
||||||
}[]> {
|
|
||||||
return await this.getChartPv(span, amount, cursor, limit, offset);
|
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
|
||||||
|
|
||||||
|
const gt =
|
||||||
|
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
|
||||||
|
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
|
||||||
|
new Error('not happen') as never;
|
||||||
|
|
||||||
|
const repository =
|
||||||
|
span === 'hour' ? this.repositoryForHour :
|
||||||
|
span === 'day' ? this.repositoryForDay :
|
||||||
|
new Error('not happen') as never;
|
||||||
|
|
||||||
|
// ログ取得
|
||||||
|
return await repository.createQueryBuilder()
|
||||||
|
.select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"')
|
||||||
|
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
|
||||||
|
.groupBy('"userId"')
|
||||||
|
.orderBy('"count"', order)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.getRawMany<{ userId: string, count: number }>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,10 +147,8 @@ export default abstract class Chart<T extends Schema> {
|
|||||||
// ↓にしたいけどfindOneとかで型エラーになる
|
// ↓にしたいけどfindOneとかで型エラーになる
|
||||||
//private repositoryForHour: Repository<RawRecord<T>>;
|
//private repositoryForHour: Repository<RawRecord<T>>;
|
||||||
//private repositoryForDay: Repository<RawRecord<T>>;
|
//private repositoryForDay: Repository<RawRecord<T>>;
|
||||||
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number;}>;
|
protected repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
|
||||||
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number;}>;
|
protected repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
|
||||||
private repositoryUserPvForHour: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
|
|
||||||
private repositoryUserPvForDay: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
|
|
||||||
/**
|
/**
|
||||||
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
||||||
*/
|
*/
|
||||||
@ -186,11 +184,11 @@ export default abstract class Chart<T extends Schema> {
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static dateToTimestamp(x: Date): number {
|
protected static dateToTimestamp(x: Date): number {
|
||||||
return Math.floor(x.getTime() / 1000);
|
return Math.floor(x.getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
protected static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
||||||
const y = date.getUTCFullYear();
|
const y = date.getUTCFullYear();
|
||||||
const m = date.getUTCMonth();
|
const m = date.getUTCMonth();
|
||||||
const d = date.getUTCDate();
|
const d = date.getUTCDate();
|
||||||
@ -202,7 +200,7 @@ export default abstract class Chart<T extends Schema> {
|
|||||||
return [y, m, d, h, _m, _s, _ms];
|
return [y, m, d, h, _m, _s, _ms];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getCurrentDate() {
|
protected static getCurrentDate() {
|
||||||
return Chart.parseDate(new Date());
|
return Chart.parseDate(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,8 +272,6 @@ export default abstract class Chart<T extends Schema> {
|
|||||||
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
||||||
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
|
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
|
||||||
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
|
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
|
||||||
this.repositoryUserPvForHour = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(hour);
|
|
||||||
this.repositoryUserPvForDay = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(day);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -725,51 +721,4 @@ export default abstract class Chart<T extends Schema> {
|
|||||||
}
|
}
|
||||||
return object as Unflatten<ChartResult<T>>;
|
return object as Unflatten<ChartResult<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number): Promise<
|
|
||||||
{
|
|
||||||
userId: string,
|
|
||||||
count: number,
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
|
|
||||||
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
|
|
||||||
|
|
||||||
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
|
|
||||||
|
|
||||||
const gt =
|
|
||||||
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
|
|
||||||
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
|
|
||||||
new Error('not happen') as never;
|
|
||||||
|
|
||||||
const repository =
|
|
||||||
span === 'hour' ? this.repositoryUserPvForHour :
|
|
||||||
span === 'day' ? this.repositoryUserPvForDay :
|
|
||||||
new Error('not happen') as never;
|
|
||||||
|
|
||||||
// ログ取得
|
|
||||||
const logs = await repository.createQueryBuilder()
|
|
||||||
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
|
|
||||||
.orderBy('___pv_visitor + ___upv_visitor + ___pv_user + ___upv_user', 'DESC')
|
|
||||||
.skip(offset)
|
|
||||||
.take(limit)
|
|
||||||
.getMany() as {
|
|
||||||
___pv_visitor: number,
|
|
||||||
___upv_visitor: number,
|
|
||||||
___pv_user: number,
|
|
||||||
___upv_user: number,
|
|
||||||
group: string,
|
|
||||||
}[];
|
|
||||||
const result = [] as {
|
|
||||||
userId: string,
|
|
||||||
count: number,
|
|
||||||
}[];
|
|
||||||
for (const row of logs) {
|
|
||||||
const userId = row.group;
|
|
||||||
const count = row.___pv_user + row.___upv_user + row.___pv_visitor + row.___upv_visitor;
|
|
||||||
result.push({ userId, count });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { MiUserAccountMoveLog, UserAccountMoveLogRepository } from '@/models/_.js';
|
||||||
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserAccountMoveLogEntityService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userAccountMoveLogRepository)
|
||||||
|
private userAccountMoveLogRepository: UserAccountMoveLogRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
private idService: IdService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async pack(
|
||||||
|
src: MiUserAccountMoveLog['id'] | MiUserAccountMoveLog,
|
||||||
|
me: { id: MiUser['id'] } | null | undefined,
|
||||||
|
) : Promise<Packed<'UserAccountMoveLog'>> {
|
||||||
|
const log = typeof src === 'object' ? src : await this.userAccountMoveLogRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
return await awaitAll({
|
||||||
|
id: log.id,
|
||||||
|
createdAt: this.idService.parse(log.id).date.toISOString(),
|
||||||
|
movedFromId: log.movedFromId,
|
||||||
|
movedFrom: this.userEntityService.pack(log.movedFrom ?? log.movedFromId, me, {
|
||||||
|
schema: 'UserDetailed',
|
||||||
|
}),
|
||||||
|
movedToId: log.movedToId,
|
||||||
|
movedTo: this.userEntityService.pack(log.movedTo ?? log.movedToId, me, {
|
||||||
|
schema: 'UserDetailed',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packMany(
|
||||||
|
reports: (MiUserAccountMoveLog['id'] | MiUserAccountMoveLog)[],
|
||||||
|
me: { id: MiUser['id'] } | null | undefined,
|
||||||
|
) : Promise<Packed<'UserAccountMoveLog'>[]> {
|
||||||
|
return (await Promise.allSettled(reports.map(x => this.pack(x, me))))
|
||||||
|
.filter(result => result.status === 'fulfilled')
|
||||||
|
.map(result => (result as PromiseFulfilledResult<Packed<'UserAccountMoveLog'>>).value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ export const DI = {
|
|||||||
userListMembershipsRepository: Symbol('userListMembershipsRepository'),
|
userListMembershipsRepository: Symbol('userListMembershipsRepository'),
|
||||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||||
userIpsRepository: Symbol('userIpsRepository'),
|
userIpsRepository: Symbol('userIpsRepository'),
|
||||||
|
userAccountMoveLogRepository: Symbol('userAccountMoveLogRepository'),
|
||||||
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
||||||
followingsRepository: Symbol('followingsRepository'),
|
followingsRepository: Symbol('followingsRepository'),
|
||||||
followRequestsRepository: Symbol('followRequestsRepository'),
|
followRequestsRepository: Symbol('followRequestsRepository'),
|
||||||
|
43
packages/backend/src/misc/collapsed-queue.ts
Normal file
43
packages/backend/src/misc/collapsed-queue.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Job<V> = {
|
||||||
|
value: V;
|
||||||
|
timer: NodeJS.Timeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CollapsedQueue<K, V> {
|
||||||
|
private jobs: Map<K, Job<V>> = new Map();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private timeout: number,
|
||||||
|
private collapse: (oldValue: V, newValue: V) => V,
|
||||||
|
private perform: (key: K, value: V) => Promise<void>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
enqueue(key: K, value: V) {
|
||||||
|
if (this.jobs.has(key)) {
|
||||||
|
const old = this.jobs.get(key)!;
|
||||||
|
const merged = this.collapse(old.value, value);
|
||||||
|
this.jobs.set(key, { ...old, value: merged });
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const job = this.jobs.get(key)!;
|
||||||
|
this.jobs.delete(key);
|
||||||
|
this.perform(key, job.value);
|
||||||
|
}, this.timeout);
|
||||||
|
this.jobs.set(key, { value, timer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performAllNow() {
|
||||||
|
const entries = [...this.jobs.entries()];
|
||||||
|
this.jobs.clear();
|
||||||
|
for (const [_key, job] of entries) {
|
||||||
|
clearTimeout(job.timer);
|
||||||
|
}
|
||||||
|
await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value)));
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
|||||||
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
||||||
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
|
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
|
||||||
import { packedUserListMembershipSchema, packedUserListSchema } from '@/models/json-schema/user-list.js';
|
import { packedUserListMembershipSchema, packedUserListSchema } from '@/models/json-schema/user-list.js';
|
||||||
|
import { packedUserAccountMoveLogSchema } from '@/models/json-schema/user-account-move-log.js';
|
||||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||||
import {
|
import {
|
||||||
@ -72,6 +73,7 @@ export const refs = {
|
|||||||
|
|
||||||
UserList: packedUserListSchema,
|
UserList: packedUserListSchema,
|
||||||
UserListMembership: packedUserListMembershipSchema,
|
UserListMembership: packedUserListMembershipSchema,
|
||||||
|
UserAccountMoveLog: packedUserAccountMoveLogSchema,
|
||||||
Ad: packedAdSchema,
|
Ad: packedAdSchema,
|
||||||
Announcement: packedAnnouncementSchema,
|
Announcement: packedAnnouncementSchema,
|
||||||
App: packedAppSchema,
|
App: packedAppSchema,
|
||||||
|
@ -74,6 +74,7 @@ import {
|
|||||||
MiUserProfile,
|
MiUserProfile,
|
||||||
MiUserPublickey,
|
MiUserPublickey,
|
||||||
MiUserSecurityKey,
|
MiUserSecurityKey,
|
||||||
|
MiUserAccountMoveLog,
|
||||||
MiWebhook,
|
MiWebhook,
|
||||||
MiBubbleGameRecord,
|
MiBubbleGameRecord,
|
||||||
MiReversiGame,
|
MiReversiGame,
|
||||||
@ -201,6 +202,12 @@ const $userListMembershipsRepository: Provider = {
|
|||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $userAccountMoveLogRepository: Provider = {
|
||||||
|
provide: DI.userAccountMoveLogRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiUserAccountMoveLog),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $userNotePiningsRepository: Provider = {
|
const $userNotePiningsRepository: Provider = {
|
||||||
provide: DI.userNotePiningsRepository,
|
provide: DI.userNotePiningsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
|
useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
|
||||||
@ -531,6 +538,7 @@ const $abuseReportResolversRepository: Provider = {
|
|||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
$userListFavoritesRepository,
|
$userListFavoritesRepository,
|
||||||
$userListMembershipsRepository,
|
$userListMembershipsRepository,
|
||||||
|
$userAccountMoveLogRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
$usedUsernamesRepository,
|
$usedUsernamesRepository,
|
||||||
@ -604,6 +612,7 @@ const $abuseReportResolversRepository: Provider = {
|
|||||||
$userListsRepository,
|
$userListsRepository,
|
||||||
$userListFavoritesRepository,
|
$userListFavoritesRepository,
|
||||||
$userListMembershipsRepository,
|
$userListMembershipsRepository,
|
||||||
|
$userAccountMoveLogRepository,
|
||||||
$userNotePiningsRepository,
|
$userNotePiningsRepository,
|
||||||
$userIpsRepository,
|
$userIpsRepository,
|
||||||
$usedUsernamesRepository,
|
$usedUsernamesRepository,
|
||||||
|
35
packages/backend/src/models/UserAccountMoveLog.ts
Normal file
35
packages/backend/src/models/UserAccountMoveLog.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Entity, Index, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||||
|
import { id } from './util/id.js';
|
||||||
|
import { MiUser } from './User.js';
|
||||||
|
|
||||||
|
@Entity('user_account_move_log')
|
||||||
|
export class MiUserAccountMoveLog {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public movedToId: MiUser['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => MiUser, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public movedTo: MiUser | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public movedFromId: MiUser['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => MiUser, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public movedFrom: MiUser | null;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the UserIp.',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
}
|
@ -64,6 +64,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
|||||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||||
|
import { MiUserAccountMoveLog } from '@/models/UserAccountMoveLog.js';
|
||||||
import { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { MiChannel } from '@/models/Channel.js';
|
import { MiChannel } from '@/models/Channel.js';
|
||||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||||
@ -148,6 +149,7 @@ export {
|
|||||||
MiUserMemo,
|
MiUserMemo,
|
||||||
MiBubbleGameRecord,
|
MiBubbleGameRecord,
|
||||||
MiReversiGame,
|
MiReversiGame,
|
||||||
|
MiUserAccountMoveLog,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AbuseReportResolversRepository = Repository<MiAbuseReportResolver>;
|
export type AbuseReportResolversRepository = Repository<MiAbuseReportResolver>;
|
||||||
@ -211,6 +213,7 @@ export type UserPendingsRepository = Repository<MiUserPending>;
|
|||||||
export type UserProfilesRepository = Repository<MiUserProfile>;
|
export type UserProfilesRepository = Repository<MiUserProfile>;
|
||||||
export type UserPublickeysRepository = Repository<MiUserPublickey>;
|
export type UserPublickeysRepository = Repository<MiUserPublickey>;
|
||||||
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
|
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
|
||||||
|
export type UserAccountMoveLogRepository = Repository<MiUserAccountMoveLog>;
|
||||||
export type WebhooksRepository = Repository<MiWebhook>;
|
export type WebhooksRepository = Repository<MiWebhook>;
|
||||||
export type ChannelsRepository = Repository<MiChannel>;
|
export type ChannelsRepository = Repository<MiChannel>;
|
||||||
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>;
|
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>;
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
export const packedUserAccountMoveLogSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
example: 'xxxxxxxxxx',
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
movedToId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
movedTo: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserDetailed',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
movedFromId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
movedFrom: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserDetailed',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
@ -73,6 +73,7 @@ import { MiUserPending } from '@/models/UserPending.js';
|
|||||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||||
|
import { MiUserAccountMoveLog } from '@/models/UserAccountMoveLog.js';
|
||||||
import { MiWebhook } from '@/models/Webhook.js';
|
import { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { MiChannel } from '@/models/Channel.js';
|
import { MiChannel } from '@/models/Channel.js';
|
||||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||||
@ -154,6 +155,7 @@ export const entities = [
|
|||||||
MiUserListMembership,
|
MiUserListMembership,
|
||||||
MiUserNotePining,
|
MiUserNotePining,
|
||||||
MiUserSecurityKey,
|
MiUserSecurityKey,
|
||||||
|
MiUserAccountMoveLog,
|
||||||
MiUsedUsername,
|
MiUsedUsername,
|
||||||
MiFollowing,
|
MiFollowing,
|
||||||
MiFollowRequest,
|
MiFollowRequest,
|
||||||
|
@ -16,6 +16,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
|||||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||||
|
import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js';
|
||||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||||
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
||||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||||
@ -72,6 +73,7 @@ import { AutoNoteRemovalProcessorService } from './processors/AutoNoteRemovalPro
|
|||||||
ImportUserListsProcessorService,
|
ImportUserListsProcessorService,
|
||||||
ImportCustomEmojisProcessorService,
|
ImportCustomEmojisProcessorService,
|
||||||
ImportAntennasProcessorService,
|
ImportAntennasProcessorService,
|
||||||
|
UserSuspendProcessorService,
|
||||||
DeleteAccountProcessorService,
|
DeleteAccountProcessorService,
|
||||||
TruncateAccountProcessorService,
|
TruncateAccountProcessorService,
|
||||||
DeleteFileProcessorService,
|
DeleteFileProcessorService,
|
||||||
|
@ -29,6 +29,7 @@ import { ImportBlockingProcessorService } from './processors/ImportBlockingProce
|
|||||||
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
|
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
|
||||||
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
|
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
|
||||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||||
|
import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js';
|
||||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||||
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
||||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||||
@ -110,6 +111,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
private importUserListsProcessorService: ImportUserListsProcessorService,
|
private importUserListsProcessorService: ImportUserListsProcessorService,
|
||||||
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
|
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
|
||||||
private importAntennasProcessorService: ImportAntennasProcessorService,
|
private importAntennasProcessorService: ImportAntennasProcessorService,
|
||||||
|
private userSuspendProcessorService: UserSuspendProcessorService,
|
||||||
private deleteAccountProcessorService: DeleteAccountProcessorService,
|
private deleteAccountProcessorService: DeleteAccountProcessorService,
|
||||||
private truncateAccountProcessorService: TruncateAccountProcessorService,
|
private truncateAccountProcessorService: TruncateAccountProcessorService,
|
||||||
private deleteFileProcessorService: DeleteFileProcessorService,
|
private deleteFileProcessorService: DeleteFileProcessorService,
|
||||||
@ -192,6 +194,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
||||||
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||||
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||||
|
case 'userSuspend': return this.userSuspendProcessorService.process(job);
|
||||||
case 'reportAbuse': return this.reportAbuseProcessorService.process(job);
|
case 'reportAbuse': return this.reportAbuseProcessorService.process(job);
|
||||||
case 'truncateAccount': return this.truncateAccountProcessorService.process(job);
|
case 'truncateAccount': return this.truncateAccountProcessorService.process(job);
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||||
|
@ -40,7 +40,7 @@ export class DeleteAccountProcessorService {
|
|||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
|
this.logger = this.queueLoggerService.logger.createSubLogger('account:delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteNotes(user: MiUser) {
|
private async deleteNotes(user: MiUser) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import httpSignature from '@peertube/http-signature';
|
import httpSignature from '@peertube/http-signature';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
@ -26,12 +26,15 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
|||||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
import { MiNote } from '@/models/Note.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { InboxJobData } from '../types.js';
|
import type { InboxJobData } from '../types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InboxProcessorService {
|
export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private updateInstanceQueue: CollapsedQueue<MiNote['id'], Date>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
@ -48,6 +51,7 @@ export class InboxProcessorService {
|
|||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
||||||
|
this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -180,10 +184,7 @@ export class InboxProcessorService {
|
|||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
|
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
|
||||||
this.federatedInstanceService.update(i.id, {
|
this.updateInstanceQueue.enqueue(i.id, new Date());
|
||||||
latestRequestReceivedAt: new Date(),
|
|
||||||
isNotResponding: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
|
|
||||||
@ -211,4 +212,27 @@ export class InboxProcessorService {
|
|||||||
}
|
}
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public collapseUpdateInstanceJobs(oldValue: Date, newValue: Date) {
|
||||||
|
return oldValue < newValue ? newValue : oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async performUpdateInstance(id: string, value: Date) {
|
||||||
|
await this.federatedInstanceService.update(id, {
|
||||||
|
latestRequestReceivedAt: value,
|
||||||
|
isNotResponding: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
|
await this.updateInstanceQueue.performAllNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
async onApplicationShutdown(signal?: string) {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import type {
|
||||||
|
AntennasRepository,
|
||||||
|
ClipNotesRepository,
|
||||||
|
ClipsRepository,
|
||||||
|
FollowRequestsRepository,
|
||||||
|
UserListMembershipsRepository,
|
||||||
|
UserListsRepository, UsersRepository,
|
||||||
|
WebhooksRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
|
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||||
|
import type * as Bull from "bullmq";
|
||||||
|
import type { DbUserSuspendJobData } from "@/queue/types.js";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserSuspendProcessorService {
|
||||||
|
public logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followRequestsRepository)
|
||||||
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.antennasRepository)
|
||||||
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
|
@Inject(DI.webhooksRepository)
|
||||||
|
private webhooksRepository: WebhooksRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userListsRepository)
|
||||||
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipsRepository)
|
||||||
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipNotesRepository)
|
||||||
|
private clipNotesRepository: ClipNotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userListMembershipsRepository)
|
||||||
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('account:suspend');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<DbUserSuspendJobData>): Promise<string | void> {
|
||||||
|
this.logger.warn(`Cleaning up suspended account of ${job.data.user.id} ...`, { userSuspendJobData: job.data });
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
|
if (user == null) {
|
||||||
|
return 'User not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
let cursor = '';
|
||||||
|
while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
|
||||||
|
const clipNotes = await this.clipNotesRepository.createQueryBuilder('c')
|
||||||
|
.select('c.id')
|
||||||
|
.innerJoin('c.note', 'n')
|
||||||
|
.where('n.userId = :userId', { userId: user.id })
|
||||||
|
.andWhere('c.id > :cursor', { cursor })
|
||||||
|
.orderBy('c.id', 'ASC')
|
||||||
|
.limit(100)
|
||||||
|
.getRawMany<{ id: string }>();
|
||||||
|
|
||||||
|
if (clipNotes.length === 0) break;
|
||||||
|
|
||||||
|
cursor = clipNotes.at(-1)?.id ?? '';
|
||||||
|
|
||||||
|
promises.push(this.clipNotesRepository.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) })
|
||||||
|
.execute());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled([
|
||||||
|
this.followRequestsRepository.delete({ followeeId: user.id }),
|
||||||
|
this.followRequestsRepository.delete({ followerId: user.id }),
|
||||||
|
|
||||||
|
this.antennasRepository.delete({ userId: user.id }),
|
||||||
|
this.webhooksRepository.delete({ userId: user.id }),
|
||||||
|
this.userListsRepository.delete({ userId: user.id }),
|
||||||
|
this.clipsRepository.delete({ userId: user.id }),
|
||||||
|
|
||||||
|
...promises,
|
||||||
|
this.userListMembershipsRepository.delete({ userId: user.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.logger.info(`Completed cleaning up suspended account of ${job.data.user.id}`);
|
||||||
|
|
||||||
|
return 'done';
|
||||||
|
}
|
||||||
|
}
|
@ -89,6 +89,10 @@ export type DbUserTruncateJobData = {
|
|||||||
purgeDrive: boolean;
|
purgeDrive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DbUserSuspendJobData = {
|
||||||
|
user: ThinUser
|
||||||
|
};
|
||||||
|
|
||||||
export type DbUserImportJobData = {
|
export type DbUserImportJobData = {
|
||||||
user: ThinUser;
|
user: ThinUser;
|
||||||
fileId: MiDriveFile['id'];
|
fileId: MiDriveFile['id'];
|
||||||
|
@ -79,6 +79,7 @@ import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-ab
|
|||||||
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
||||||
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
||||||
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
||||||
|
import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user-account-move-logs.js';
|
||||||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||||
@ -474,6 +475,7 @@ const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abu
|
|||||||
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
|
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
|
||||||
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
|
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
|
||||||
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
|
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
|
||||||
|
const $admin_showUserAccountMoveLogs: Provider = { provide: 'ep:admin/show-user-account-move-logs', useClass: ep___admin_showUserAccountMoveLogs.default };
|
||||||
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
|
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
|
||||||
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
|
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
|
||||||
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
||||||
@ -873,6 +875,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||||||
$admin_sendEmail,
|
$admin_sendEmail,
|
||||||
$admin_serverInfo,
|
$admin_serverInfo,
|
||||||
$admin_showModerationLogs,
|
$admin_showModerationLogs,
|
||||||
|
$admin_showUserAccountMoveLogs,
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
@ -1266,6 +1269,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||||||
$admin_sendEmail,
|
$admin_sendEmail,
|
||||||
$admin_serverInfo,
|
$admin_serverInfo,
|
||||||
$admin_showModerationLogs,
|
$admin_showModerationLogs,
|
||||||
|
$admin_showUserAccountMoveLogs,
|
||||||
$admin_showUser,
|
$admin_showUser,
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
|
@ -79,6 +79,7 @@ import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-ab
|
|||||||
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
|
||||||
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
|
||||||
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
|
||||||
|
import * as ep___admin_showUserAccountMoveLogs from './endpoints/admin/show-user-account-move-logs.js';
|
||||||
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||||
@ -472,6 +473,7 @@ const eps = [
|
|||||||
['admin/send-email', ep___admin_sendEmail],
|
['admin/send-email', ep___admin_sendEmail],
|
||||||
['admin/server-info', ep___admin_serverInfo],
|
['admin/server-info', ep___admin_serverInfo],
|
||||||
['admin/show-moderation-logs', ep___admin_showModerationLogs],
|
['admin/show-moderation-logs', ep___admin_showModerationLogs],
|
||||||
|
['admin/show-user-account-move-logs', ep___admin_showUserAccountMoveLogs],
|
||||||
['admin/show-user', ep___admin_showUser],
|
['admin/show-user', ep___admin_showUser],
|
||||||
['admin/show-users', ep___admin_showUsers],
|
['admin/show-users', ep___admin_showUsers],
|
||||||
['admin/suspend-user', ep___admin_suspendUser],
|
['admin/suspend-user', ep___admin_suspendUser],
|
||||||
|
@ -97,6 +97,11 @@ export const meta = {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
lastReadAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -140,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
userId: announcement.userId,
|
userId: announcement.userId,
|
||||||
user: announcement.userInfo,
|
user: announcement.userInfo,
|
||||||
reads: announcement.reads,
|
reads: announcement.reads,
|
||||||
|
lastReadAt: announcement.lastReadAt?.toISOString() ?? null,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { UserAccountMoveLogRepository } from '@/models/_.js';
|
||||||
|
import { UserAccountMoveLogEntityService } from '@/core/entities/UserAccountMoveLogEntityService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
kind: 'read:admin:show-account-move-log',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
movedToId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
movedTo: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserDetailed',
|
||||||
|
},
|
||||||
|
movedFromId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
movedFrom: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserDetailed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
movedFromId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
movedToId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.userAccountMoveLogRepository)
|
||||||
|
private userAccountMoveLogRepository: UserAccountMoveLogRepository,
|
||||||
|
|
||||||
|
private userAccountMoveLogEntityService: UserAccountMoveLogEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.queryService.makePaginationQuery(this.userAccountMoveLogRepository.createQueryBuilder('accountMoveLogs'), ps.sinceId, ps.untilId);
|
||||||
|
|
||||||
|
if (ps.movedFromId != null) {
|
||||||
|
query.andWhere('accountMoveLogs.movedFromId = :movedFromId', { movedFromId: ps.movedFromId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.movedToId != null) {
|
||||||
|
query.andWhere('accountMoveLogs.movedToId = :movedToId', { movedToId: ps.movedToId });
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountMoveLogs = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -72,12 +72,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (ps.hostname) {
|
if (ps.hostname) {
|
||||||
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
||||||
}
|
}
|
||||||
const chartUsers: { userId: string; count: number; }[] = [];
|
|
||||||
|
let pvRankedUsers: { userId: string; count: number; }[] | undefined = undefined;
|
||||||
if (ps.sort?.endsWith('pv')) {
|
if (ps.sort?.endsWith('pv')) {
|
||||||
await this.perUserPvChart.getChartUsers('hour', 0, null, ps.limit, ps.offset).then(users => {
|
// 直近12時間のPVランキングを取得
|
||||||
chartUsers.push(...users);
|
pvRankedUsers = await this.perUserPvChart.getUsersRanking(
|
||||||
});
|
'hour', ps.sort.startsWith('+') ? 'DESC' : 'ASC',
|
||||||
|
12, null, ps.limit, ps.offset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ps.sort) {
|
switch (ps.sort) {
|
||||||
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
||||||
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
||||||
@ -85,16 +89,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
case '-createdAt': query.orderBy('user.id', 'ASC'); break;
|
case '-createdAt': query.orderBy('user.id', 'ASC'); break;
|
||||||
case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
|
case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
|
||||||
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
|
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
|
||||||
case '+pv':
|
case '+pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||||
if (chartUsers.length > 0) {
|
case '-pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||||
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '-pv':
|
|
||||||
if (chartUsers.length > 0) {
|
|
||||||
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default: query.orderBy('user.id', 'ASC'); break;
|
default: query.orderBy('user.id', 'ASC'); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +103,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
const users = await query.getMany();
|
const users = await query.getMany();
|
||||||
if (ps.sort === '+pv') {
|
if (ps.sort === '+pv') {
|
||||||
users.sort((a, b) => {
|
users.sort((a, b) => {
|
||||||
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
|
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||||
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
|
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||||
return bPv - aPv;
|
return bPv - aPv;
|
||||||
});
|
});
|
||||||
} else if (ps.sort === '-pv') {
|
} else if (ps.sort === '-pv') {
|
||||||
users.sort((a, b) => {
|
users.sort((a, b) => {
|
||||||
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
|
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||||
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
|
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||||
return aPv - bPv;
|
return aPv - bPv;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
keydbtest:
|
dragonflytest:
|
||||||
image: eqalpha/keydb:latest
|
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||||
|
ulimits:
|
||||||
|
memlock: -1
|
||||||
|
environment:
|
||||||
|
DFLY_version_check: false
|
||||||
|
DFLY_tcp_backlog: 2048
|
||||||
|
DFLY_default_lua_flags: allow-undeclared-keys
|
||||||
|
DFLY_pipeline_squash: 0
|
||||||
|
DFLY_multi_exec_squash: false
|
||||||
|
DFLY_conn_io_threads: 4
|
||||||
|
DFLY_epoll_file_threads: 4
|
||||||
|
DFLY_proactor_threads: 4
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:56312:6379"
|
- "127.0.0.1:56312:6379"
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="user" :class="$style.root">
|
<div v-if="user" :class="$style.root">
|
||||||
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
||||||
{{ i18n.ts.accountMoved }}
|
<span v-if="movedTo">{{ i18n.ts.accountMoved }}</span>
|
||||||
|
<span v-if="movedFrom">{{ i18n.ts.accountMovedFrom }}</span>
|
||||||
<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -22,10 +23,11 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||||||
const user = ref<Misskey.entities.UserLite>();
|
const user = ref<Misskey.entities.UserLite>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
movedTo: string; // user id
|
movedTo?: string; // user id
|
||||||
|
movedFrom?: string; // user id
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
misskeyApi('users/show', { userId: props.movedTo ?? props.movedFrom }).then(u => user.value = u);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
import { debounce } from 'throttle-debounce';
|
||||||
import contains from '@/scripts/contains.js';
|
import contains from '@/scripts/contains.js';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||||
import { acct } from '@/filters/user.js';
|
import { acct } from '@/filters/user.js';
|
||||||
@ -189,6 +190,7 @@ const mfmTags = ref<string[]>([]);
|
|||||||
const mfmParams = ref<string[]>([]);
|
const mfmParams = ref<string[]>([]);
|
||||||
const select = ref(-1);
|
const select = ref(-1);
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex('high');
|
||||||
|
const abortController = ref<AbortController>();
|
||||||
|
|
||||||
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
|
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
|
||||||
emit('done', { type, value });
|
emit('done', { type, value });
|
||||||
@ -217,6 +219,39 @@ function setPosition() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchUsers = debounce(1000, (query: string, cacheKey: string) => {
|
||||||
|
if (abortController.value) {
|
||||||
|
abortController.value.abort();
|
||||||
|
}
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
misskeyApi('users/search-by-username-and-host', {
|
||||||
|
username: query,
|
||||||
|
limit: 10,
|
||||||
|
detail: false,
|
||||||
|
}, undefined, abortController.value.signal).then(searchedUsers => {
|
||||||
|
users.value = searchedUsers as any[];
|
||||||
|
fetching.value = false;
|
||||||
|
// キャッシュ
|
||||||
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchHashtags = debounce(1000, (query: string, cacheKey: string) => {
|
||||||
|
if (abortController.value) {
|
||||||
|
abortController.value.abort();
|
||||||
|
}
|
||||||
|
abortController.value = new AbortController();
|
||||||
|
misskeyApi('hashtags/search', {
|
||||||
|
query,
|
||||||
|
limit: 30,
|
||||||
|
}, undefined, abortController.value.signal).then(searchedHashtags => {
|
||||||
|
hashtags.value = searchedHashtags as any[];
|
||||||
|
fetching.value = false;
|
||||||
|
// キャッシュ
|
||||||
|
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function exec() {
|
function exec() {
|
||||||
select.value = -1;
|
select.value = -1;
|
||||||
if (suggests.value) {
|
if (suggests.value) {
|
||||||
@ -238,16 +273,7 @@ function exec() {
|
|||||||
users.value = JSON.parse(cache);
|
users.value = JSON.parse(cache);
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
misskeyApi('users/search-by-username-and-host', {
|
searchUsers(props.q, cacheKey);
|
||||||
username: props.q,
|
|
||||||
limit: 10,
|
|
||||||
detail: false,
|
|
||||||
}).then(searchedUsers => {
|
|
||||||
users.value = searchedUsers as any[];
|
|
||||||
fetching.value = false;
|
|
||||||
// キャッシュ
|
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (props.type === 'hashtag') {
|
} else if (props.type === 'hashtag') {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === '') {
|
||||||
@ -261,15 +287,7 @@ function exec() {
|
|||||||
hashtags.value = hashtags;
|
hashtags.value = hashtags;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
misskeyApi('hashtags/search', {
|
searchHashtags(props.q, cacheKey);
|
||||||
query: props.q,
|
|
||||||
limit: 30,
|
|
||||||
}).then(searchedHashtags => {
|
|
||||||
hashtags.value = searchedHashtags as any[];
|
|
||||||
fetching.value = false;
|
|
||||||
// キャッシュ
|
|
||||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (props.type === 'emoji') {
|
} else if (props.type === 'emoji') {
|
||||||
|
@ -594,6 +594,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;
|
||||||
@ -700,7 +701,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;
|
||||||
|
@ -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>
|
||||||
@ -123,7 +123,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>
|
||||||
|
@ -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;
|
||||||
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
{{ message }}
|
{{ message }}
|
||||||
</MkInfo>
|
</MkInfo>
|
||||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
<MkInput v-model="username" :debounce="true" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #prefix><i class="ti ti-lock"></i></template>
|
<template #prefix><i class="ti ti-lock"></i></template>
|
||||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
<MkButton type="submit" large primary rounded :disabled="!user || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||||
@ -64,6 +64,7 @@ import { login } from '@/account.js';
|
|||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const signing = ref(false);
|
const signing = ref(false);
|
||||||
|
const userAbortController = ref<AbortController>();
|
||||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
@ -97,9 +98,13 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onUsernameChange(): void {
|
function onUsernameChange(): void {
|
||||||
|
if (userAbortController.value) {
|
||||||
|
userAbortController.value.abort();
|
||||||
|
}
|
||||||
|
userAbortController.value = new AbortController();
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
}).then(userResponse => {
|
}, undefined, userAbortController.value.signal).then(userResponse => {
|
||||||
user.value = userResponse;
|
user.value = userResponse;
|
||||||
}, () => {
|
}, () => {
|
||||||
user.value = null;
|
user.value = null;
|
||||||
|
@ -189,7 +189,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await misskeyApi('signup', {
|
await os.apiWithDialog('signup', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
password: password.value.password,
|
password: password.value.password,
|
||||||
emailAddress: email.value,
|
emailAddress: email.value,
|
||||||
@ -198,7 +198,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
'm-captcha-response': mCaptchaResponse.value,
|
'm-captcha-response': mCaptchaResponse.value,
|
||||||
'g-recaptcha-response': reCaptchaResponse.value,
|
'g-recaptcha-response': reCaptchaResponse.value,
|
||||||
'turnstile-response': turnstileResponse.value,
|
'turnstile-response': turnstileResponse.value,
|
||||||
});
|
}, undefined, (res) => {
|
||||||
if (instance.emailRequiredForSignup) {
|
if (instance.emailRequiredForSignup) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -207,26 +207,18 @@ async function onSubmit(): Promise<void> {
|
|||||||
});
|
});
|
||||||
emit('signupEmailPending');
|
emit('signupEmailPending');
|
||||||
} else {
|
} else {
|
||||||
const res = await misskeyApi('signin', {
|
emit('signup', { id: res.id, i: res.token });
|
||||||
username: username.value,
|
|
||||||
password: password.value.password,
|
|
||||||
});
|
|
||||||
emit('signup', res);
|
|
||||||
|
|
||||||
if (props.autoSet) {
|
if (props.autoSet) {
|
||||||
return login(res.i, '/onboarding');
|
login(res.i, '/onboarding');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
hcaptcha.value?.reset?.();
|
hcaptcha.value?.reset?.();
|
||||||
recaptcha.value?.reset?.();
|
recaptcha.value?.reset?.();
|
||||||
turnstile.value?.reset?.();
|
turnstile.value?.reset?.();
|
||||||
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: i18n.ts.somethingHappened,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
{{ i18n.ts._announcement.silence }}
|
{{ i18n.ts._announcement.silence }}
|
||||||
<template #caption>{{ i18n.ts._announcement.silenceDescription }}</template>
|
<template #caption>{{ i18n.ts._announcement.silenceDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }}</p>
|
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }} <span v-if="lastReadAt">(<MkTime :time="lastReadAt" mode="absolute"/>)</span></p>
|
||||||
<MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini>
|
<MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini>
|
||||||
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
@ -94,6 +94,7 @@ const closeDuration = ref<number>(props.announcement ? props.announcement.closeD
|
|||||||
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
|
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
|
||||||
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);
|
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);
|
||||||
const reads = ref<number>(props.announcement ? props.announcement.reads : 0);
|
const reads = ref<number>(props.announcement ? props.announcement.reads : 0);
|
||||||
|
const lastReadAt = ref<string | null>(props.announcement ? props.announcement.lastReadAt : null);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||||
|
@ -16,16 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #header>{{ i18n.ts.selectUser }}</template>
|
<template #header>{{ i18n.ts.selectUser }}</template>
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.form">
|
<div :class="$style.form">
|
||||||
<MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search">
|
<MkInput v-if="localOnly" v-model="username" :debounce="true" :autofocus="true" @update:modelValue="search">
|
||||||
<template #label>{{ i18n.ts.username }}</template>
|
<template #label>{{ i18n.ts.username }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<FormSplit v-else :minWidth="170">
|
<FormSplit v-else :minWidth="170">
|
||||||
<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
|
<MkInput v-model="username" :debounce="true" :autofocus="true" @update:modelValue="search">
|
||||||
<template #label>{{ i18n.ts.username }}</template>
|
<template #label>{{ i18n.ts.username }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="host" :datalist="[hostname]" @update:modelValue="search">
|
<MkInput v-model="host" :debounce="true" :datalist="[hostname]" @update:modelValue="search">
|
||||||
<template #label>{{ i18n.ts.host }}</template>
|
<template #label>{{ i18n.ts.host }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
@ -92,18 +92,23 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
|||||||
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
||||||
const selected = ref<Misskey.entities.UserLite | null>(null);
|
const selected = ref<Misskey.entities.UserLite | null>(null);
|
||||||
const dialogEl = ref();
|
const dialogEl = ref();
|
||||||
|
const abortController = ref<AbortController>();
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
|
if (abortController.value) {
|
||||||
|
abortController.value.abort();
|
||||||
|
}
|
||||||
if (username.value === '' && host.value === '') {
|
if (username.value === '' && host.value === '') {
|
||||||
users.value = [];
|
users.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
abortController.value = new AbortController();
|
||||||
misskeyApi('users/search-by-username-and-host', {
|
misskeyApi('users/search-by-username-and-host', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
host: props.localOnly ? '.' : host.value,
|
host: props.localOnly ? '.' : host.value,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
detail: false,
|
detail: false,
|
||||||
}).then(_users => {
|
}, undefined, abortController.value.signal).then(_users => {
|
||||||
users.value = _users.filter((u) => {
|
users.value = _users.filter((u) => {
|
||||||
if (props.includeSelf) {
|
if (props.includeSelf) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -34,11 +34,19 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
|||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as P,
|
data: P = {} as P,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
) => {
|
onSuccess?: ((res: Misskey.api.SwitchCaseResponseType<E, P>) => void) | null | undefined,
|
||||||
|
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||||
|
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> => {
|
||||||
const promise = misskeyApi(endpoint, data, token);
|
const promise = misskeyApi(endpoint, data, token);
|
||||||
promiseDialog(promise, null, async (err) => {
|
promiseDialog(promise, onSuccess, onFailure ?? (err => apiErrorHandler(err, endpoint)));
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function apiErrorHandler(err: Misskey.api.APIError, endpoint?: string): Promise<void> {
|
||||||
let title: string | undefined;
|
let title: string | undefined;
|
||||||
let text = err.message + '\n' + err.id;
|
let text = err.message + '\n' + err.id;
|
||||||
|
|
||||||
if (err.code === 'INTERNAL_ERROR') {
|
if (err.code === 'INTERNAL_ERROR') {
|
||||||
title = i18n.ts.internalServerError;
|
title = i18n.ts.internalServerError;
|
||||||
text = i18n.ts.internalServerErrorDescription;
|
text = i18n.ts.internalServerErrorDescription;
|
||||||
@ -71,23 +79,32 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
|||||||
} else if (err.code === 'ROLE_PERMISSION_DENIED') {
|
} else if (err.code === 'ROLE_PERMISSION_DENIED') {
|
||||||
title = i18n.ts.permissionDeniedError;
|
title = i18n.ts.permissionDeniedError;
|
||||||
text = i18n.ts.permissionDeniedErrorDescription;
|
text = i18n.ts.permissionDeniedErrorDescription;
|
||||||
} else if (err.code.startsWith('TOO_MANY')) {
|
} else if (err.code?.startsWith('TOO_MANY')) {
|
||||||
title = i18n.ts.youCannotCreateAnymore;
|
title = i18n.ts.youCannotCreateAnymore;
|
||||||
text = `${i18n.ts.error}: ${err.id}`;
|
text = `${i18n.ts.error}: ${err.id}`;
|
||||||
} else if (err.message.startsWith('Unexpected token')) {
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error Misskey内部で定義されていない不明なエラー
|
||||||
|
if (!err.id && (err.statusCode ?? 0) > 499) {
|
||||||
title = i18n.ts.gotInvalidResponseError;
|
title = i18n.ts.gotInvalidResponseError;
|
||||||
text = i18n.ts.gotInvalidResponseErrorDescription;
|
text = i18n.ts.gotInvalidResponseErrorDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.id && !title) {
|
||||||
|
title = i18n.ts.somethingHappened;
|
||||||
|
} else if (!title) {
|
||||||
|
title = i18n.ts.somethingHappened;
|
||||||
|
text = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
alert({
|
alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
details: err.info,
|
// @ts-expect-error Misskeyのエラーならinfoを、そうでなければそのまま表示
|
||||||
|
details: err.id ? err.info : err as unknown,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return promise;
|
|
||||||
}) as typeof misskeyApi;
|
|
||||||
|
|
||||||
export function promiseDialog<T>(
|
export function promiseDialog<T>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
|
@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ announcement.title }}</span>
|
<span>{{ announcement.title }}</span>
|
||||||
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
|
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
|
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
|
||||||
{{ i18n.ts._announcement.silence }}
|
{{ i18n.ts._announcement.silence }}
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
|
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></p>
|
||||||
<MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini>
|
<MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini>
|
||||||
<MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton>
|
<MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton>
|
||||||
<div class="buttons _buttons">
|
<div class="buttons _buttons">
|
||||||
|
@ -156,6 +156,11 @@ const menuDef = computed(() => [{
|
|||||||
text: i18n.ts.moderationLogs,
|
text: i18n.ts.moderationLogs,
|
||||||
to: '/admin/modlog',
|
to: '/admin/modlog',
|
||||||
active: currentPage.value?.route.name === 'modlog',
|
active: currentPage.value?.route.name === 'modlog',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-list-search',
|
||||||
|
text: i18n.ts.userAccountMoveLogs,
|
||||||
|
to: '/admin/useraccountmovelog',
|
||||||
|
active: currentPage.value?.route.name === 'useraccountmovelog',
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
title: i18n.ts.settings,
|
title: i18n.ts.settings,
|
||||||
|
98
packages/frontend/src/pages/admin/useraccountmovelog.vue
Normal file
98
packages/frontend/src/pages/admin/useraccountmovelog.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
|
<MkSpacer :contentMax="900">
|
||||||
|
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||||
|
<MkInput v-model="movedFromId" style="margin: 0; flex: 1;">
|
||||||
|
<template #label> {{ i18n.ts.moveFromId }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="movedToId" style="margin: 0; flex: 1;">
|
||||||
|
<template #label> {{ i18n.ts.movedToId }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkFolder v-for="item in items" :key="item.id">
|
||||||
|
<template #label>
|
||||||
|
{{ i18n.tsx.userAccountMoveLogsTitle({
|
||||||
|
from: '@' + item.movedFrom.username + (item.movedFrom.host ? `@${item.movedFrom.host}` : ''),
|
||||||
|
to: '@' + item.movedTo.username + (item.movedTo.host ? `@${item.movedTo.host}` : '')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<div :class="$style.card">
|
||||||
|
<MkA :to="userPage(item.movedFrom)" :class="$style.cardContent">
|
||||||
|
<MkAvatar :user="item.movedFrom" :class="$style.avatar" link/>
|
||||||
|
<MkAcct :user="item.movedFrom"/>
|
||||||
|
</MkA>
|
||||||
|
→
|
||||||
|
<MkA :to="userPage(item.movedTo)" :class="$style.cardContent">
|
||||||
|
<MkAvatar :user="item.movedTo" :class="$style.avatar"/>
|
||||||
|
<MkAcct :user="item.movedTo"/>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</div>
|
||||||
|
</MkPagination>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, shallowRef, ref } from 'vue';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import { userPage } from '@/filters/user.js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
|
const logs = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
const movedToId = ref('');
|
||||||
|
const movedFromId = ref('');
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'admin/show-user-account-move-logs' as const,
|
||||||
|
limit: 30,
|
||||||
|
params: computed(() => ({
|
||||||
|
movedFromId: movedFromId.value === '' ? null : movedFromId.value,
|
||||||
|
movedToId: movedToId.value === '' ? null : movedToId.value,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
|
definePageMetadata(() => ({
|
||||||
|
title: i18n.ts.userAccountMoveLogs,
|
||||||
|
icon: 'ti ti-list-search',
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--margin);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: var(--margin);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent{
|
||||||
|
display: flex;
|
||||||
|
gap: var(--margin);
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSpacer :contentMax="700">
|
<MkSpacer :contentMax="700">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()">
|
<MkInput v-model="endpoint" :debounce="true" :datalist="endpoints" @update:modelValue="onEndpointChange()">
|
||||||
<template #label>Endpoint</template>
|
<template #label>Endpoint</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea v-model="body" code>
|
<MkTextarea v-model="body" code>
|
||||||
@ -50,6 +50,7 @@ const endpoints = ref<string[]>([]);
|
|||||||
const sending = ref(false);
|
const sending = ref(false);
|
||||||
const res = ref('');
|
const res = ref('');
|
||||||
const withCredential = ref(true);
|
const withCredential = ref(true);
|
||||||
|
const endpointAbortController = ref<AbortController>();
|
||||||
|
|
||||||
misskeyApi('endpoints').then(endpointResponse => {
|
misskeyApi('endpoints').then(endpointResponse => {
|
||||||
endpoints.value = endpointResponse;
|
endpoints.value = endpointResponse;
|
||||||
@ -68,7 +69,11 @@ function send() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onEndpointChange() {
|
function onEndpointChange() {
|
||||||
misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
|
if (endpointAbortController.value) {
|
||||||
|
endpointAbortController.value.abort();
|
||||||
|
}
|
||||||
|
endpointAbortController.value = new AbortController();
|
||||||
|
misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null, endpointAbortController.value.signal).then(resp => {
|
||||||
const endpointBody = {};
|
const endpointBody = {};
|
||||||
for (const p of resp.params) {
|
for (const p of resp.params) {
|
||||||
endpointBody[p.name] =
|
endpointBody[p.name] =
|
||||||
|
@ -349,6 +349,7 @@ definePageMetadata(() => ({
|
|||||||
> .img {
|
> .img {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
@ -395,6 +396,7 @@ definePageMetadata(() => ({
|
|||||||
> .img {
|
> .img {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
@ -132,7 +132,9 @@ async function deleteAccount() {
|
|||||||
{
|
{
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.ts.deleteAccountConfirm,
|
text: i18n.ts.deleteAccountConfirmAndWarn,
|
||||||
|
okWaitInitiate: 'dialog',
|
||||||
|
okWaitDuration: 5,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
}
|
}
|
||||||
@ -147,6 +149,7 @@ async function deleteAccount() {
|
|||||||
|
|
||||||
await os.alert({
|
await os.alert({
|
||||||
title: i18n.ts._accountDelete.started,
|
title: i18n.ts._accountDelete.started,
|
||||||
|
text: i18n.ts._accountDelete.dontLogin,
|
||||||
});
|
});
|
||||||
|
|
||||||
await signout();
|
await signout();
|
||||||
|
@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<div class="profile _gaps">
|
<div class="profile _gaps">
|
||||||
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
||||||
|
<MkAccountMoved v-if="movedFromLog" :movedFrom="movedFromLog[0]?.movedFromId"/>
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
||||||
|
|
||||||
<div :key="user.id" class="main _panel">
|
<div :key="user.id" class="main _panel">
|
||||||
@ -281,6 +282,7 @@ const memoDraft = ref(props.user.memo);
|
|||||||
const isEditingMemo = ref(false);
|
const isEditingMemo = ref(false);
|
||||||
const moderationNote = ref(props.user.moderationNote);
|
const moderationNote = ref(props.user.moderationNote);
|
||||||
const editModerationNote = ref(false);
|
const editModerationNote = ref(false);
|
||||||
|
const movedFromLog = ref<null | {movedFromId:string;}[]>(null);
|
||||||
|
|
||||||
const hideModerationNote = !iAmModerator || (defaultStore.state.privateMode && defaultStore.state.hideModerationLog);
|
const hideModerationNote = !iAmModerator || (defaultStore.state.privateMode && defaultStore.state.hideModerationLog);
|
||||||
const hideRoleList = defaultStore.state.privateMode && defaultStore.state.hideRoleList;
|
const hideRoleList = defaultStore.state.privateMode && defaultStore.state.hideRoleList;
|
||||||
@ -307,6 +309,15 @@ function menu(ev: MouseEvent) {
|
|||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMovedFromLog() {
|
||||||
|
if (!props.user.id) {
|
||||||
|
movedFromLog.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
movedFromLog.value = await misskeyApi('admin/show-user-account-move-logs', { movedToId: props.user.id });
|
||||||
|
}
|
||||||
|
|
||||||
function parallaxLoop() {
|
function parallaxLoop() {
|
||||||
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
|
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
|
||||||
parallax();
|
parallax();
|
||||||
@ -383,6 +394,9 @@ function buildSkebStatus(): string {
|
|||||||
watch([props.user], () => {
|
watch([props.user], () => {
|
||||||
memoDraft.value = props.user.memo;
|
memoDraft.value = props.user.memo;
|
||||||
fetchSkebStatus();
|
fetchSkebStatus();
|
||||||
|
if ($i?.isModerator) {
|
||||||
|
fetchMovedFromLog();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -401,6 +415,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchSkebStatus();
|
fetchSkebStatus();
|
||||||
|
if ($i?.isModerator) {
|
||||||
|
fetchMovedFromLog();
|
||||||
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
adjustMemoTextarea();
|
adjustMemoTextarea();
|
||||||
});
|
});
|
||||||
|
@ -436,6 +436,10 @@ const routes: RouteDef[] = [{
|
|||||||
path: '/modlog',
|
path: '/modlog',
|
||||||
name: 'modlog',
|
name: 'modlog',
|
||||||
component: page(() => import('@/pages/admin/modlog.vue')),
|
component: page(() => import('@/pages/admin/modlog.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/useraccountmovelog',
|
||||||
|
name: 'useraccountmovelog',
|
||||||
|
component: page(() => import('@/pages/admin/useraccountmovelog.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
@ -44,14 +44,15 @@ export function misskeyApi<
|
|||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
const body = res.status === 204 ? null : await res.json();
|
if (res.ok && res.status !== 204) {
|
||||||
|
const body = await res.json();
|
||||||
if (res.status === 200) {
|
|
||||||
resolve(body);
|
resolve(body);
|
||||||
} else if (res.status === 204) {
|
} else if (res.status === 204) {
|
||||||
resolve(undefined as _ResT); // void -> undefined
|
resolve(undefined as _ResT); // void -> undefined
|
||||||
} else {
|
} else {
|
||||||
reject(body.error);
|
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
|
||||||
|
const body = await res.json().catch(() => ({ statusCode: res.status, message: res.statusText }));
|
||||||
|
reject(typeof body.error === 'object' ? body.error : body);
|
||||||
}
|
}
|
||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
|
@ -337,6 +337,12 @@ type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']
|
|||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
|
type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminShowUserAccountMoveLogsRequest = operations['admin___show-user-account-move-logs']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminShowUserAccountMoveLogsResponse = operations['admin___show-user-account-move-logs']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
|
type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
@ -1295,6 +1301,8 @@ declare namespace entities {
|
|||||||
AdminServerInfoResponse,
|
AdminServerInfoResponse,
|
||||||
AdminShowModerationLogsRequest,
|
AdminShowModerationLogsRequest,
|
||||||
AdminShowModerationLogsResponse,
|
AdminShowModerationLogsResponse,
|
||||||
|
AdminShowUserAccountMoveLogsRequest,
|
||||||
|
AdminShowUserAccountMoveLogsResponse,
|
||||||
AdminShowUserRequest,
|
AdminShowUserRequest,
|
||||||
AdminShowUserResponse,
|
AdminShowUserResponse,
|
||||||
AdminShowUsersRequest,
|
AdminShowUsersRequest,
|
||||||
@ -1795,6 +1803,7 @@ declare namespace entities {
|
|||||||
User,
|
User,
|
||||||
UserList,
|
UserList,
|
||||||
UserListMembership,
|
UserListMembership,
|
||||||
|
UserAccountMoveLog,
|
||||||
Ad,
|
Ad,
|
||||||
Announcement,
|
Announcement,
|
||||||
App,
|
App,
|
||||||
@ -2774,7 +2783,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
|
|||||||
function parse(acct: string): Acct;
|
function parse(acct: string): Acct;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||||
@ -3073,6 +3082,9 @@ function toString_2(acct: Acct): string;
|
|||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type User = components['schemas']['User'];
|
type User = components['schemas']['User'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type UserAccountMoveLog = components['schemas']['UserAccountMoveLog'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type UserDetailed = components['schemas']['UserDetailed'];
|
type UserDetailed = components['schemas']['UserDetailed'];
|
||||||
|
|
||||||
|
@ -810,6 +810,17 @@ declare module '../api.js' {
|
|||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log*
|
||||||
|
*/
|
||||||
|
request<E extends 'admin/show-user-account-move-logs', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
@ -94,6 +94,8 @@ import type {
|
|||||||
AdminServerInfoResponse,
|
AdminServerInfoResponse,
|
||||||
AdminShowModerationLogsRequest,
|
AdminShowModerationLogsRequest,
|
||||||
AdminShowModerationLogsResponse,
|
AdminShowModerationLogsResponse,
|
||||||
|
AdminShowUserAccountMoveLogsRequest,
|
||||||
|
AdminShowUserAccountMoveLogsResponse,
|
||||||
AdminShowUserRequest,
|
AdminShowUserRequest,
|
||||||
AdminShowUserResponse,
|
AdminShowUserResponse,
|
||||||
AdminShowUsersRequest,
|
AdminShowUsersRequest,
|
||||||
@ -660,6 +662,7 @@ export type Endpoints = {
|
|||||||
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
|
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
|
||||||
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
|
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
|
||||||
'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
|
'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
|
||||||
|
'admin/show-user-account-move-logs': { req: AdminShowUserAccountMoveLogsRequest; res: AdminShowUserAccountMoveLogsResponse };
|
||||||
'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
|
'admin/show-user': { req: AdminShowUserRequest; res: AdminShowUserResponse };
|
||||||
'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
|
'admin/show-users': { req: AdminShowUsersRequest; res: AdminShowUsersResponse };
|
||||||
'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
|
'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse };
|
||||||
|
@ -97,6 +97,8 @@ export type AdminSendEmailRequest = operations['admin___send-email']['requestBod
|
|||||||
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
|
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
|
||||||
export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
|
export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
|
||||||
export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
|
export type AdminShowModerationLogsResponse = operations['admin___show-moderation-logs']['responses']['200']['content']['application/json'];
|
||||||
|
export type AdminShowUserAccountMoveLogsRequest = operations['admin___show-user-account-move-logs']['requestBody']['content']['application/json'];
|
||||||
|
export type AdminShowUserAccountMoveLogsResponse = operations['admin___show-user-account-move-logs']['responses']['200']['content']['application/json'];
|
||||||
export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
|
export type AdminShowUserRequest = operations['admin___show-user']['requestBody']['content']['application/json'];
|
||||||
export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json'];
|
export type AdminShowUserResponse = operations['admin___show-user']['responses']['200']['content']['application/json'];
|
||||||
export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
|
export type AdminShowUsersRequest = operations['admin___show-users']['requestBody']['content']['application/json'];
|
||||||
|
@ -9,6 +9,7 @@ export type UserDetailed = components['schemas']['UserDetailed'];
|
|||||||
export type User = components['schemas']['User'];
|
export type User = components['schemas']['User'];
|
||||||
export type UserList = components['schemas']['UserList'];
|
export type UserList = components['schemas']['UserList'];
|
||||||
export type UserListMembership = components['schemas']['UserListMembership'];
|
export type UserListMembership = components['schemas']['UserListMembership'];
|
||||||
|
export type UserAccountMoveLog = components['schemas']['UserAccountMoveLog'];
|
||||||
export type Ad = components['schemas']['Ad'];
|
export type Ad = components['schemas']['Ad'];
|
||||||
export type Announcement = components['schemas']['Announcement'];
|
export type Announcement = components['schemas']['Announcement'];
|
||||||
export type App = components['schemas']['App'];
|
export type App = components['schemas']['App'];
|
||||||
|
@ -673,6 +673,15 @@ export type paths = {
|
|||||||
*/
|
*/
|
||||||
post: operations['admin___show-moderation-logs'];
|
post: operations['admin___show-moderation-logs'];
|
||||||
};
|
};
|
||||||
|
'/admin/show-user-account-move-logs': {
|
||||||
|
/**
|
||||||
|
* admin/show-user-account-move-logs
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log*
|
||||||
|
*/
|
||||||
|
post: operations['admin___show-user-account-move-logs'];
|
||||||
|
};
|
||||||
'/admin/show-user': {
|
'/admin/show-user': {
|
||||||
/**
|
/**
|
||||||
* admin/show-user
|
* admin/show-user
|
||||||
@ -4126,6 +4135,21 @@ export type components = {
|
|||||||
user: components['schemas']['UserLite'];
|
user: components['schemas']['UserLite'];
|
||||||
withReplies: boolean;
|
withReplies: boolean;
|
||||||
};
|
};
|
||||||
|
UserAccountMoveLog: {
|
||||||
|
/**
|
||||||
|
* Format: id
|
||||||
|
* @example xxxxxxxxxx
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** Format: id */
|
||||||
|
movedToId: string;
|
||||||
|
movedTo: components['schemas']['UserDetailed'];
|
||||||
|
/** Format: id */
|
||||||
|
movedFromId: string;
|
||||||
|
movedFrom: components['schemas']['UserDetailed'];
|
||||||
|
};
|
||||||
Ad: {
|
Ad: {
|
||||||
/**
|
/**
|
||||||
* Format: id
|
* Format: id
|
||||||
@ -6217,6 +6241,8 @@ export type operations = {
|
|||||||
userId: string | null;
|
userId: string | null;
|
||||||
user: components['schemas']['UserLite'] | null;
|
user: components['schemas']['UserLite'] | null;
|
||||||
reads: number;
|
reads: number;
|
||||||
|
/** Format: date-time */
|
||||||
|
lastReadAt: string | null;
|
||||||
})[];
|
})[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -9589,6 +9615,79 @@ export type operations = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* admin/show-user-account-move-logs
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:admin:show-account-move-log*
|
||||||
|
*/
|
||||||
|
'admin___show-user-account-move-logs': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** @default 10 */
|
||||||
|
limit?: number;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
sinceId?: string;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
untilId?: string;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
movedFromId?: string | null;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
movedToId?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: id */
|
||||||
|
id: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** Format: id */
|
||||||
|
movedToId: string;
|
||||||
|
movedTo: components['schemas']['UserDetailed'];
|
||||||
|
/** Format: id */
|
||||||
|
movedFromId: string;
|
||||||
|
movedFrom: components['schemas']['UserDetailed'];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* admin/show-user
|
* admin/show-user
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
@ -57,6 +57,7 @@ export const permissions = [
|
|||||||
'write:admin:send-email',
|
'write:admin:send-email',
|
||||||
'read:admin:server-info',
|
'read:admin:server-info',
|
||||||
'read:admin:show-moderation-log',
|
'read:admin:show-moderation-log',
|
||||||
|
'read:admin:show-account-move-log',
|
||||||
'read:admin:show-user',
|
'read:admin:show-user',
|
||||||
'read:admin:show-users',
|
'read:admin:show-users',
|
||||||
'write:admin:suspend-user',
|
'write:admin:suspend-user',
|
||||||
|
Loading…
Reference in New Issue
Block a user