mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-11-27 06:18:46 +09:00
Merge remote-branch 'upstream/develop'
This commit is contained in:
commit
97c4782014
@ -131,11 +131,20 @@ proxyBypassHosts:
|
||||
|
||||
# Media Proxy
|
||||
# Reference Implementation: https://github.com/misskey-dev/media-proxy
|
||||
# * Deliver a common cache between instances
|
||||
# * Perform image compression (on a different server resource than the main process)
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains.
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
# Movie Thumbnail Generation URL
|
||||
# There is no reference implementation.
|
||||
# For example, Misskey will point to the following URL:
|
||||
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
|
||||
#videoThumbnailGenerator: https://example.com
|
||||
|
||||
# Sign to ActivityPub GET request (default: true)
|
||||
signToActivityPubGet: true
|
||||
|
||||
|
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
@ -0,0 +1 @@
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18
|
11
.devcontainer/devcontainer.json
Normal file
11
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Misskey",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh"
|
||||
}
|
146
.devcontainer/devcontainer.yml
Normal file
146
.devcontainer/devcontainer.yml
Normal file
@ -0,0 +1,146 @@
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Misskey configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
url: http://127.0.0.1:3000/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# URL SETTINGS AFTER THAT!
|
||||
|
||||
# ┌───────────────────────┐
|
||||
#───┘ Port and TLS settings └───────────────────────────────────
|
||||
|
||||
#
|
||||
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||
#
|
||||
# +----- https://example.tld/ ------------+
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# +---------------------------------------+
|
||||
#
|
||||
# You need to set up a reverse proxy. (e.g. nginx)
|
||||
# An encrypted connection with HTTPS is highly recommended
|
||||
# because tokens may be transferred in GET requests.
|
||||
|
||||
# The port that your Misskey server should listen on.
|
||||
port: 3000
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
host: db
|
||||
port: 5432
|
||||
|
||||
# Database name
|
||||
db: misskey
|
||||
|
||||
# Auth
|
||||
user: postgres
|
||||
pass: postgres
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
# You can select the ID generation method.
|
||||
# You don't usually need to change this setting, but you can
|
||||
# change it according to your preferences.
|
||||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
proxyBypassHosts:
|
||||
- api.deepl.com
|
||||
- api-free.deepl.com
|
||||
- www.recaptcha.net
|
||||
- hcaptcha.com
|
||||
- challenges.cloudflare.com
|
||||
|
||||
# Proxy for SMTP/SMTPS
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
# Sign to ActivityPub GET request (default: true)
|
||||
signToActivityPubGet: true
|
||||
|
||||
allowedPrivateNetworks: [
|
||||
'127.0.0.1/32'
|
||||
]
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
53
.devcontainer/docker-compose.yml
Normal file
53
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,53 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../:/workspace:cached
|
||||
|
||||
command: sleep infinity
|
||||
|
||||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
image: redis:7-alpine
|
||||
networks:
|
||||
- internal_network
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: "redis-cli ping"
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
db:
|
||||
restart: unless-stopped
|
||||
image: postgres:15-alpine
|
||||
networks:
|
||||
- internal_network
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: misskey
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
networks:
|
||||
internal_network:
|
||||
internal: true
|
||||
external_network:
|
10
.devcontainer/init.sh
Executable file
10
.devcontainer/init.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
sudo chown -R node /workspace
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
@ -5,6 +5,7 @@ indent_style = tab
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -5,3 +5,4 @@
|
||||
*.glb -diff -text
|
||||
*.blend -diff -text
|
||||
*.afdesign -diff -text
|
||||
* text=auto eol=lf
|
||||
|
8
.github/workflows/docker-develop.yml
vendored
8
.github/workflows/docker-develop.yml
vendored
@ -15,7 +15,10 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.3.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
@ -27,10 +30,13 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ steps.buildx.outputs.platforms }}
|
||||
provenance: false
|
||||
tags: noridev/cherrypick:develop
|
||||
labels: develop
|
||||
cache-from: type=gha
|
||||
|
12
.github/workflows/docker.yml
vendored
12
.github/workflows/docker.yml
vendored
@ -13,6 +13,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.3.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
@ -31,9 +36,14 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ steps.buildx.outputs.platforms }}
|
||||
provenance: false
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -33,6 +33,7 @@ coverage
|
||||
!/.config/docker_example.yml
|
||||
!/.config/docker_example.env
|
||||
docker-compose.yml
|
||||
!/.devcontainer/docker-compose.yml
|
||||
|
||||
# cherrypick
|
||||
/build
|
||||
|
38
CHANGELOG.md
38
CHANGELOG.md
@ -8,6 +8,44 @@
|
||||
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- Server: URLプレビュー(summaly)はプロキシを通すように
|
||||
|
||||
### Bugfixes
|
||||
-
|
||||
|
||||
## 13.6.1 (2023/02/12)
|
||||
|
||||
### Improvements
|
||||
- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化
|
||||
- Backend: activitypub情報がcorsでブロックされないようヘッダーを追加
|
||||
- enhance: レートリミットを0%にできるように
|
||||
- チャンネル内Renoteを行えるように
|
||||
|
||||
### Bugfixes
|
||||
- Client: ユーザーページでアクティビティを見ることができない問題を修正
|
||||
|
||||
## 13.6.0 (2023/02/11)
|
||||
|
||||
### Improvements
|
||||
- MkPageHeaderをごっそり変えた
|
||||
* モバイルではヘッダーは上下に分割され、下段にタブが表示されるように
|
||||
* iconOnlyのタブ項目がアクティブな場合にはタブのタイトルを表示するように
|
||||
* メインタイムラインではタイトルを表示しない
|
||||
* メインタイムラインかつモバイルで表示される左上のアバターを選択するとアカウントメニューが開くように
|
||||
- ユーザーページのノート一覧をタブとして分離
|
||||
- コンディショナルロールもバッジとして表示可能に
|
||||
- enhance(client): ロールをより簡単に付与できるように
|
||||
- enhance(client): 一度見たノートのRenoteは省略して表示するように
|
||||
- enhance(client): 迷惑になる可能性のある投稿を行う前に警告を表示
|
||||
- リアクションの数が多い場合の表示を改善
|
||||
- 一部のMFM構文をopt-outに
|
||||
|
||||
### Bugfixes
|
||||
- Client: ユーザーページでタブがほとんど見れないことがないように
|
||||
|
||||
## 13.5.6 (2023/02/10)
|
||||
|
||||
### Improvements
|
||||
|
@ -111,6 +111,26 @@ command.
|
||||
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
|
||||
- Service Worker is watched by esbuild.
|
||||
|
||||
### Dev Container
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||
**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled.
|
||||
|
||||
It will run the following command automatically inside the container.
|
||||
``` bash
|
||||
git submodule update --init
|
||||
pnpm install --frozen-lockfile
|
||||
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||
pnpm build
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
After finishing the migration, run the `pnpm dev` command to start the development server.
|
||||
|
||||
``` bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
||||
|
||||
|
14
Dockerfile
14
Dockerfile
@ -1,3 +1,5 @@
|
||||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=18.13.0-bullseye
|
||||
|
||||
FROM node:${NODE_VERSION} AS builder
|
||||
@ -14,16 +16,16 @@ RUN corepack enable
|
||||
|
||||
WORKDIR /misskey
|
||||
|
||||
COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
|
||||
COPY ["scripts", "./scripts"]
|
||||
COPY ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY ["packages/frontend/package.json", "./packages/frontend/"]
|
||||
COPY ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
|
||||
COPY --link ["scripts", "./scripts"]
|
||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
pnpm i --frozen-lockfile --aggregate-output
|
||||
|
||||
COPY . ./
|
||||
COPY --link . ./
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: cherrypick
|
||||
version: 0.0.0
|
||||
description: This chart is created for the purpose of previewing Pull Requests. Do not use this for production use.
|
||||
|
@ -886,56 +886,6 @@ _nsfw:
|
||||
respect: "اخف الوسائط ذات المحتوى الحساس"
|
||||
ignore: "اعرض الوسائط ذات المحتوى الحساس"
|
||||
force: "اخف كل الوسائط"
|
||||
_mfm:
|
||||
cheatSheet: "مرجع ملخص عن MFM"
|
||||
intro: "MFM هي لغة ترميزية مخصصة يمكن استخدامها في عدّة أماكن في ميسكي. يمكنك مراجعة كل تعابيرها مع كيفية استخدامها هنا."
|
||||
mention: "أشر الى"
|
||||
mentionDescription: "يمكنك الإشارة لمستخدم معيّن من خلال كتابة @ متبوعة باسم مستخدم."
|
||||
hashtag: "الوسوم"
|
||||
hashtagDescription: "يمكنك تعيين وسم من خلال كتابة # متبوعة بالنص المطلوب."
|
||||
url: "الرابط"
|
||||
urlDescription: "يمكن عرض الروابط"
|
||||
link: "رابط"
|
||||
bold: "عريض"
|
||||
boldDescription: "جعل الحروف أثخن لإبرازها."
|
||||
small: "صغير"
|
||||
smallDescription: "يعرض المحتوى صغيرًا ورفيعًا."
|
||||
center: "وسط"
|
||||
centerDescription: "يمركز المحتوى في الوَسَط."
|
||||
quote: "اقتبس"
|
||||
quoteDescription: "يعرض المحتوى كاقتباس"
|
||||
emoji: "إيموجي مخصص"
|
||||
emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي."
|
||||
search: "البحث"
|
||||
searchDescription: "يعرض نصًا في صندوق البحث"
|
||||
flip: "اقلب"
|
||||
flipDescription: "يقلب المحتوى عموديًا أو أفقيًا"
|
||||
jelly: "تأثير (هلام)"
|
||||
jellyDescription: "يمنح المحتوى حركة هلامية."
|
||||
tada: "تأثير (تادا)"
|
||||
tadaDescription: "يمنح للمحتوى تأثير تادا"
|
||||
jump: "تأثير (قفز)"
|
||||
jumpDescription: "يمنح للمحتوى حركة قفز."
|
||||
bounce: "تأثير (ارتداد)"
|
||||
bounceDescription: "يمنح للمحتوى حركة ارتدادية"
|
||||
shake: "تأثير (اهتزاز)"
|
||||
shakeDescription: "يمنح المحتوى حركة اهتزازية."
|
||||
spin: "تأثير (دوران)"
|
||||
spinDescription: "يمنح المحتوى حركة دورانية."
|
||||
x2: "كبير"
|
||||
x2Description: "يُكبر المحتوى"
|
||||
x3: "كبير جداً"
|
||||
x3Description: "يُضخم المحتوى"
|
||||
x4: "هائل"
|
||||
x4Description: "يُضخم المحتوى أكثر مما سبق."
|
||||
blur: "طمس"
|
||||
blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح."
|
||||
font: "الخط"
|
||||
fontDescription: "الخط المستخدم لعرض المحتوى."
|
||||
rainbow: "قوس قزح"
|
||||
rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
|
||||
rotate: "تدوير"
|
||||
rotateDescription: "يُدير المحتوى بزاوية معيّنة."
|
||||
_instanceTicker:
|
||||
none: "لا تظهره بتاتًا"
|
||||
remote: "أظهر للمستخدمين البِعاد"
|
||||
|
@ -455,7 +455,6 @@ youHaveNoGroups: "আপনার কোন গ্রুপ নেই "
|
||||
joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷"
|
||||
noHistory: "কোনো ইতিহাস নেই"
|
||||
signinHistory: "প্রবেশ করার ইতিহাস"
|
||||
disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন"
|
||||
doing: "প্রক্রিয়া করছে..."
|
||||
category: "বিভাগ"
|
||||
tags: "ট্যাগসমূহ"
|
||||
|
@ -375,11 +375,6 @@ file: "Fitxers"
|
||||
_email:
|
||||
_follow:
|
||||
title: "t'ha seguit"
|
||||
_mfm:
|
||||
mention: "Menció"
|
||||
quote: "Citar"
|
||||
emoji: "Emojis personalitzats"
|
||||
search: "Cercar"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat."
|
||||
_theme:
|
||||
|
@ -642,19 +642,6 @@ _registry:
|
||||
_aboutMisskey:
|
||||
allContributors: "Všichni přispěvatelé"
|
||||
source: "Zdrojový kód"
|
||||
_mfm:
|
||||
mention: "Zmínění"
|
||||
hashtag: "Hashtag"
|
||||
link: "Odkaz"
|
||||
bold: "Tučně"
|
||||
quote: "Citovat"
|
||||
emoji: "Vlastní emoji"
|
||||
search: "Vyhledávání"
|
||||
flip: "Otočit"
|
||||
tada: "Animace (tadá)"
|
||||
blur: "Rozmazání"
|
||||
font: "Font"
|
||||
rainbow: "Duha"
|
||||
_channel:
|
||||
featured: "Trendy"
|
||||
_menuDisplay:
|
||||
|
@ -467,7 +467,6 @@ youHaveNoGroups: "Keine Gruppen vorhanden"
|
||||
joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene."
|
||||
noHistory: "Kein Verlauf gefunden"
|
||||
signinHistory: "Anmeldungsverlauf"
|
||||
disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren"
|
||||
doing: "In Bearbeitung …"
|
||||
category: "Kategorie"
|
||||
tags: "Schlagwörter"
|
||||
|
@ -298,11 +298,6 @@ cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω
|
||||
_email:
|
||||
_follow:
|
||||
title: "Έχετε ένα νέο ακόλουθο"
|
||||
_mfm:
|
||||
mention: "Επισήμανση"
|
||||
quote: "Παράθεση"
|
||||
emoji: "Επιπλέον emoji"
|
||||
search: "Αναζήτηση"
|
||||
_channel:
|
||||
featured: "Δημοφιλή"
|
||||
_theme:
|
||||
|
@ -480,7 +480,8 @@ youHaveNoGroups: "You have no groups"
|
||||
joinOrCreateGroup: "Get invited to a group or create your own."
|
||||
noHistory: "No history available"
|
||||
signinHistory: "Login history"
|
||||
disableAnimatedMfm: "Disable MFM with animation"
|
||||
enableAdvancedMfm: "Enable advanced MFM"
|
||||
enableAnimatedMfm: "Enable MFM with animation"
|
||||
doing: "Processing..."
|
||||
category: "Category"
|
||||
tags: "Tags"
|
||||
@ -959,6 +960,10 @@ selectFromPresets: "Choose from presets"
|
||||
achievements: "Achievements"
|
||||
gotInvalidResponseError: "Invalid server response"
|
||||
gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later."
|
||||
thisPostMayBeAnnoying: "This note may annoy others."
|
||||
thisPostMayBeAnnoyingHome: "Post to home timeline"
|
||||
thisPostMayBeAnnoyingCancel: "Cancel"
|
||||
thisPostMayBeAnnoyingIgnore: "Post anyway"
|
||||
_achievements:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
|
@ -467,7 +467,6 @@ youHaveNoGroups: "Sin grupos"
|
||||
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo."
|
||||
noHistory: "No hay datos en el historial"
|
||||
signinHistory: "Historial de ingresos"
|
||||
disableAnimatedMfm: "Deshabilitar MFM que tiene animaciones"
|
||||
doing: "Voy en camino"
|
||||
category: "Categoría"
|
||||
tags: "Etiqueta"
|
||||
|
@ -464,7 +464,6 @@ youHaveNoGroups: "Vous n’avez aucun groupe"
|
||||
joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou créer votre propre nouveau groupe."
|
||||
noHistory: "Pas d'historique"
|
||||
signinHistory: "Historique de connexion"
|
||||
disableAnimatedMfm: "Désactiver MFM ayant des animations"
|
||||
doing: "En cours..."
|
||||
category: "Catégorie"
|
||||
tags: "Étiquettes"
|
||||
|
@ -464,7 +464,6 @@ youHaveNoGroups: "Kamu tidak memiliki grup"
|
||||
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
|
||||
noHistory: "Tidak ada riwayat"
|
||||
signinHistory: "Riwayat masuk"
|
||||
disableAnimatedMfm: "Nonaktifkan MFM dengan animasi"
|
||||
doing: "Sedang berkerja..."
|
||||
category: "Kategori"
|
||||
tags: "Tandai"
|
||||
|
@ -464,7 +464,6 @@ youHaveNoGroups: "Nessun gruppo"
|
||||
joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono."
|
||||
noHistory: "Nessuna cronologia"
|
||||
signinHistory: "Storico degli accessi al profilo"
|
||||
disableAnimatedMfm: "Disabilità i MFM animati"
|
||||
doing: "In corso..."
|
||||
category: "Categoria"
|
||||
tags: "Tag"
|
||||
|
@ -116,6 +116,8 @@ renoted: "Renoteしました。"
|
||||
cantRenote: "この投稿はRenoteできません。"
|
||||
cantReRenote: "RenoteをRenoteすることはできません。"
|
||||
quote: "引用"
|
||||
inChannelRenote: "チャンネル内Renote"
|
||||
inChannelQuote: "チャンネル内引用"
|
||||
pinnedNote: "ピン留めされたノート"
|
||||
pinned: "ピン留め"
|
||||
you: "あなた"
|
||||
@ -480,7 +482,8 @@ youHaveNoGroups: "グループがありません"
|
||||
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
|
||||
noHistory: "履歴はありません"
|
||||
signinHistory: "ログイン履歴"
|
||||
disableAnimatedMfm: "動きのあるMFMを無効にする"
|
||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||
enableAnimatedMfm: "動きのあるMFMを有効にする"
|
||||
doing: "やっています"
|
||||
category: "カテゴリ"
|
||||
tags: "タグ"
|
||||
@ -959,6 +962,14 @@ selectFromPresets: "プリセットから選択"
|
||||
achievements: "実績"
|
||||
gotInvalidResponseError: "サーバーの応答が無効です"
|
||||
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
|
||||
thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります。"
|
||||
thisPostMayBeAnnoyingHome: "ホームに投稿"
|
||||
thisPostMayBeAnnoyingCancel: "やめる"
|
||||
thisPostMayBeAnnoyingIgnore: "このまま投稿"
|
||||
collapseRenotes: "見たことのあるRenoteを省略して表示"
|
||||
internalServerError: "サーバー内部エラー"
|
||||
internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。"
|
||||
copyErrorInfo: "エラー情報をコピー"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
|
@ -467,7 +467,8 @@ youHaveNoGroups: "グループがあらへんねぇ。"
|
||||
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループ作ってからやってな"
|
||||
noHistory: "履歴はあらへんねぇ。"
|
||||
signinHistory: "ログイン履歴"
|
||||
disableAnimatedMfm: "動きがやかましいMFMを止める"
|
||||
enableAdvancedMfm: "ややこしいMFMもありにする"
|
||||
enableAnimatedMfm: "動きがやかましいMFMも許したる"
|
||||
doing: "やっとるがな"
|
||||
category: "カテゴリ"
|
||||
tags: "タグ"
|
||||
@ -946,6 +947,8 @@ selectFromPresets: "プリセットから選ぶ"
|
||||
achievements: "実績"
|
||||
gotInvalidResponseError: "サーバー黙っとるわ、知らんけど"
|
||||
gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。"
|
||||
thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。"
|
||||
collapseRenotes: "見たことあるRenoteは省略やで"
|
||||
_achievements:
|
||||
earnedAt: "貰った日ぃ"
|
||||
_types:
|
||||
|
@ -61,10 +61,6 @@ account: "Imiḍan"
|
||||
_email:
|
||||
_follow:
|
||||
title: "Yeṭṭafaṛ-ik·em-id"
|
||||
_mfm:
|
||||
mention: "Bder"
|
||||
search: "Nadi"
|
||||
font: "Tasefsit"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "Bder"
|
||||
|
@ -64,8 +64,6 @@ file: "ಕಡತಗಳು"
|
||||
_email:
|
||||
_follow:
|
||||
title: "ಹಿಂಬಾಲಿಸಿದರು"
|
||||
_mfm:
|
||||
search: "ಹುಡುಕು"
|
||||
_sfx:
|
||||
notification: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
_widgets:
|
||||
|
@ -142,6 +142,7 @@ unblockConfirm: "이 계정의 차단을 해제할까요? 상대방이 나를
|
||||
suspendConfirm: "이 계정을 정지할까요?"
|
||||
unsuspendConfirm: "이 계정의 정지를 해제할까요?"
|
||||
selectList: "리스트 선택"
|
||||
selectChannel: "채널 선택"
|
||||
selectAntenna: "안테나 선택"
|
||||
selectWidget: "위젯 선택"
|
||||
editWidgets: "위젯 편집"
|
||||
@ -269,6 +270,8 @@ noMoreHistory: "타임머신이 더 이상은 돌아갈 수 없대요!"
|
||||
startMessaging: "대화 시작하기"
|
||||
nUsersRead: "{n}명이 읽음"
|
||||
agreeTo: "{0}에 동의"
|
||||
agreeBelow: "아래 내용에 동의합니다"
|
||||
basicNotesBeforeCreateAccount: "기본적인 주의사항"
|
||||
tos: "이용 약관"
|
||||
start: "시작하기"
|
||||
home: "홈"
|
||||
@ -477,8 +480,9 @@ youHaveNoGroups: "그룹이 없어요"
|
||||
joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들 수 있어요!"
|
||||
noHistory: "기록이 없어요"
|
||||
signinHistory: "로그인 기록"
|
||||
disableAnimatedMfm: "움직임이 있는 MFM 비활성화"
|
||||
doing: "잠시만요.."
|
||||
enableAdvancedMfm: "고급 MFM을 활성화"
|
||||
enableAnimatedMfm: "움직임이 있는 MFM을 활성화"
|
||||
doing: "잠시만 기다려 주세요"
|
||||
category: "카테고리"
|
||||
tags: "태그"
|
||||
docSource: "이 문서의 소스"
|
||||
@ -875,6 +879,8 @@ failedToFetchAccountInformation: "계정 정보를 가져오지 못했어요"
|
||||
rateLimitExceeded: "요청 제한 횟수를 초과했어요! 나중에 다시 시도해 주세요."
|
||||
cropImage: "이미지 자르기"
|
||||
cropImageAsk: "이미지를 자르시겠어요?"
|
||||
cropYes: "잘라내기"
|
||||
cropNo: "그대로 사용"
|
||||
file: "파일"
|
||||
recentNHours: "최근 {n}시간"
|
||||
recentNDays: "최근 {n}일"
|
||||
@ -953,6 +959,12 @@ cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시
|
||||
preset: "프리셋"
|
||||
selectFromPresets: "프리셋에서 선택"
|
||||
achievements: "도전 과제"
|
||||
gotInvalidResponseError: "서버의 응답이 올바르지 않아요.."
|
||||
gotInvalidResponseErrorDescription: "서버가 다운되었거나 점검 중일 가능성이 있어요. 잠시 후에 다시 접속해 주세요."
|
||||
thisPostMayBeAnnoying: "이 게시물은 다른 유저에게 피해를 줄 가능성이 있어요!"
|
||||
thisPostMayBeAnnoyingHome: "홈에 게시"
|
||||
thisPostMayBeAnnoyingCancel: "그만두기"
|
||||
thisPostMayBeAnnoyingIgnore: "이대로 게시"
|
||||
_achievements:
|
||||
earnedAt: "달성 일시"
|
||||
_types:
|
||||
@ -1209,6 +1221,9 @@ _role:
|
||||
baseRole: "기본 역할"
|
||||
useBaseValue: "기본값 사용"
|
||||
chooseRoleToAssign: "할당할 역할 선택"
|
||||
iconUrl: "아이콘 URL"
|
||||
asBadge: "배지로 표시"
|
||||
descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시돼요."
|
||||
canEditMembersByModerator: "모더레이터의 역할 수정 허용"
|
||||
descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있어요. 꺼져 있으면 관리자만 사용자를 할당할 수 있어요."
|
||||
priority: "우선순위"
|
||||
@ -1618,12 +1633,15 @@ _permissions:
|
||||
"read:gallery-likes": "갤러리의 좋아요를 확인합니다"
|
||||
"write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다"
|
||||
_auth:
|
||||
shareAccessTitle: "애플리케이션 접근 허가"
|
||||
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?"
|
||||
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?"
|
||||
permissionAsk: "이 앱은 다음 권한을 요청해요"
|
||||
permission: "{name}에서 다음 권한을 요청했어요"
|
||||
permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다"
|
||||
pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요"
|
||||
callback: "앱으로 돌아갈게요!"
|
||||
denied: "앗, 접근이 거부되었어요!"
|
||||
pleaseLogin: "애플리케이션의 접근을 허가하려면 먼저 로그인해 주세요."
|
||||
_antennaSources:
|
||||
all: "모든 노트"
|
||||
homeTimeline: "팔로우중인 유저의 노트"
|
||||
|
@ -187,11 +187,6 @@ file: "ໄຟລ໌"
|
||||
_email:
|
||||
_follow:
|
||||
title: "ໄດ້ຕິດຕາມທ່ານ"
|
||||
_mfm:
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
|
||||
emoji: "ອີໂມຈິແບບກຳນົດເອງ"
|
||||
search: "ຄົ້ນຫາ"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "ໄດ້ກ່າວມາ"
|
||||
|
@ -427,11 +427,6 @@ loggedInAsBot: "Momenteel als bot ingelogd"
|
||||
_email:
|
||||
_follow:
|
||||
title: "volgde jou"
|
||||
_mfm:
|
||||
mention: "Vermelding"
|
||||
quote: "Quote"
|
||||
emoji: "Maatwerk emoji"
|
||||
search: "Zoeken"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "Vermelding"
|
||||
|
@ -461,7 +461,6 @@ youHaveNoGroups: "Nie masz żadnych grup"
|
||||
joinOrCreateGroup: "Uzyskaj zaproszenie do dołączenia do grupy lub utwórz własną grupę."
|
||||
noHistory: "Brak historii"
|
||||
signinHistory: "Historia logowania"
|
||||
disableAnimatedMfm: "Wyłącz MFM z animacją"
|
||||
doing: "Przetwarzanie..."
|
||||
category: "Kategoria"
|
||||
tags: "Tagi"
|
||||
|
@ -475,11 +475,6 @@ file: "Ficheiros"
|
||||
_email:
|
||||
_follow:
|
||||
title: "Você tem um novo seguidor"
|
||||
_mfm:
|
||||
mention: "Menção"
|
||||
quote: "Citar"
|
||||
emoji: "Emoji personalizado"
|
||||
search: "Buscar"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "Menção"
|
||||
|
@ -455,7 +455,6 @@ youHaveNoGroups: "Nu ai niciun grup"
|
||||
joinOrCreateGroup: "Primește o invitație într-un grup sau creează unul nou."
|
||||
noHistory: "Nu există istoric"
|
||||
signinHistory: "Istoric autentificări"
|
||||
disableAnimatedMfm: "Dezactivează MFM cu animații"
|
||||
doing: "Se procesează..."
|
||||
category: "Categorie"
|
||||
tags: "Etichete"
|
||||
@ -655,11 +654,6 @@ _role:
|
||||
_email:
|
||||
_follow:
|
||||
title: "te-a urmărit"
|
||||
_mfm:
|
||||
mention: "Mențiune"
|
||||
quote: "Citează"
|
||||
emoji: "Emoji personalizat"
|
||||
search: "Caută"
|
||||
_theme:
|
||||
description: "Descriere"
|
||||
keys:
|
||||
|
@ -462,7 +462,6 @@ youHaveNoGroups: "У вас нет ни одной группы"
|
||||
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
|
||||
noHistory: "История пока пуста"
|
||||
signinHistory: "Журнал посещений"
|
||||
disableAnimatedMfm: "Отключение анимированной разметки MFM"
|
||||
doing: "В процессе"
|
||||
category: "Категория"
|
||||
tags: "Метки"
|
||||
|
@ -464,7 +464,6 @@ youHaveNoGroups: "Nemáte žiadne skupiny"
|
||||
joinOrCreateGroup: "Požiadajte o pozvanie do existujúcej skupiny alebo vytvorte novú."
|
||||
noHistory: "Žiadna história"
|
||||
signinHistory: "História prihlásení"
|
||||
disableAnimatedMfm: "Vypnúť MFM s animáciou"
|
||||
doing: "Pracujem..."
|
||||
category: "Kategórie"
|
||||
tags: "Značky"
|
||||
|
@ -371,11 +371,6 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för
|
||||
_email:
|
||||
_follow:
|
||||
title: "följde dig"
|
||||
_mfm:
|
||||
mention: "Nämn"
|
||||
quote: "Citat"
|
||||
emoji: "Anpassa emoji"
|
||||
search: "Sök"
|
||||
_channel:
|
||||
setBanner: "Välj banner"
|
||||
removeBanner: "Ta bort banner"
|
||||
|
@ -465,7 +465,6 @@ youHaveNoGroups: "คุณยังไม่มีกลุ่ม"
|
||||
joinOrCreateGroup: "รับเชิญเข้าร่วมกลุ่มหรือสร้างกลุ่มของคุณเองเลยนะ"
|
||||
noHistory: "ไม่มีรายการ"
|
||||
signinHistory: "ประวัติการเข้าสู่ระบบ"
|
||||
disableAnimatedMfm: "ปิดการใช้งาน MFM ด้วยแอนิเมชั่น"
|
||||
doing: "กำลังประมวลผล......"
|
||||
category: "หมวดหมู่"
|
||||
tags: "แท็ก"
|
||||
|
@ -48,8 +48,6 @@ smtpUser: "Kullanıcı Adı"
|
||||
smtpPass: "Şifre"
|
||||
user: "Kullanıcı"
|
||||
searchByGoogle: "Arama"
|
||||
_mfm:
|
||||
search: "Arama"
|
||||
_sfx:
|
||||
notification: "Bildirim"
|
||||
_widgets:
|
||||
|
@ -2,5 +2,3 @@
|
||||
_lang_: "ياپونچە"
|
||||
search: "ئىزدەش"
|
||||
searchByGoogle: "ئىزدەش"
|
||||
_mfm:
|
||||
search: "ئىزدەش"
|
||||
|
@ -166,7 +166,7 @@ recipient: "Отримувач"
|
||||
annotation: "Коментарі"
|
||||
federation: "Федіверс"
|
||||
instances: "Інстанс"
|
||||
registeredAt: "Приєднався(лась)"
|
||||
registeredAt: "Реєстрація"
|
||||
latestRequestReceivedAt: "Останній запит прийнято"
|
||||
latestStatus: "Останній статус"
|
||||
storageUsage: "Використання простору"
|
||||
@ -263,7 +263,7 @@ activity: "Активність"
|
||||
images: "Зображення"
|
||||
birthday: "День народження"
|
||||
yearsOld: "{age} років"
|
||||
registeredDate: "Приєднався(лась)"
|
||||
registeredDate: "Приєднання"
|
||||
location: "Локація"
|
||||
theme: "Тема"
|
||||
themeForLightMode: "Світла тема"
|
||||
@ -461,7 +461,6 @@ youHaveNoGroups: "Немає груп"
|
||||
joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи."
|
||||
noHistory: "Історія порожня"
|
||||
signinHistory: "Історія входів"
|
||||
disableAnimatedMfm: "Відключити анімації MFM"
|
||||
doing: "Виконується"
|
||||
category: "Категорія"
|
||||
tags: "Теги"
|
||||
@ -1087,6 +1086,9 @@ _achievements:
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "Вивести \"hello world\" у Скретчпаді"
|
||||
_reactWithoutRead:
|
||||
title: "Прочитали як слід?"
|
||||
description: "Реакція на нотатку, що містить понад 100 символів, протягом 3 секунд після її публікації"
|
||||
_clickedClickHere:
|
||||
title: "Натисніть тут"
|
||||
description: "Натиснуто тут"
|
||||
|
@ -457,7 +457,6 @@ youHaveNoGroups: "Không có nhóm nào"
|
||||
joinOrCreateGroup: "Tham gia hoặc tạo một nhóm mới."
|
||||
noHistory: "Không có dữ liệu"
|
||||
signinHistory: "Lịch sử đăng nhập"
|
||||
disableAnimatedMfm: "Tắt MFM với chuyển động"
|
||||
doing: "Đang xử lý..."
|
||||
category: "Phân loại"
|
||||
tags: "Thẻ"
|
||||
|
@ -467,7 +467,8 @@ youHaveNoGroups: "没有群组"
|
||||
joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。"
|
||||
noHistory: "没有历史记录"
|
||||
signinHistory: "登录历史"
|
||||
disableAnimatedMfm: "禁用MFM动画"
|
||||
enableAdvancedMfm: "启用扩展MFM"
|
||||
enableAnimatedMfm: "启用MFM动画"
|
||||
doing: "正在进行"
|
||||
category: "类别"
|
||||
tags: "标签"
|
||||
@ -946,6 +947,10 @@ selectFromPresets: "從預設值中選擇"
|
||||
achievements: "成就"
|
||||
gotInvalidResponseError: "服务器无应答"
|
||||
gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。"
|
||||
thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。"
|
||||
thisPostMayBeAnnoyingHome: "发到首页"
|
||||
thisPostMayBeAnnoyingCancel: "取消"
|
||||
thisPostMayBeAnnoyingIgnore: "就这样发布"
|
||||
_achievements:
|
||||
earnedAt: "达成时间"
|
||||
_types:
|
||||
|
@ -467,7 +467,8 @@ youHaveNoGroups: "找不到群組"
|
||||
joinOrCreateGroup: "請加入現有群組,或創建新群組。"
|
||||
noHistory: "沒有歷史紀錄"
|
||||
signinHistory: "登入歷史"
|
||||
disableAnimatedMfm: "禁用MFM動畫"
|
||||
enableAdvancedMfm: "啟用高級MFM"
|
||||
enableAnimatedMfm: "啟用MFM動畫"
|
||||
doing: "正在進行"
|
||||
category: "類別"
|
||||
tags: "標籤"
|
||||
@ -946,6 +947,11 @@ selectFromPresets: "從預設值中選擇"
|
||||
achievements: "成就"
|
||||
gotInvalidResponseError: "伺服器的回應無效"
|
||||
gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。"
|
||||
thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。"
|
||||
thisPostMayBeAnnoyingHome: "發布到首頁"
|
||||
thisPostMayBeAnnoyingCancel: "退出"
|
||||
thisPostMayBeAnnoyingIgnore: "直接發布貼文"
|
||||
collapseRenotes: "省略顯示已看過的轉發貼文"
|
||||
_achievements:
|
||||
earnedAt: "獲得日期"
|
||||
_types:
|
||||
|
@ -1,14 +0,0 @@
|
||||
// https://github.com/facebook/jest/issues/12270#issuecomment-1194746382
|
||||
|
||||
const nativeModule = require('node:module');
|
||||
|
||||
function resolver(module, options) {
|
||||
const { basedir, defaultResolver } = options;
|
||||
try {
|
||||
return defaultResolver(module, options);
|
||||
} catch (error) {
|
||||
return nativeModule.createRequire(basedir).resolve(module);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolver;
|
@ -83,7 +83,14 @@ module.exports = {
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
"^@/(.*?).js": "<rootDir>/src/$1.ts",
|
||||
// Do not resolve .wasm.js to .wasm by the rule below
|
||||
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
|
||||
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
|
||||
// converts it again to `../../src/foo/bar` which then can be resolved to
|
||||
// `.ts` files.
|
||||
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
|
||||
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
|
||||
// directly import `.ts` files without this hack.
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
|
||||
@ -112,7 +119,7 @@ module.exports = {
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
resolver: './jest-resolver.cjs',
|
||||
// resolver: './jest-resolver.cjs',
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
restoreMocks: true,
|
||||
|
@ -109,14 +109,14 @@
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.17.8",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.12",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.5",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
|
@ -67,6 +67,7 @@ export type Source = {
|
||||
|
||||
mediaProxy?: string;
|
||||
proxyRemoteFiles?: boolean;
|
||||
videoThumbnailGenerator?: string;
|
||||
|
||||
signToActivityPubGet?: boolean;
|
||||
};
|
||||
@ -89,6 +90,7 @@ export type Mixin = {
|
||||
clientManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
};
|
||||
|
||||
export type Config = Source & Mixin;
|
||||
@ -144,6 +146,10 @@ export function loadConfig() {
|
||||
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy;
|
||||
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
|
||||
|
||||
mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ?
|
||||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||
: null;
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
|
@ -32,7 +32,7 @@ export class AccountUpdateService {
|
||||
|
||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
this.relayService.deliverToRelays(user, content);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import Logger from '@/logger.js';
|
||||
import type { IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@ -250,6 +250,14 @@ export class DriveService {
|
||||
@bindThis
|
||||
public async generateAlts(path: string, type: string, generateWeb: boolean) {
|
||||
if (type.startsWith('video/')) {
|
||||
if (this.config.videoThumbnailGenerator != null) {
|
||||
// videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ
|
||||
return {
|
||||
webpublic: null,
|
||||
thumbnail: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
|
||||
return {
|
||||
@ -391,7 +399,7 @@ export class DriveService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteOldFile(user: IRemoteUser) {
|
||||
private async deleteOldFile(user: RemoteUser) {
|
||||
const q = this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.andWhere('file.isLink = FALSE');
|
||||
@ -492,7 +500,7 @@ export class DriveService {
|
||||
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
|
||||
} else {
|
||||
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
||||
this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as IRemoteUser);
|
||||
this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,6 @@ export class HttpRequestService {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
@ -114,7 +113,6 @@ export class HttpRequestService {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
@ -144,7 +142,10 @@ export class HttpRequestService {
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: args.method ?? 'GET',
|
||||
headers: args.headers,
|
||||
headers: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
...(args.headers ?? {})
|
||||
},
|
||||
body: args.body,
|
||||
size: args.size ?? 10 * 1024 * 1024,
|
||||
agent: (url) => this.getAgentByUrl(url),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
||||
|
||||
@Injectable()
|
||||
export class InstanceActorService {
|
||||
private cache: Cache<ILocalUser>;
|
||||
private cache: Cache<LocalUser>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@ -19,24 +19,24 @@ export class InstanceActorService {
|
||||
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
) {
|
||||
this.cache = new Cache<ILocalUser>(Infinity);
|
||||
this.cache = new Cache<LocalUser>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getInstanceActor(): Promise<ILocalUser> {
|
||||
public async getInstanceActor(): Promise<LocalUser> {
|
||||
const cached = this.cache.get(null);
|
||||
if (cached) return cached;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
username: ACTOR_USERNAME,
|
||||
}) as ILocalUser | undefined;
|
||||
}) as LocalUser | undefined;
|
||||
|
||||
if (user) {
|
||||
this.cache.set(null, user);
|
||||
return user;
|
||||
} else {
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as ILocalUser;
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
||||
this.cache.set(null, created);
|
||||
return created;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import type { Config } from '@/config.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { User, CacheableUser, IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { User, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
@ -48,7 +48,7 @@ export class MessagingService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
|
||||
public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
|
||||
const message = {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
@ -135,7 +135,7 @@ export class MessagingService {
|
||||
}))),
|
||||
} as Note;
|
||||
|
||||
const activity = this.apRendererService.renderActivity(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
|
||||
const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
|
||||
|
||||
this.queueService.deliver(user, activity, recipientUser.inbox);
|
||||
}
|
||||
@ -158,7 +158,7 @@ export class MessagingService {
|
||||
if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
|
||||
|
||||
if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) {
|
||||
const activity = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user));
|
||||
const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user));
|
||||
this.queueService.deliver(user, activity, recipient.inbox);
|
||||
}
|
||||
} else if (message.groupId) {
|
||||
@ -291,16 +291,16 @@ export class MessagingService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
|
||||
public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: RemoteUser, messages: MessagingMessage | MessagingMessage[]) {
|
||||
messages = toArray(messages).filter(x => x.uri);
|
||||
const contents = messages.map(x => this.apRendererService.renderRead(user, x));
|
||||
|
||||
if (contents.length > 1) {
|
||||
const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents);
|
||||
this.queueService.deliver(user, this.apRendererService.renderActivity(collection), recipient.inbox);
|
||||
this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox);
|
||||
} else {
|
||||
for (const content of contents) {
|
||||
this.queueService.deliver(user, this.apRendererService.renderActivity(content), recipient.inbox);
|
||||
this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { App } from '@/models/entities/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { IPoll } from '@/models/entities/Poll.js';
|
||||
import { Poll } from '@/models/entities/Poll.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
@ -52,7 +52,7 @@ class NotificationManager {
|
||||
private notifier: { id: User['id']; };
|
||||
private note: Note;
|
||||
private queue: {
|
||||
target: ILocalUser['id'];
|
||||
target: LocalUser['id'];
|
||||
reason: NotificationType;
|
||||
}[];
|
||||
|
||||
@ -68,7 +68,7 @@ class NotificationManager {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public push(notifiee: ILocalUser['id'], reason: NotificationType) {
|
||||
public push(notifiee: LocalUser['id'], reason: NotificationType) {
|
||||
// 自分自身へは通知しない
|
||||
if (this.notifier.id === notifiee) return;
|
||||
|
||||
@ -608,7 +608,7 @@ export class NoteCreateService {
|
||||
|
||||
// メンションされたリモートユーザーに配送
|
||||
for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
dm.addDirectRecipe(u as RemoteUser);
|
||||
}
|
||||
|
||||
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
||||
@ -714,7 +714,7 @@ export class NoteCreateService {
|
||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
||||
|
||||
return this.apRendererService.renderActivity(content);
|
||||
return this.apRendererService.addContext(content);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Brackets, In } from 'typeorm';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js';
|
||||
import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
@ -78,7 +78,7 @@ export class NoteDeleteService {
|
||||
});
|
||||
}
|
||||
|
||||
const content = this.apRendererService.renderActivity(renote
|
||||
const content = this.apRendererService.addContext(renote
|
||||
? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user)
|
||||
: this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user));
|
||||
|
||||
@ -90,7 +90,7 @@ export class NoteDeleteService {
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
||||
this.deliverToConcerned(cascadingNote.user, cascadingNote, content);
|
||||
}
|
||||
//#endregion
|
||||
@ -159,11 +159,11 @@ export class NoteDeleteService {
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where,
|
||||
}) as IRemoteUser[];
|
||||
}) as RemoteUser[];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) {
|
||||
private async deliverToConcerned(user: { id: LocalUser['id']; host: null; }, note: Note, content: any) {
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
this.relayService.deliverToRelays(user, content);
|
||||
const remoteUsers = await this.getMentionedRemoteUsers(note);
|
||||
|
@ -115,7 +115,7 @@ export class NotePiningService {
|
||||
|
||||
const target = `${this.config.url}/users/${user.id}/collections/featured`;
|
||||
const item = `${this.config.url}/notes/${noteId}`;
|
||||
const content = this.apRendererService.renderActivity(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item));
|
||||
const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item));
|
||||
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
this.relayService.deliverToRelays(user, content);
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, User } from '@/models/index.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import type { CacheableUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
@ -39,7 +38,7 @@ export class PollService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async vote(user: CacheableUser, note: Note, choice: number) {
|
||||
public async vote(user: User, note: Note, choice: number) {
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
|
||||
if (poll == null) throw new Error('poll not found');
|
||||
@ -97,7 +96,7 @@ export class PollService {
|
||||
if (user == null) throw new Error('note not found');
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
this.relayService.deliverToRelays(user, content);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -16,9 +16,9 @@ export class ProxyAccountService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(): Promise<ILocalUser | null> {
|
||||
public async fetch(): Promise<LocalUser | null> {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (meta.proxyAccountId == null) return null;
|
||||
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser;
|
||||
return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as LocalUser;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||
@ -85,7 +85,7 @@ export class ReactionService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) {
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) {
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
@ -177,11 +177,11 @@ export class ReactionService {
|
||||
|
||||
//#region 配信
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
const content = this.apRendererService.renderActivity(await this.apRendererService.renderLike(record, note));
|
||||
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
|
||||
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||
if (note.userHost !== null) {
|
||||
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||
dm.addDirectRecipe(reactee as IRemoteUser);
|
||||
dm.addDirectRecipe(reactee as RemoteUser);
|
||||
}
|
||||
|
||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||
@ -189,7 +189,7 @@ export class ReactionService {
|
||||
} else if (note.visibility === 'specified') {
|
||||
const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id })));
|
||||
for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
dm.addDirectRecipe(u as RemoteUser);
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,11 +235,11 @@ export class ReactionService {
|
||||
|
||||
//#region 配信
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
||||
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||
if (note.userHost !== null) {
|
||||
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||
dm.addDirectRecipe(reactee as IRemoteUser);
|
||||
dm.addDirectRecipe(reactee as RemoteUser);
|
||||
}
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
import type { ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
@ -34,16 +34,16 @@ export class RelayService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getRelayActor(): Promise<ILocalUser> {
|
||||
private async getRelayActor(): Promise<LocalUser> {
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
username: ACTOR_USERNAME,
|
||||
});
|
||||
|
||||
if (user) return user as ILocalUser;
|
||||
if (user) return user as LocalUser;
|
||||
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
|
||||
return created as ILocalUser;
|
||||
return created as LocalUser;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -56,7 +56,7 @@ export class RelayService {
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const activity = this.apRendererService.renderActivity(follow);
|
||||
const activity = this.apRendererService.addContext(follow);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox);
|
||||
|
||||
return relay;
|
||||
@ -75,7 +75,7 @@ export class RelayService {
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const undo = this.apRendererService.renderUndo(follow, relayActor);
|
||||
const activity = this.apRendererService.renderActivity(undo);
|
||||
const activity = this.apRendererService.addContext(undo);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox);
|
||||
|
||||
await this.relaysRepository.delete(relay.id);
|
||||
|
@ -4,7 +4,7 @@ import chalk from 'chalk';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@ -60,7 +60,7 @@ export class RemoteUserResolveService {
|
||||
});
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as IRemoteUser | null;
|
||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
||||
|
||||
const acctLower = `${usernameLower}@${host}`;
|
||||
|
||||
@ -82,7 +82,7 @@ export class RemoteUserResolveService {
|
||||
const self = await this.resolveSelf(acctLower);
|
||||
|
||||
if (user.uri !== self.href) {
|
||||
// if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
|
||||
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
||||
this.logger.info(`uri missmatch: ${acctLower}`);
|
||||
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
|
||||
|
||||
|
@ -3,7 +3,7 @@ import Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
@ -211,9 +211,15 @@ export class RoleService implements OnApplicationShutdown {
|
||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||
// コンディショナルロールも含めるのは負荷高そうだから一旦無し
|
||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||
if (badgeCondRoles.length > 0) {
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
} else {
|
||||
return assignedBadgeRoles;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { CacheableUser, User } from '@/models/entities/User.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
@ -117,7 +117,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking));
|
||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
@ -162,13 +162,13 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
// リモートにフォローリクエストをしていたらUndoFollow送信
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
}
|
||||
|
||||
// リモートからフォローリクエストを受けていたらReject送信
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
}
|
||||
@ -210,13 +210,13 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
// リモートにフォローをしていたらUndoFollow送信
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
}
|
||||
|
||||
// リモートからフォローをされていたらRejectFollow送信
|
||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
}
|
||||
@ -236,7 +236,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unblock(blocker: CacheableUser, blockee: CacheableUser) {
|
||||
public async unblock(blocker: User, blockee: User) {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
@ -261,7 +261,7 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
// deliver if remote bloking
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class UserCacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: Cache<CacheableUser>;
|
||||
public localUserByNativeTokenCache: Cache<CacheableLocalUser | null>;
|
||||
public localUserByIdCache: Cache<CacheableLocalUser>;
|
||||
public uriPersonCache: Cache<CacheableUser | null>;
|
||||
public userByIdCache: Cache<User>;
|
||||
public localUserByNativeTokenCache: Cache<LocalUser | null>;
|
||||
public localUserByIdCache: Cache<LocalUser>;
|
||||
public uriPersonCache: Cache<User | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.userByIdCache = new Cache<CacheableUser>(Infinity);
|
||||
this.localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new Cache<CacheableLocalUser>(Infinity);
|
||||
this.uriPersonCache = new Cache<CacheableUser | null>(Infinity);
|
||||
this.userByIdCache = new Cache<User>(Infinity);
|
||||
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new Cache<LocalUser>(Infinity);
|
||||
this.uriPersonCache = new Cache<User | null>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
}
|
||||
@ -58,7 +58,7 @@ export class UserCacheService implements OnApplicationShutdown {
|
||||
break;
|
||||
}
|
||||
case 'userTokenRegenerated': {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as ILocalUser;
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
|
||||
this.localUserByNativeTokenCache.delete(body.oldToken);
|
||||
this.localUserByNativeTokenCache.set(body.newToken, user);
|
||||
break;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { CacheableUser, ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
@ -21,16 +21,16 @@ import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
|
||||
type Local = ILocalUser | {
|
||||
id: ILocalUser['id'];
|
||||
host: ILocalUser['host'];
|
||||
uri: ILocalUser['uri']
|
||||
type Local = LocalUser | {
|
||||
id: LocalUser['id'];
|
||||
host: LocalUser['host'];
|
||||
uri: LocalUser['uri']
|
||||
};
|
||||
type Remote = IRemoteUser | {
|
||||
id: IRemoteUser['id'];
|
||||
host: IRemoteUser['host'];
|
||||
uri: IRemoteUser['uri'];
|
||||
inbox: IRemoteUser['inbox'];
|
||||
type Remote = RemoteUser | {
|
||||
id: RemoteUser['id'];
|
||||
host: RemoteUser['host'];
|
||||
uri: RemoteUser['uri'];
|
||||
inbox: RemoteUser['inbox'];
|
||||
};
|
||||
type Both = Local | Remote;
|
||||
|
||||
@ -81,7 +81,7 @@ export class UserFollowingService {
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
|
||||
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
return;
|
||||
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
|
||||
@ -130,7 +130,7 @@ export class UserFollowingService {
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
}
|
||||
@ -293,13 +293,13 @@ export class UserFollowingService {
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||
// local user has null host
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
}
|
||||
@ -388,7 +388,7 @@ export class UserFollowingService {
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
}
|
||||
}
|
||||
@ -403,7 +403,7 @@ export class UserFollowingService {
|
||||
},
|
||||
): Promise<void> {
|
||||
if (this.userEntityService.isRemoteUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||||
this.queueService.deliver(follower, content, followee.inbox);
|
||||
@ -434,7 +434,7 @@ export class UserFollowingService {
|
||||
followee: {
|
||||
id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'];
|
||||
},
|
||||
follower: CacheableUser,
|
||||
follower: User,
|
||||
): Promise<void> {
|
||||
const request = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
@ -448,7 +448,7 @@ export class UserFollowingService {
|
||||
await this.insertFollowingDoc(followee, follower);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
|
||||
@ -556,7 +556,7 @@ export class UserFollowingService {
|
||||
followerId: follower.id,
|
||||
});
|
||||
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService {
|
||||
public static TooManyUsersError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@ -36,7 +38,7 @@ export class UserListService {
|
||||
userListId: list.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||
throw new Error('Too many users');
|
||||
throw new UserListService.TooManyUsersError();
|
||||
}
|
||||
|
||||
await this.userListJoiningsRepository.insert({
|
||||
|
@ -35,7 +35,7 @@ export class UserSuspendService {
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
@ -65,7 +65,7 @@ export class UserSuspendService {
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにUndo Delete配信
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user));
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { ImageProcessingService } from '@/core/ImageProcessingService.js';
|
||||
import type { IImage } from '@/core/ImageProcessingService.js';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||
|
||||
@Injectable()
|
||||
export class VideoProcessingService {
|
||||
@ -41,5 +42,18 @@ export class VideoProcessingService {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getExternalVideoThumbnailUrl(url: string): string | null {
|
||||
if (this.config.videoThumbnailGenerator == null) return null;
|
||||
|
||||
return appendQuery(
|
||||
`${this.config.videoThumbnailGenerator}/thumbnail.webp`,
|
||||
query({
|
||||
thumbnail: '1',
|
||||
url,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
@ -14,8 +14,8 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
||||
|
||||
type AudienceInfo = {
|
||||
visibility: Visibility,
|
||||
mentionedUsers: CacheableUser[],
|
||||
visibleUsers: CacheableUser[],
|
||||
mentionedUsers: User[],
|
||||
visibleUsers: User[],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -26,16 +26,16 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||
const toGroups = this.groupingAudience(getApIds(to), actor);
|
||||
const ccGroups = this.groupingAudience(getApIds(cc), actor);
|
||||
|
||||
const others = unique(concat([toGroups.other, ccGroups.other]));
|
||||
|
||||
const limit = promiseLimit<CacheableUser | null>(2);
|
||||
const limit = promiseLimit<User | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||
)).filter((x): x is CacheableUser => x != null);
|
||||
)).filter((x): x is User => x != null);
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
@ -69,7 +69,7 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private groupingAudience(ids: string[], actor: CacheableRemoteUser) {
|
||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
||||
const groups = {
|
||||
public: [] as string[],
|
||||
followers: [] as string[],
|
||||
@ -101,7 +101,7 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private isFollowers(id: string, actor: CacheableRemoteUser) {
|
||||
private isFollowers(id: string, actor: RemoteUser) {
|
||||
return (
|
||||
id === (actor.followersUri ?? `${actor.uri}/followers`)
|
||||
);
|
||||
|
@ -3,13 +3,13 @@ import escapeRegexp from 'escape-regexp';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { getApId } from './type.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import type { IObject } from './type.js';
|
||||
@ -122,7 +122,7 @@ export class ApDbResolverService {
|
||||
* AP Person => Misskey User in DB
|
||||
*/
|
||||
@bindThis
|
||||
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
|
||||
public async getUserFromApId(value: string | IObject): Promise<User | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
|
||||
if (parsed.local) {
|
||||
@ -143,7 +143,7 @@ export class ApDbResolverService {
|
||||
*/
|
||||
@bindThis
|
||||
public async getAuthUserFromKeyId(keyId: string): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
user: RemoteUser;
|
||||
key: UserPublickey;
|
||||
} | null> {
|
||||
const key = await this.publicKeyCache.fetch(keyId, async () => {
|
||||
@ -159,7 +159,7 @@ export class ApDbResolverService {
|
||||
if (key == null) return null;
|
||||
|
||||
return {
|
||||
user: await this.userCacheService.findById(key.userId) as CacheableRemoteUser,
|
||||
user: await this.userCacheService.findById(key.userId) as RemoteUser,
|
||||
key,
|
||||
};
|
||||
}
|
||||
@ -169,10 +169,10 @@ export class ApDbResolverService {
|
||||
*/
|
||||
@bindThis
|
||||
public async getAuthUserFromApId(uri: string): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
user: RemoteUser;
|
||||
key: UserPublickey | null;
|
||||
} | null> {
|
||||
const user = await this.apPersonService.resolvePerson(uri) as CacheableRemoteUser;
|
||||
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
||||
|
||||
if (user == null) return null;
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { IsNull, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -18,7 +18,7 @@ interface IFollowersRecipe extends IRecipe {
|
||||
|
||||
interface IDirectRecipe extends IRecipe {
|
||||
type: 'Direct';
|
||||
to: IRemoteUser;
|
||||
to: RemoteUser;
|
||||
}
|
||||
|
||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||
@ -50,7 +50,7 @@ export class ApDeliverManagerService {
|
||||
* @param from Followee
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) {
|
||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
@ -68,7 +68,7 @@ export class ApDeliverManagerService {
|
||||
* @param to Target user
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) {
|
||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
@ -132,7 +132,7 @@ class DeliverManager {
|
||||
* @param to To
|
||||
*/
|
||||
@bindThis
|
||||
public addDirectRecipe(to: IRemoteUser) {
|
||||
public addDirectRecipe(to: RemoteUser) {
|
||||
const recipe = {
|
||||
type: 'Direct',
|
||||
to,
|
||||
|
@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableRemoteUser } from '@/models/entities/User.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
@ -22,6 +21,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { MessagingService } from '@/core/MessagingService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
@ -32,7 +33,6 @@ import { ApPersonService } from './models/ApPersonService.js';
|
||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||
import type { Resolver } from './ApResolverService.js';
|
||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate } from './type.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApInboxService {
|
||||
@ -87,7 +87,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
||||
public async performActivity(actor: RemoteUser, activity: IObject) {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
@ -115,7 +115,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> {
|
||||
public async performOneActivity(actor: RemoteUser, activity: IObject): Promise<void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (isCreate(activity)) {
|
||||
@ -152,7 +152,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async follow(actor: CacheableRemoteUser, activity: IFollow): Promise<string> {
|
||||
private async follow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
|
||||
if (followee == null) {
|
||||
@ -168,7 +168,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async like(actor: CacheableRemoteUser, activity: ILike): Promise<string> {
|
||||
private async like(actor: RemoteUser, activity: ILike): Promise<string> {
|
||||
const targetUri = getApId(activity.object);
|
||||
|
||||
const note = await this.apNoteService.fetchNote(targetUri);
|
||||
@ -186,7 +186,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async read(actor: CacheableRemoteUser, activity: IRead): Promise<string> {
|
||||
private async read(actor: RemoteUser, activity: IRead): Promise<string> {
|
||||
const id = await getApId(activity.object);
|
||||
|
||||
if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) {
|
||||
@ -209,7 +209,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async accept(actor: CacheableRemoteUser, activity: IAccept): Promise<string> {
|
||||
private async accept(actor: RemoteUser, activity: IAccept): Promise<string> {
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
this.logger.info(`Accept: ${uri}`);
|
||||
@ -227,7 +227,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async acceptFollow(actor: CacheableRemoteUser, activity: IFollow): Promise<string> {
|
||||
private async acceptFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
||||
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
|
||||
@ -251,7 +251,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async add(actor: CacheableRemoteUser, activity: IAdd): Promise<void> {
|
||||
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
@ -271,7 +271,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announce(actor: CacheableRemoteUser, activity: IAnnounce): Promise<void> {
|
||||
private async announce(actor: RemoteUser, activity: IAnnounce): Promise<void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Announce: ${uri}`);
|
||||
@ -282,7 +282,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async announceNote(actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
private async announceNote(actor: RemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
if (actor.isSuspended) {
|
||||
@ -342,7 +342,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async block(actor: CacheableRemoteUser, activity: IBlock): Promise<string> {
|
||||
private async block(actor: RemoteUser, activity: IBlock): Promise<string> {
|
||||
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
|
||||
|
||||
const blockee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
@ -360,7 +360,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async create(actor: CacheableRemoteUser, activity: ICreate): Promise<void> {
|
||||
private async create(actor: RemoteUser, activity: ICreate): Promise<void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Create: ${uri}`);
|
||||
@ -396,7 +396,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async createNote(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
|
||||
private async createNote(resolver: Resolver, actor: RemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
|
||||
const uri = getApId(note);
|
||||
|
||||
if (typeof note === 'object') {
|
||||
@ -431,7 +431,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async delete(actor: CacheableRemoteUser, activity: IDelete): Promise<string> {
|
||||
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
@ -473,7 +473,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||
private async deleteActor(actor: RemoteUser, uri: string): Promise<string> {
|
||||
this.logger.info(`Deleting the Actor: ${uri}`);
|
||||
|
||||
if (actor.uri !== uri) {
|
||||
@ -495,7 +495,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteNote(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||
private async deleteNote(actor: RemoteUser, uri: string): Promise<string> {
|
||||
this.logger.info(`Deleting the Note: ${uri}`);
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
@ -528,7 +528,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async flag(actor: CacheableRemoteUser, activity: IFlag): Promise<string> {
|
||||
private async flag(actor: RemoteUser, activity: IFlag): Promise<string> {
|
||||
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
|
||||
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
||||
const uris = getApIds(activity.object);
|
||||
@ -553,7 +553,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async reject(actor: CacheableRemoteUser, activity: IReject): Promise<string> {
|
||||
private async reject(actor: RemoteUser, activity: IReject): Promise<string> {
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
this.logger.info(`Reject: ${uri}`);
|
||||
@ -571,7 +571,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async rejectFollow(actor: CacheableRemoteUser, activity: IFollow): Promise<string> {
|
||||
private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
||||
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
|
||||
@ -595,7 +595,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async remove(actor: CacheableRemoteUser, activity: IRemove): Promise<void> {
|
||||
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
@ -615,7 +615,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undo(actor: CacheableRemoteUser, activity: IUndo): Promise<string> {
|
||||
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
@ -641,7 +641,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undoAccept(actor: CacheableRemoteUser, activity: IAccept): Promise<string> {
|
||||
private async undoAccept(actor: RemoteUser, activity: IAccept): Promise<string> {
|
||||
const follower = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
if (follower == null) {
|
||||
return 'skip: follower not found';
|
||||
@ -661,7 +661,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undoAnnounce(actor: CacheableRemoteUser, activity: IAnnounce): Promise<string> {
|
||||
private async undoAnnounce(actor: RemoteUser, activity: IAnnounce): Promise<string> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
@ -676,7 +676,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undoBlock(actor: CacheableRemoteUser, activity: IBlock): Promise<string> {
|
||||
private async undoBlock(actor: RemoteUser, activity: IBlock): Promise<string> {
|
||||
const blockee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
|
||||
if (blockee == null) {
|
||||
@ -692,7 +692,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undoFollow(actor: CacheableRemoteUser, activity: IFollow): Promise<string> {
|
||||
private async undoFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||
if (followee == null) {
|
||||
return 'skip: followee not found';
|
||||
@ -726,7 +726,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async undoLike(actor: CacheableRemoteUser, activity: ILike): Promise<string> {
|
||||
private async undoLike(actor: RemoteUser, activity: ILike): Promise<string> {
|
||||
const targetUri = getApId(activity.object);
|
||||
|
||||
const note = await this.apNoteService.fetchNote(targetUri);
|
||||
@ -741,7 +741,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async update(actor: CacheableRemoteUser, activity: IUpdate): Promise<string> {
|
||||
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
return 'skip: invalid actor';
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
import type { Relay } from '@/models/entities/Relay.js';
|
||||
@ -24,7 +24,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import type { IActivity, IObject } from './type.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IRead, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
import type { IIdentifier } from './models/identifier.js';
|
||||
|
||||
@Injectable()
|
||||
@ -61,7 +61,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAccept(object: any, user: { id: User['id']; host: null }) {
|
||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
||||
return {
|
||||
type: 'Accept',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -70,7 +70,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAdd(user: ILocalUser, target: any, object: any) {
|
||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
||||
return {
|
||||
type: 'Add',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -80,7 +80,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAnnounce(object: any, note: Note) {
|
||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
||||
|
||||
let to: string[] = [];
|
||||
@ -93,7 +93,7 @@ export class ApRendererService {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
} else {
|
||||
return null;
|
||||
throw new Error('renderAnnounce: cannot render non-public note');
|
||||
}
|
||||
|
||||
return {
|
||||
@ -113,7 +113,7 @@ export class ApRendererService {
|
||||
* @param block The block to be rendered. The blockee relation must be loaded.
|
||||
*/
|
||||
@bindThis
|
||||
public renderBlock(block: Blocking) {
|
||||
public renderBlock(block: Blocking): IBlock {
|
||||
if (block.blockee?.uri == null) {
|
||||
throw new Error('renderBlock: missing blockee uri');
|
||||
}
|
||||
@ -127,14 +127,14 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderCreate(object: any, note: Note) {
|
||||
public renderCreate(object: IObject, note: Note): ICreate {
|
||||
const activity = {
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: `${this.config.url}/users/${note.userId}`,
|
||||
type: 'Create',
|
||||
published: note.createdAt.toISOString(),
|
||||
object,
|
||||
} as any;
|
||||
} as ICreate;
|
||||
|
||||
if (object.to) activity.to = object.to;
|
||||
if (object.cc) activity.cc = object.cc;
|
||||
@ -143,7 +143,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderDelete(object: any, user: { id: User['id']; host: null }) {
|
||||
public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete {
|
||||
return {
|
||||
type: 'Delete',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -153,7 +153,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderDocument(file: DriveFile) {
|
||||
public renderDocument(file: DriveFile): IApDocument {
|
||||
return {
|
||||
type: 'Document',
|
||||
mediaType: file.type,
|
||||
@ -163,12 +163,12 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderEmoji(emoji: Emoji) {
|
||||
public renderEmoji(emoji: Emoji): IApEmoji {
|
||||
return {
|
||||
id: `${this.config.url}/emojis/${emoji.name}`,
|
||||
type: 'Emoji',
|
||||
name: `:${emoji.name}:`,
|
||||
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
|
||||
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString(),
|
||||
icon: {
|
||||
type: 'Image',
|
||||
mediaType: emoji.type ?? 'image/png',
|
||||
@ -181,7 +181,7 @@ export class ApRendererService {
|
||||
// to anonymise reporters, the reporting actor must be a system user
|
||||
// object has to be a uri or array of uris
|
||||
@bindThis
|
||||
public renderFlag(user: ILocalUser, object: [string], content: string) {
|
||||
public renderFlag(user: LocalUser, object: IObject | string | string[], content: string): IFlag {
|
||||
return {
|
||||
type: 'Flag',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -191,15 +191,13 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
|
||||
const follow = {
|
||||
public renderFollowRelay(relay: Relay, relayActor: LocalUser): IFollow {
|
||||
return {
|
||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
||||
type: 'Follow',
|
||||
actor: `${this.config.url}/users/${relayActor.id}`,
|
||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
};
|
||||
|
||||
return follow;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,19 +215,17 @@ export class ApRendererService {
|
||||
follower: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
followee: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||
requestId?: string,
|
||||
) {
|
||||
const follow = {
|
||||
): IFollow {
|
||||
return {
|
||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||
type: 'Follow',
|
||||
actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri,
|
||||
object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri,
|
||||
} as any;
|
||||
|
||||
return follow;
|
||||
actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!,
|
||||
object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderHashtag(tag: string) {
|
||||
public renderHashtag(tag: string): IApHashtag {
|
||||
return {
|
||||
type: 'Hashtag',
|
||||
href: `${this.config.url}/tags/${encodeURIComponent(tag)}`,
|
||||
@ -238,7 +234,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderImage(file: DriveFile) {
|
||||
public renderImage(file: DriveFile): IApImage {
|
||||
return {
|
||||
type: 'Image',
|
||||
url: this.driveFileEntityService.getPublicUrl(file),
|
||||
@ -248,7 +244,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderKey(user: ILocalUser, key: UserKeypair, postfix?: string) {
|
||||
public renderKey(user: LocalUser, key: UserKeypair, postfix?: string): IKey {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
||||
type: 'Key',
|
||||
@ -261,7 +257,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }) {
|
||||
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||
const reaction = noteReaction.reaction;
|
||||
|
||||
const object = {
|
||||
@ -271,10 +267,11 @@ export class ApRendererService {
|
||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||
content: reaction,
|
||||
_misskey_reaction: reaction,
|
||||
} as any;
|
||||
} as ILike;
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
// TODO: cache
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
@ -287,16 +284,16 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderMention(mention: User) {
|
||||
public renderMention(mention: User): IApMention {
|
||||
return {
|
||||
type: 'Mention',
|
||||
href: this.userEntityService.isRemoteUser(mention) ? mention.uri : `${this.config.url}/users/${(mention as ILocalUser).id}`,
|
||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
|
||||
href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`,
|
||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderNote(note: Note, dive = true, isTalk = false): Promise<IObject> {
|
||||
public async renderNote(note: Note, dive = true, isTalk = false): Promise<IPost> {
|
||||
const getPromisedFiles = async (ids: string[]) => {
|
||||
if (!ids || ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
@ -409,11 +406,11 @@ export class ApRendererService {
|
||||
totalItems: poll!.votes[i],
|
||||
},
|
||||
})),
|
||||
} : {};
|
||||
} as const : {};
|
||||
|
||||
const asTalk = isTalk ? {
|
||||
_misskey_talk: true,
|
||||
} : {};
|
||||
} as const : {};
|
||||
|
||||
return {
|
||||
id: `${this.config.url}/notes/${note.id}`,
|
||||
@ -441,7 +438,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderPerson(user: ILocalUser) {
|
||||
public async renderPerson(user: LocalUser) {
|
||||
const id = `${this.config.url}/users/${user.id}`;
|
||||
const isSystem = !!user.username.match(/\./);
|
||||
|
||||
@ -518,8 +515,8 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) {
|
||||
const question = {
|
||||
public renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll): IQuestion {
|
||||
return {
|
||||
type: 'Question',
|
||||
id: `${this.config.url}/questions/${note.id}`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -533,21 +530,19 @@ export class ApRendererService {
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return question;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderRead(user: { id: User['id'] }, message: MessagingMessage) {
|
||||
public renderRead(user: { id: User['id'] }, message: MessagingMessage): IRead {
|
||||
return {
|
||||
type: 'Read',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
object: message.uri,
|
||||
object: message.uri!,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderReject(object: any, user: { id: User['id'] }) {
|
||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
||||
return {
|
||||
type: 'Reject',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -556,7 +551,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderRemove(user: { id: User['id'] }, target: any, object: any) {
|
||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
||||
return {
|
||||
type: 'Remove',
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -566,7 +561,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderTombstone(id: string) {
|
||||
public renderTombstone(id: string): ITombstone {
|
||||
return {
|
||||
id,
|
||||
type: 'Tombstone',
|
||||
@ -574,8 +569,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderUndo(object: any, user: { id: User['id'] }) {
|
||||
if (object == null) return null;
|
||||
public renderUndo(object: any, user: { id: User['id'] }): IUndo {
|
||||
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||
|
||||
return {
|
||||
@ -588,21 +582,19 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderUpdate(object: any, user: { id: User['id'] }) {
|
||||
const activity = {
|
||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
type: 'Update',
|
||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
} as any;
|
||||
|
||||
return activity;
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser) {
|
||||
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
||||
actor: `${this.config.url}/users/${user.id}`,
|
||||
@ -621,9 +613,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderActivity(x: any): IActivity | null {
|
||||
if (x == null) return null;
|
||||
|
||||
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
|
||||
if (typeof x === 'object' && x.id == null) {
|
||||
x.id = `${this.config.url}/${uuid()}`;
|
||||
}
|
||||
@ -659,7 +649,7 @@ export class ApRendererService {
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
},
|
||||
],
|
||||
}, x);
|
||||
}, x as T & { id: string; });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@ -18,7 +18,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||
|
||||
export class Resolver {
|
||||
private history: Set<string>;
|
||||
private user?: ILocalUser;
|
||||
private user?: LocalUser;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@ -38,8 +38,7 @@ export class Resolver {
|
||||
private recursionLimit = 100,
|
||||
) {
|
||||
this.history = new Set();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||
this.logger = this.loggerService.getLogger('ap-resolve');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -124,17 +123,17 @@ export class Resolver {
|
||||
switch (parsed.type) {
|
||||
case 'notes':
|
||||
return this.notesRepository.findOneByOrFail({ id: parsed.id })
|
||||
.then(note => {
|
||||
.then(async note => {
|
||||
if (parsed.rest === 'activity') {
|
||||
// this refers to the create activity and not the note itself
|
||||
return this.apRendererService.renderActivity(this.apRendererService.renderCreate(this.apRendererService.renderNote(note), note));
|
||||
return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note), note));
|
||||
} else {
|
||||
return this.apRendererService.renderNote(note);
|
||||
}
|
||||
});
|
||||
case 'users':
|
||||
return this.usersRepository.findOneByOrFail({ id: parsed.id })
|
||||
.then(user => this.apRendererService.renderPerson(user as ILocalUser));
|
||||
.then(user => this.apRendererService.renderPerson(user as LocalUser));
|
||||
case 'questions':
|
||||
// Polls are indexed by the note they are attached to.
|
||||
return Promise.all([
|
||||
@ -143,8 +142,8 @@ export class Resolver {
|
||||
])
|
||||
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll));
|
||||
case 'likes':
|
||||
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(reaction =>
|
||||
this.apRendererService.renderActivity(this.apRendererService.renderLike(reaction, { uri: null }))!);
|
||||
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
|
||||
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
|
||||
case 'follows':
|
||||
// rest should be <followee id>
|
||||
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
|
||||
@ -152,7 +151,7 @@ export class Resolver {
|
||||
return Promise.all(
|
||||
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
|
||||
)
|
||||
.then(([follower, followee]) => this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee, url)));
|
||||
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url)));
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||
}
|
||||
@ -184,6 +183,7 @@ export class ApResolverService {
|
||||
private httpRequestService: HttpRequestService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -202,6 +202,7 @@ export class ApResolverService {
|
||||
this.httpRequestService,
|
||||
this.apRendererService,
|
||||
this.apDbResolverService,
|
||||
this.loggerService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import jsonld from 'jsonld';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
@ -85,7 +84,9 @@ class LdSignature {
|
||||
@bindThis
|
||||
public async normalize(data: any) {
|
||||
const customLoader = this.getLoader();
|
||||
return await jsonld.normalize(data, {
|
||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||
return (await import('jsonld')).default.normalize(data, {
|
||||
documentLoader: customLoader,
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableRemoteUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { truncate } from '@/misc/truncate.js';
|
||||
@ -36,7 +36,7 @@ export class ApImageService {
|
||||
* Imageを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
public async createImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
|
||||
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
@ -88,7 +88,7 @@ export class ApImageService {
|
||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolveImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
|
||||
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||
// TODO
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { User, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { toArray, unique } from '@/misc/prelude/array.js';
|
||||
import type { CacheableUser } from '@/models/entities/User.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isMention } from '../type.js';
|
||||
import { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||
import { ApPersonService } from './ApPersonService.js';
|
||||
import type { IObject, IApMention } from '../type.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApMentionService {
|
||||
@ -26,10 +25,10 @@ export class ApMentionService {
|
||||
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
|
||||
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
|
||||
|
||||
const limit = promiseLimit<CacheableUser | null>(2);
|
||||
const limit = promiseLimit<User | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter((x): x is CacheableUser => x != null);
|
||||
)).filter((x): x is User => x != null);
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableRemoteUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
@ -114,7 +114,7 @@ export class ApNoteService {
|
||||
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
|
||||
const object: any = await resolver.resolve(value);
|
||||
const object = await resolver.resolve(value);
|
||||
|
||||
const entryUri = getApId(value);
|
||||
const err = this.validateNote(object, entryUri);
|
||||
@ -129,7 +129,7 @@ export class ApNoteService {
|
||||
throw new Error('invalid note');
|
||||
}
|
||||
|
||||
const note: IPost = object;
|
||||
const note: IPost = object as any;
|
||||
|
||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
@ -146,7 +146,7 @@ export class ApNoteService {
|
||||
this.logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
// 投稿者をフェッチ
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser;
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
|
||||
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
|
@ -5,7 +5,7 @@ import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { CacheableUser, IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { User } from '@/models/entities/User.js';
|
||||
import { truncate } from '@/misc/truncate.js';
|
||||
import type { UserCacheService } from '@/core/UserCacheService.js';
|
||||
@ -197,7 +197,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
* Misskeyに対象のPersonが登録されていればそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchPerson(uri: string, resolver?: Resolver): Promise<CacheableUser | null> {
|
||||
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
const cached = this.userCacheService.uriPersonCache.get(uri);
|
||||
@ -259,7 +259,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
// Create user
|
||||
let user: IRemoteUser;
|
||||
let user: RemoteUser;
|
||||
try {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
@ -284,7 +284,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
showTimelineReplies: false,
|
||||
})) as IRemoteUser;
|
||||
})) as RemoteUser;
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
userId: user.id,
|
||||
@ -313,7 +313,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
});
|
||||
|
||||
if (u) {
|
||||
user = u as IRemoteUser;
|
||||
user = u as RemoteUser;
|
||||
} else {
|
||||
throw new Error('already registered');
|
||||
}
|
||||
@ -392,7 +392,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as IRemoteUser;
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser;
|
||||
|
||||
if (exist == null) {
|
||||
return;
|
||||
@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
|
||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
|
@ -2,24 +2,24 @@ export type obj = { [x: string]: any };
|
||||
export type ApObject = IObject | string | (IObject | string)[];
|
||||
|
||||
export interface IObject {
|
||||
'@context': string | string[] | obj | obj[];
|
||||
'@context'?: string | string[] | obj | obj[];
|
||||
type: string | string[];
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
summary?: string;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
attributedTo: ApObject;
|
||||
attributedTo?: ApObject;
|
||||
attachment?: any[];
|
||||
inReplyTo?: any;
|
||||
replies?: ICollection;
|
||||
content?: string;
|
||||
name?: string;
|
||||
content?: string | null;
|
||||
startTime?: Date;
|
||||
endTime?: Date;
|
||||
icon?: any;
|
||||
image?: any;
|
||||
url?: ApObject;
|
||||
url?: ApObject | string;
|
||||
href?: string;
|
||||
tag?: IObject | IObject[];
|
||||
sensitive?: boolean;
|
||||
@ -118,6 +118,7 @@ export interface IPost extends IObject {
|
||||
|
||||
export interface IQuestion extends IObject {
|
||||
type: 'Note' | 'Question';
|
||||
actor: string;
|
||||
source?: {
|
||||
content: string;
|
||||
mediaType: string;
|
||||
@ -200,6 +201,7 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||
export interface IApMention extends IObject {
|
||||
type: 'Mention';
|
||||
href: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const isMention = (object: IObject): object is IApMention =>
|
||||
@ -217,12 +219,30 @@ export const isHashtag = (object: IObject): object is IApHashtag =>
|
||||
|
||||
export interface IApEmoji extends IObject {
|
||||
type: 'Emoji';
|
||||
updated: Date;
|
||||
name: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export const isEmoji = (object: IObject): object is IApEmoji =>
|
||||
getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
|
||||
|
||||
export interface IKey extends IObject {
|
||||
type: 'Key';
|
||||
owner: string;
|
||||
publicKeyPem: string | Buffer;
|
||||
}
|
||||
|
||||
export interface IApDocument extends IObject {
|
||||
type: 'Document';
|
||||
name: string | null;
|
||||
mediaType: string;
|
||||
}
|
||||
|
||||
export interface IApImage extends IObject {
|
||||
type: 'Image';
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
export interface ICreate extends IActivity {
|
||||
type: 'Create';
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { UtilityService } from '../UtilityService.js';
|
||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
|
||||
|
||||
@ -43,6 +44,7 @@ export class DriveFileEntityService {
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private driveFolderEntityService: DriveFolderEntityService,
|
||||
private videoProcessingService: VideoProcessingService,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -72,40 +74,63 @@ export class DriveFileEntityService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail
|
||||
const proxiedUrl = (url: string) => appendQuery(
|
||||
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
|
||||
return appendQuery(
|
||||
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
||||
query({
|
||||
url,
|
||||
...(mode ? { [mode]: '1' } : {}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getThumbnailUrl(file: DriveFile): string | null {
|
||||
if (file.type.startsWith('video')) {
|
||||
if (file.thumbnailUrl) return file.thumbnailUrl;
|
||||
|
||||
if (this.config.videoThumbnailGenerator == null) {
|
||||
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
|
||||
}
|
||||
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||
// 動画ではなくリモートかつメディアプロキシ
|
||||
return this.getProxiedUrl(file.uri, 'static');
|
||||
}
|
||||
|
||||
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
||||
// リモートかつ期限切れはローカルプロキシを試みる
|
||||
// 従来は/files/${thumbnailAccessKey}にアクセスしていたが、
|
||||
// /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する
|
||||
return this.getProxiedUrl(file.uri, 'static');
|
||||
}
|
||||
|
||||
const url = file.webpublicUrl ?? file.url;
|
||||
|
||||
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPublicUrl(file: DriveFile, mode?: 'avatar'): string { // static = thumbnail
|
||||
// リモートかつメディアプロキシ
|
||||
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||
if (!(mode === 'static' && file.type.startsWith('video'))) {
|
||||
return proxiedUrl(file.uri);
|
||||
}
|
||||
return this.getProxiedUrl(file.uri, mode);
|
||||
}
|
||||
|
||||
// リモートかつ期限切れはローカルプロキシを試みる
|
||||
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
||||
const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey;
|
||||
const key = file.webpublicAccessKey;
|
||||
|
||||
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
||||
const url = `${this.config.url}/files/${key}`;
|
||||
if (mode === 'avatar') return proxiedUrl(file.uri);
|
||||
if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar');
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
const url = file.webpublicUrl ?? file.url;
|
||||
|
||||
if (mode === 'static') {
|
||||
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
|
||||
}
|
||||
if (mode === 'avatar') {
|
||||
return proxiedUrl(url);
|
||||
return this.getProxiedUrl(url, 'avatar');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@ -183,7 +208,7 @@ export class DriveFileEntityService {
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||
thumbnailUrl: this.getThumbnailUrl(file),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
@ -218,7 +243,7 @@ export class DriveFileEntityService {
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||
thumbnailUrl: this.getThumbnailUrl(file),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
|
@ -10,7 +10,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -32,13 +32,13 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
function isLocalUser(user: User): user is ILocalUser;
|
||||
function isLocalUser(user: User): user is LocalUser;
|
||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
||||
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
function isRemoteUser(user: User): user is IRemoteUser;
|
||||
function isRemoteUser(user: User): user is RemoteUser;
|
||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
||||
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||
return !isLocalUser(user);
|
||||
|
@ -215,20 +215,16 @@ export class User {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILocalUser extends User {
|
||||
export type LocalUser = User & {
|
||||
host: null;
|
||||
uri: null;
|
||||
}
|
||||
|
||||
export interface IRemoteUser extends User {
|
||||
export type RemoteUser = User & {
|
||||
host: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export type CacheableLocalUser = ILocalUser;
|
||||
|
||||
export type CacheableRemoteUser = IRemoteUser;
|
||||
|
||||
export type CacheableUser = CacheableLocalUser | CacheableRemoteUser;
|
||||
|
||||
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
|
@ -16,7 +16,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import FederationChart from '@/core/chart/charts/federation.js';
|
||||
import { getApId } from '@/core/activitypub/type.js';
|
||||
import type { CacheableRemoteUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
@ -87,7 +87,7 @@ export class InboxProcessorService {
|
||||
|
||||
// HTTP-Signature keyIdを元にDBから取得
|
||||
let authUser: {
|
||||
user: CacheableRemoteUser;
|
||||
user: RemoteUser;
|
||||
key: UserPublickey | null;
|
||||
} | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
|
||||
|
||||
|
@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import type { ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||
import type { Following } from '@/models/entities/Following.js';
|
||||
import { countIf } from '@/misc/prelude/array.js';
|
||||
@ -183,13 +183,13 @@ export class ActivityPubServerService {
|
||||
);
|
||||
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,13 +271,13 @@ export class ActivityPubServerService {
|
||||
);
|
||||
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,7 +312,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -389,7 +389,7 @@ export class ActivityPubServerService {
|
||||
);
|
||||
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
|
||||
@ -398,7 +398,7 @@ export class ActivityPubServerService {
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(rendered));
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,7 +411,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser)));
|
||||
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as LocalUser)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -441,6 +441,14 @@ export class ActivityPubServerService {
|
||||
fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||
fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Access-Control-Allow-Headers', 'Accept');
|
||||
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
reply.header('Access-Control-Allow-Origin', '*');
|
||||
reply.header('Access-Control-Expose-Headers', 'Vary');
|
||||
done();
|
||||
});
|
||||
|
||||
//#region Routing
|
||||
// inbox (limit: 64kb)
|
||||
fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||
@ -473,7 +481,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false)));
|
||||
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false));
|
||||
});
|
||||
|
||||
// note activity
|
||||
@ -494,7 +502,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(await this.packActivity(note)));
|
||||
return (this.apRendererService.addContext(await this.packActivity(note)));
|
||||
});
|
||||
|
||||
// outbox
|
||||
@ -537,7 +545,7 @@ export class ActivityPubServerService {
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
|
||||
return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair)));
|
||||
} else {
|
||||
reply.code(400);
|
||||
return;
|
||||
@ -581,7 +589,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji)));
|
||||
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
|
||||
});
|
||||
|
||||
// like
|
||||
@ -602,7 +610,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note)));
|
||||
return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note)));
|
||||
});
|
||||
|
||||
// follow
|
||||
@ -628,7 +636,7 @@ export class ActivityPubServerService {
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)));
|
||||
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
|
||||
});
|
||||
|
||||
done();
|
||||
|
@ -150,6 +150,12 @@ export class FileServerService {
|
||||
file.cleanup();
|
||||
return await reply.redirect(301, url.toString());
|
||||
} else if (file.mime.startsWith('video/')) {
|
||||
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
||||
if (externalThumbnail) {
|
||||
file.cleanup();
|
||||
return await reply.redirect(301, externalThumbnail);
|
||||
}
|
||||
|
||||
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { CacheableLocalUser, ILocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { UserIpsRepository } from '@/models/index.js';
|
||||
@ -168,7 +168,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async logIp(request: FastifyRequest, user: ILocalUser) {
|
||||
private async logIp(request: FastifyRequest, user: LocalUser) {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (!meta.enableIpLogging) return;
|
||||
const ip = request.ip;
|
||||
@ -194,7 +194,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
private async call(
|
||||
ep: IEndpoint & { exec: any },
|
||||
user: CacheableLocalUser | null | undefined,
|
||||
user: LocalUser | null | undefined,
|
||||
token: AccessToken | null | undefined,
|
||||
data: any,
|
||||
file: {
|
||||
@ -227,6 +227,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
||||
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
||||
|
||||
if (factor > 0) {
|
||||
// Rate limit
|
||||
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
|
||||
throw new ApiError({
|
||||
@ -237,6 +238,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) {
|
||||
if (user == null) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { CacheableLocalUser, ILocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { App } from '@/models/entities/App.js';
|
||||
@ -36,14 +36,14 @@ export class AuthenticateService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
if (token == null) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token,
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<ILocalUser | null>);
|
||||
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationError('user not found');
|
||||
@ -70,7 +70,7 @@ export class AuthenticateService {
|
||||
const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId,
|
||||
() => this.usersRepository.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<ILocalUser>);
|
||||
}) as Promise<LocalUser>);
|
||||
|
||||
if (accessToken.appId) {
|
||||
const app = await this.appCache.fetch(accessToken.appId,
|
||||
|
@ -7,7 +7,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -105,7 +105,7 @@ export class SigninApiService {
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
}) as ILocalUser;
|
||||
}) as LocalUser;
|
||||
|
||||
if (user == null) {
|
||||
return error(404, {
|
||||
|
@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { SigninsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { ILocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -25,7 +25,7 @@ export class SigninService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
|
||||
public signin(request: FastifyRequest, reply: FastifyReply, user: LocalUser) {
|
||||
setImmediate(async () => {
|
||||
// Append signin history
|
||||
const record = await this.signinsRepository.insert({
|
||||
|
@ -10,7 +10,7 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { SignupService } from '@/core/SignupService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { EmailService } from '@/core/EmailService.js';
|
||||
import { ILocalUser } from '@/models/entities/User.js';
|
||||
import { LocalUser } from '@/models/entities/User.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
@ -194,7 +194,7 @@ export class SignupApiService {
|
||||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
return this.signinService.signin(request, reply, account as ILocalUser);
|
||||
return this.signinService.signin(request, reply, account as LocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import Ajv from 'ajv';
|
||||
import type { Schema, SchemaType } from '@/misc/schema.js';
|
||||
import type { CacheableLocalUser } from '@/models/entities/User.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import { ApiError } from './error.js';
|
||||
import type { IEndpointMeta } from './endpoints.js';
|
||||
@ -21,16 +21,16 @@ type File = {
|
||||
|
||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||
type executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
|
||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
|
||||
constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
let cleanup: undefined | (() => void) = undefined;
|
||||
|
||||
if (meta.requireFile) {
|
||||
|
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
this.queueService.deliver(actor, this.apRendererService.renderActivity(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox);
|
||||
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox);
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
|
@ -3,7 +3,7 @@ import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, NotesRepository } from '@/models/index.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { CacheableLocalUser, User } from '@/models/entities/User.js';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
|
||||
import type { SchemaType } from '@/misc/schema.js';
|
||||
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||
@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
* URIからUserかNoteを解決する
|
||||
*/
|
||||
@bindThis
|
||||
private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
private async fetchAny(uri: string, me: LocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
// ブロックしてたら中断
|
||||
const fetchedMeta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
|
||||
@ -147,7 +147,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
|
||||
private async mergePack(me: LocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
|
||||
if (user != null) {
|
||||
return {
|
||||
type: 'User',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Not } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
@ -160,9 +160,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
// リモート投票の場合リプライ送信
|
||||
if (note.userHost != null) {
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser;
|
||||
|
||||
this.queueService.deliver(me, this.apRendererService.renderActivity(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox);
|
||||
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox);
|
||||
}
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
|
@ -45,6 +45,12 @@ export const meta = {
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b',
|
||||
},
|
||||
|
||||
tooManyUsers: {
|
||||
message: 'You can not push users any more.',
|
||||
code: 'TOO_MANY_USERS',
|
||||
id: '2dd9752e-a338-413d-8eec-41814430989b',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -110,8 +116,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.alreadyAdded);
|
||||
}
|
||||
|
||||
// Push the user
|
||||
try {
|
||||
await this.userListService.push(user, userList, me);
|
||||
} catch (err) {
|
||||
if (err instanceof UserListService.TooManyUsersError) {
|
||||
throw new ApiError(meta.errors.tooManyUsers);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js';
|
||||
import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js';
|
||||
import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
|
||||
import { MessagingService } from '@/core/MessagingService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
@ -89,7 +89,7 @@ class MessagingChannel extends Channel {
|
||||
// リモートユーザーからのメッセージだったら既読配信
|
||||
if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) {
|
||||
this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => {
|
||||
if (message) this.messagingService.deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message);
|
||||
if (message) this.messagingService.deliverReadActivity(this.user as LocalUser, this.otherparty as RemoteUser, message);
|
||||
});
|
||||
}
|
||||
} else if (this.groupId) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import summaly from 'summaly';
|
||||
import { summaly } from 'summaly';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@ -30,7 +30,7 @@ export class UrlPreviewService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private wrap(url?: string): string | null {
|
||||
private wrap(url?: string | null): string | null {
|
||||
return url != null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `${this.config.mediaProxy}/preview.webp?${query({
|
||||
@ -64,12 +64,19 @@ export class UrlPreviewService {
|
||||
? `(Proxy) Getting preview of ${url}@${lang} ...`
|
||||
: `Getting preview of ${url}@${lang} ...`);
|
||||
try {
|
||||
const summary = meta.summalyProxy ? await this.httpRequestService.getJson<ReturnType<typeof summaly.default>>(`${meta.summalyProxy}?${query({
|
||||
const summary = meta.summalyProxy ?
|
||||
await this.httpRequestService.getJson<ReturnType<typeof summaly>>(`${meta.summalyProxy}?${query({
|
||||
url: url,
|
||||
lang: lang ?? 'ja-JP',
|
||||
})}`) : await summaly.default(url, {
|
||||
})}`)
|
||||
:
|
||||
await summaly(url, {
|
||||
followRedirects: false,
|
||||
lang: lang ?? 'ja-JP',
|
||||
agent: {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
https: this.httpRequestService.httpsAgent,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user