Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
아르페 2023-10-31 14:47:51 +09:00
commit f2595b12cc
No known key found for this signature in database
GPG key ID: B1EFBBF5C93FF78F
204 changed files with 6407 additions and 2240 deletions

View file

@ -47,13 +47,13 @@ Please include errors from the developer console and/or server log files if you
* Server URL:
<!-- Example: kokonect.link -->
* CherryPick:
13.x.x-cp-4.x.x
4.x.x (Misskey: 2023.x.x)
### 🛰 Backend (for server admin)
<!-- If you are using a managed service, put that after the version. -->
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "CherryPick install shell script", development environment -->
* CherryPick: 13.x.x-cp-4.x.x
* CherryPick: 4.x.x (Misskey: 2023.x.x)
* Node: 20.x.x
* PostgreSQL: 15.x.x
* Redis: 7.x.x

View file

@ -9,7 +9,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- run: corepack enable

View file

@ -10,7 +10,7 @@ jobs:
check_copyright_year:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
- run: |
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
echo "Please change copyright year!"

View file

@ -17,7 +17,6 @@ jobs:
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: false
# All of these default to true, but feel free to set to "false" if necessary for your workflow
android: true
dotnet: true
@ -25,7 +24,7 @@ jobs:
large-packages: true
swap-storage: true
- name: Check out the repo
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.0.0

View file

@ -16,7 +16,6 @@ jobs:
with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB
tool-cache: false
# All of these default to true, but feel free to set to "false" if necessary for your workflow
android: true
dotnet: true
@ -24,7 +23,7 @@ jobs:
large-packages: true
swap-storage: true
- name: Check out the repo
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.0.0

View file

@ -14,7 +14,7 @@ jobs:
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb

186
.github/workflows/get-api-diff.yml vendored Normal file
View file

@ -0,0 +1,186 @@
# this name is used in report-api-diff.yml so be careful when change name
name: Get api.json from CherryPick
on:
pull_request:
branches:
- master
- develop
jobs:
get-base:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node-version: [20.5.1]
services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_DB: cherrypick
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-cherrypick-user
POSTGRESS_PASS: example-cherrypick-pass
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4.1.1
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.base_ref }}
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.8.1
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .config/example.yml .config/default.yml
- name: Build
run: pnpm build
- name : Migrate
run: pnpm migrate
- name: Launch cherrypick
run: |
screen -S cherrypick -dm pnpm run dev
sleep 30s
- name: Wait for CherryPick to be ready
run: |
MAX_RETRIES=12
RETRY_DELAY=5
count=0
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
printf '.'
sleep $RETRY_DELAY
count=$((count + 1))
done
if [[ $count -eq $MAX_RETRIES ]]; then
echo "Failed to connect to CherryPick after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from CherryPick
run: |
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
echo $RESULT > api-base.json
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: api-base.json
- name: Kill CherryPick Job
run: screen -S cherrypick -X quit
get-head:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node-version: [20.5.1]
services:
db:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_DB: cherrypick
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: example-cherrypick-user
POSTGRESS_PASS: example-cherrypick-pass
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4.1.1
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.head_ref }}
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.8.1
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .config/example.yml .config/default.yml
- name: Build
run: pnpm build
- name : Migrate
run: pnpm migrate
- name: Launch cherrypick
run: |
screen -S cherrypick -dm pnpm run dev
sleep 30s
- name: Wait for CherryPick to be ready
run: |
MAX_RETRIES=12
RETRY_DELAY=5
count=0
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
printf '.'
sleep $RETRY_DELAY
count=$((count + 1))
done
if [[ $count -eq $MAX_RETRIES ]]; then
echo "Failed to connect to CherryPick after $MAX_RETRIES attempts."
exit 1
fi
- id: fetch
name: Get api.json from CherryPick
run: |
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
echo $RESULT > api-head.json
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: api-head.json
- name: Kill CherryPick Job
run: screen -S cherrypick -X quit
save-pr-number:
runs-on: ubuntu-latest
steps:
- name: Save PR number
env:
PR_NUMBER: ${{ github.event.number }}
run: |
echo "$PR_NUMBER" > ./pr_number
- uses: actions/upload-artifact@v3
with:
name: api-artifact
path: pr_number

View file

@ -11,7 +11,7 @@ jobs:
pnpm_install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
@ -38,7 +38,7 @@ jobs:
- sw
- cherrypick-js
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true
@ -64,7 +64,7 @@ jobs:
- backend
- cherrypick-js
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
submodules: true

View file

@ -53,7 +53,7 @@ jobs:
# Check out merge commit
- name: Fork based /deploy checkout
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'

85
.github/workflows/report-api-diff.yml vendored Normal file
View file

@ -0,0 +1,85 @@
name: Report API Diff
on:
workflow_run:
types: [completed]
workflows:
- Get api.json from CherryPick # get-api-diff.yml
jobs:
compare-diff:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
pull-requests: write
# api-artifact
steps:
- name: Download artifact
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "api-artifact"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data));
- name: Extract artifact
run: unzip api-artifact.zip -d artifacts
- name: Load PR Number
id: load-pr-num
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
- name: Output base
run: cat ./artifacts/api-base.json
- name: Output head
run: cat ./artifacts/api-head.json
- name: Arrange json files
run: |
jq '.' ./artifacts/api-base.json > ./api-base.json
jq '.' ./artifacts/api-head.json > ./api-head.json
- name: Get diff of 2 files
run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff
- name: Get full diff
run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff
- name: Echo full diff
run: cat ./api-full.json.diff
- name: Upload full diff to Artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: |
api-full.json.diff
api-base.json
api-head.json
- id: out-diff
name: Build diff Comment
run: |
cat <<- EOF > ./output.md
이 PR에 의한 api.json 차이
<details>
<summary>차이점은 여기에서 볼 수 있음</summary>
\`\`\`diff
$(cat ./api.json.diff)
\`\`\`
</details>
[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
EOF
- uses: thollander/actions-comment-pull-request@v2
with:
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
comment_tag: show_diff
filePath: ./output.md

View file

@ -29,7 +29,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm

View file

@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- run: corepack enable

View file

@ -16,7 +16,7 @@ jobs:
node-version: [20.5.1]
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
@ -68,7 +68,7 @@ jobs:
- 56312:6379
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150

View file

@ -19,7 +19,7 @@ jobs:
node-version: [20.5.1]
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm

View file

@ -12,25 +12,51 @@
-->
## 2023.x.x (unreleased)
## 2023.11.0 (unreleased)
### General
- Feat: アイコンデコレーション機能
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
### Client
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
- Feat: AiScript関数`Mk:nyaize()`が追加されました
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
### Server
- Enhance: RedisへのTLのキャッシュをオフにできるように
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正
- Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正
- Fix: RedisへのTLキャッシュが有効の場合にHTL/LTL/STLが空になることがある問題を修正
- Fix: STLでフォローしていないチャンネルが取得される問題を修正
## 2023.10.2
### General
- Feat: アンテナでローカルの投稿のみ収集できるようになりました
- Feat: サーバーサイレンス機能が追加されました
- Enhance: 新規にフォローした人の返信をデフォルトでTLに追加できるオプションを追加
- Enhance: HTLとLTLを2023.10.0アップデート以前まで遡れるように
- Enhance: HTL/LTL/STLを2023.10.0アップデート以前まで遡れるように
- Enhance: フォロー/フォロー解除したときに過去分のHTLにも含まれる投稿が反映されるように
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
### Client
- Enhance: TLの返信表示オプションを記憶するように
- Enhance: 投稿されてから時間が経過しているノートであることを視覚的に分かりやすく
- Feat: 絵文字ピッカーのカテゴリに「/」を入れることでフォルダ分け表示できるように
### Server
- Enhance: タイムライン取得時のパフォーマンスを向上
- Enhance: ストリーミングAPIのパフォーマンスを向上
- Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正
- Fix: コントロールパネルの設定項目が正しく保存できない問題を修正
- Change: nyaizeはAPIレスポンス時ではなく投稿時に一度だけ非可逆的に行われるようになりました
- Fix: 管理者権限のロールを持っていても一部のAPIが使用できないことがある問題を修正
- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました
- isCatな場合、クライアントでnyaize処理を行うことを推奨します
## 2023.10.1
### General

View file

@ -23,13 +23,68 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGE
# 릴리즈 노트
이 문서는 CherryPick의 변경 사항만 포함합니다.
## 4.x.x
## 4.5.0
출시일: unreleased<br>
기반 Misskey 버전: 2023.x.x<br>
기반 Misskey 버전: 2023.11.0<br>
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023xx](CHANGELOG.md#2023xx) 문서를 참고하십시오.
### Client
- Feat: 본문 미리보기의 프로필을 표시하지 않도록 설정할 수 있음
- Feat: 스와이프하여 타임라인을 다시 불러올 수 있음 (misskey-dev/misskey#12113)
- PC의 경우 오른쪽 상단의 버튼을 통해서도 다시 불러올 수 있습니다
- Feat: 타임라인 자동 업데이트를 비활성화할 수 있음 (misskey-dev/misskey#12113)
- Feat: 이모티콘 피커의 카테고리를 다중 계층 폴더로 분류할 수 있음 (misskey-dev/misskey#12132)
- Feat: AiScript 함수 `Mk:nyaize()`가 추가됨 (misskey-dev/misskey#12136)
- Enhance: 노트 작성 폼에서 노트를 게시한 뒤에 textarea의 높이를 원래대로 되돌림
- Enhance: 노트 상세 페이지의 답글 목록 개선
- Enhance: 유저 페이지 개선
- 요약 탭의 하이라이트를 제거 & 노트 탭으로 하이라이트를 이동
- 요약 탭의 리액션을 제거 & 노트 탭으로 리액션을 이동
- chore: 이모티콘 이름 필드에서 autocapitalize를 끄기 (misskey-dev/misskey#12139)
- Fix: 외부 리소스 설치 페이지에서 페이지 캐시가 작동하는 문제 수정 (misskey-dev/misskey#12105)
- Fix: 채널 생성/업데이트 시 실패하면 아무 것도 표시되지 않는 문제 수정 misskey-dev/misskey#11983 (misskey-dev/misskey#12142)
- Fix: 유저 페이지의 미디어 타임라인에서 미디어가 없는 답글이 표시됨 #388
- Fix: Friendly UI가 아닌 경우 헤더 디자인이 잘못 표시되는 문제
- 헤더의 액션 항목이 여러 개 일 때 왼쪽으로 타이틀이 치우칠 수 있음
- 특정 조건에서 헤더의 왼쪽에 여백이 발생할 수 있음
- 일부 페이지에서 잘못된 디자인이 표시됨
- 일부 페이지에서 액션 항목이 존재해도 버튼이 표시되지 않을 수 있음
- Fix: 네비게이션 메뉴의 하단 프로필 영역이 잘못된 디자인으로 표시됨
### Server
- Feat: 연합에서 노트 수정이 반영됨 (libnare/cp-castella#1)
- Enhance: 사용자 차단 개선 (Renote Part) (misskey-dev/misskey#12089)
- Fix: 장시간 기다려도 마이그레이션이 완료되지 않을 수 있음
- Fix: Redis 에서 TL 캐시를 반환하지 않으면 '고양이만 보기'가 작동하지 않을 수 있음
---
## 4.4.1
출시일: 2023/10/21<br>
기반 Misskey 버전: 2023.10.2<br>
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2023102](CHANGELOG.md#2023102) 문서를 참고하십시오.
> 버전 관리 방식이 변경되었기 때문에, 기존 버전보다 낮은 것으로 인식되어 업데이트 대화 상자가 표시되지 않을 수 있습니다.
> 또한, 일부 locale이 누락되거나 기능이 정상적으로 작동하지 않는 등의 문제가 발생할 수 있습니다.
> 문제가 발생하면 '설정 - 캐시 비우기'를 진행하거나, 브라우저 캐시를 삭제하십시오.
### Client
- Feat: 노트 편집 시 토스트 알림을 표시하고 사운드를 재생
- Feat: PostForm 접두사에 현재 공개 범위 표시 ([tanukey-dev/tanukey@1cc0071](https://github.com/tanukey-dev/tanukey/commit/1cc0071bbd424949d9305bcec554f5d755a73554))
- Feat: 플러그인 및 테마를 외부 사이트에서 직접 설치할 수 있음 (misskey-dev/misskey#12034)
- 외부 사이트에서 구현해야 합니다. 자세한 내용은 Misskey Hub를 참조하세요.
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
- Enhance: 노트를 편집할 때 편집 중인 노트임을 강조함
- Enhance: 타임라인에서 새 노트가 20개 이상이면 '20+'로 표기
- Enhance: 노트를 편집한 시간이 개별적으로 표시됨
- Fix: '새 노트 알림'을 '노트 수 표시'로 설정했을 때 한국어 이외의 언어에서 내용이 표시되지 않음
- Fix: 알림에 여는 인용문 사용 (misskey-dev/misskey#12082)
### Server
- Enhance: '내용 숨기기'로 설정된 노트의 주석도 노트 편집 기록에 표시됨
- Revert: Perf: 부팅 시 MeiliSearch 설정을 업데이트하지 마십시오 (MisskeyIO/misskey#158)
- Fix: 이모지를 여러 개 추가할 때도 이름의 중복을 확인하도록
- Fix: 유저 페이지 및 이벤트 검색에서 '미래순'으로 정렬할 수 없음
---

View file

@ -1,3 +1,4 @@
/* flaky
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
@ -67,3 +68,4 @@ describe('After user signed in', () => {
buildWidgetTest('aiscript');
buildWidgetTest('aichan');
});
*/

View file

@ -1,5 +1,7 @@
---
_lang_: "English"
previewNoteProfile: "Show profile"
noteEdited: "Note are now edited."
removeModalBgColorForBlur: "Remove modal background color"
skipThisVersion: "Skip this release"
enableReceivePrerelease: "Get notified of pre-release versions"
@ -365,6 +367,7 @@ folderName: "Folder name"
createFolder: "Create a folder"
renameFolder: "Rename this folder"
deleteFolder: "Delete this folder"
folder: "Folder"
addFile: "Add a file"
emptyDrive: "Your Drive is empty"
emptyFolder: "This folder is empty"
@ -604,6 +607,7 @@ showFixedPostForm: "Display the posting form at the top of the timeline"
showFixedPostFormInChannel: "Display the posting form at the top of the timeline (Channels)"
withRepliesByDefaultForNewlyFollowed: "Include replies by newly followed users in the timeline by default"
newNoteRecived: "There are new notes"
newNoteRecivedCount: "There are {n} new notes"
sounds: "Sounds"
sound: "Sounds"
vibrations: "Vibrations"
@ -665,7 +669,7 @@ poll: "Poll"
useCw: "Hide content"
enablePlayer: "Open video player"
disablePlayer: "Close video player"
expandTweet: "Expand tweet"
expandTweet: "Expand post"
themeEditor: "Theme editor"
description: "Description"
describeFile: "Add caption"
@ -1223,6 +1227,10 @@ impressumDescription: "In some countries, like germany, the inclusion of operato
privacyPolicy: "Privacy Policy"
privacyPolicyUrl: "Privacy Policy URL"
tosAndPrivacyPolicy: "Terms of Service and Privacy Policy"
releaseToRefresh: "Release to refresh"
refreshing: "Loading..."
pullDownToRefresh: "Pull down to refresh"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
showUnreadNotificationCount: "Show the number of unread notifications"
showCatOnly: "Show only cats"
additionalPermissionsForFlash: "Allow to add permission to Play"
@ -1977,6 +1985,7 @@ _theme:
_sfx:
note: "New note"
noteMy: "Own note"
noteEdited: "Note Edited"
notification: "Notifications"
chat: "Chat"
chatBg: "Chat (Background)"

View file

@ -387,7 +387,7 @@ antennaSource: "Source de lantenne"
antennaKeywords: "Mots clés à recevoir"
antennaExcludeKeywords: "Mots clés à exclure"
antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
notifyAntenna: "Je souhaite recevoir les notifications des nouvelles notes"
notifyAntenna: "Me notifier pour les nouvelles notes"
withFileAntenna: "Notes ayant des attachements uniquement"
enableServiceworker: "Activer ServiceWorker"
antennaUsersDescription: "Saisissez un seul nom dutilisateur·rice par ligne"
@ -999,11 +999,13 @@ internalServerError: "Erreur interne du serveur"
copyErrorInfo: "Copier les détails de lerreur"
exploreOtherServers: "Trouver une autre instance"
disableFederationOk: "Désactiver"
postToTheChannel: "Publier au canal"
likeOnly: "Les favoris uniquement"
sensitiveWords: "Mots sensibles"
notesSearchNotAvailable: "La recherche de notes n'est pas disponible."
license: "Licence"
myClips: "Mes clips"
retryAllQueuesConfirmText: "Cela peut augmenter temporairement la charge du serveur."
showClipButtonInNoteFooter: "Ajouter « Clip » au menu d'action de la note"
noteIdOrUrl: "Identifiant de la note ou URL"
video: "Vidéo"
@ -1027,21 +1029,30 @@ vertical: "Vertical"
horizontal: "Latéral"
position: "Position"
serverRules: "Règles du serveur"
preservedUsernames: "Nom d'utilisateur·rice réservé"
pleaseAgreeAllToContinue: "Pour continuer, veuillez accepter tous les champs ci-dessus."
continue: "Continuer"
preservedUsernames: "Noms d'utilisateur·rice réservés"
archive: "Archive"
displayOfNote: "Affichage de la note"
initialAccountSetting: "Réglage initial du profil"
youFollowing: "Abonné·e"
preventAiLearning: "Refuser l'usage dans l'apprentissage automatique d'IA générative"
options: "Options"
later: "Plus tard"
goToMisskey: "Retour vers CherryPick"
expirationDate: "Date dexpiration"
waitingForMailAuth: "En attente de la vérification de l'adresse courriel"
usedAt: "Utilisé le"
unused: "Non-utilisé"
used: "Utilisé"
expired: "Expiré"
doYouAgree: "Êtes-vous daccord ?"
beSureToReadThisAsItIsImportant: "Assurez-vous de le lire; c'est important."
dialog: "Dialogue"
icon: "Avatar"
forYou: "Pour vous"
currentAnnouncements: "Annonces actuelles"
pastAnnouncements: "Annonces passées"
replies: "Répondre"
renotes: "Renoter"
loadReplies: "Inclure les réponses"

View file

@ -1 +1,5 @@
---
_lang_: "japanski"
ok: "OK"
gotIt: "Razumijem"
cancel: "otkazati"

View file

@ -1 +1,18 @@
---
_lang_: "Japonè"
password: "modpas"
ok: "OK"
gotIt: "Konprann"
cancel: "anile"
noThankYou: "Sispann"
instance: "sèvè"
profile: "pwofil"
save: "kenbe"
delete: "efase"
instances: "sèvè"
remove: "efase"
smtpPass: "modpas"
_2fa:
renewTOTPCancel: "Sispann"
_widgets:
profile: "pwofil"

79
locales/index.d.ts vendored
View file

@ -3,6 +3,8 @@
// Do not edit this file directly.
export interface Locale {
"_lang_": string;
"previewNoteProfile": string;
"noteEdited": string;
"removeModalBgColorForBlur": string;
"skipThisVersion": string;
"enableReceivePrerelease": string;
@ -368,6 +370,7 @@ export interface Locale {
"createFolder": string;
"renameFolder": string;
"deleteFolder": string;
"folder": string;
"addFile": string;
"emptyDrive": string;
"emptyFolder": string;
@ -607,6 +610,7 @@ export interface Locale {
"showFixedPostFormInChannel": string;
"withRepliesByDefaultForNewlyFollowed": string;
"newNoteRecived": string;
"newNoteRecivedCount": string;
"sounds": string;
"sound": string;
"vibrations": string;
@ -1219,6 +1223,10 @@ export interface Locale {
"fileAttachedOnly": string;
"showRepliesToOthersInTimeline": string;
"hideRepliesToOthersInTimeline": string;
"showRepliesToOthersInTimelineAll": string;
"hideRepliesToOthersInTimelineAll": string;
"confirmShowRepliesAll": string;
"confirmHideRepliesAll": string;
"externalServices": string;
"impressum": string;
"impressumUrl": string;
@ -1226,6 +1234,16 @@ export interface Locale {
"privacyPolicy": string;
"privacyPolicyUrl": string;
"tosAndPrivacyPolicy": string;
"avatarDecorations": string;
"attach": string;
"detach": string;
"angle": string;
"flip": string;
"showAvatarDecorations": string;
"releaseToRefresh": string;
"refreshing": string;
"pullDownToRefresh": string;
"disableStreamingTimeline": string;
"showUnreadNotificationCount": string;
"showCatOnly": string;
"additionalPermissionsForFlash": string;
@ -1369,6 +1387,7 @@ export interface Locale {
"manifestJsonOverride": string;
"shortName": string;
"shortNameDescription": string;
"fanoutTimelineDescription": string;
};
"_accountMigration": {
"moveFrom": string;
@ -2104,6 +2123,7 @@ export interface Locale {
"_sfx": {
"note": string;
"noteMy": string;
"noteEdited": string;
"notification": string;
"chat": string;
"chatBg": string;
@ -2589,6 +2609,9 @@ export interface Locale {
"createAd": string;
"deleteAd": string;
"updateAd": string;
"createAvatarDecoration": string;
"updateAvatarDecoration": string;
"deleteAvatarDecoration": string;
};
"_fileViewer": {
"title": string;
@ -2626,6 +2649,62 @@ export interface Locale {
"resizeCompressLossy": string;
"noResizeCompressLossy": string;
};
"_externalResourceInstaller": {
"title": string;
"checkVendorBeforeInstall": string;
"_plugin": {
"title": string;
"metaTitle": string;
};
"_theme": {
"title": string;
"metaTitle": string;
};
"_meta": {
"base": string;
};
"_vendorInfo": {
"title": string;
"endpoint": string;
"hashVerify": string;
"hashVerified": string;
};
"_errors": {
"_invalidParams": {
"title": string;
"description": string;
};
"_resourceTypeNotSupported": {
"title": string;
"description": string;
};
"_failedToFetch": {
"title": string;
"fetchErrorDescription": string;
"parseErrorDescription": string;
};
"_hashUnmatched": {
"title": string;
"description": string;
};
"_pluginParseFailed": {
"title": string;
"description": string;
};
"_pluginInstallFailed": {
"title": string;
"description": string;
};
"_themeParseFailed": {
"title": string;
"description": string;
};
"_themeInstallFailed": {
"title": string;
"description": string;
};
};
};
}
declare const locales: {
[lang: string]: Locale;

View file

@ -110,7 +110,7 @@ unrenote: "Elimina la Rinota"
renoted: "Rinotato!"
cantRenote: "È impossibile rinotare questa nota."
cantReRenote: "È impossibile rinotare una Rinota."
quote: "Cita"
quote: "Citazione"
inChannelRenote: "Rinota nel canale"
inChannelQuote: "Cita nel canale"
pinnedNote: "Nota in primo piano"
@ -546,6 +546,7 @@ serverLogs: "Log del server"
deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline"
withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita"
newNoteRecived: "Nuove note da leggere"
sounds: "Impostazioni suoni"
sound: "Suono"
@ -1149,7 +1150,7 @@ externalServices: "Servizi esterni"
impressum: "Dichiarazione di proprietà"
impressumUrl: "URL della dichiarazione di proprietà"
impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)."
privacyPolicy: "Informativa ai sensi degli artt. 13 e 14 del Regolamento UE 2016/679 per la protezione dei dati personali (GDPR)"
privacyPolicy: "Informativa privacy ai sensi del Regolamento UE 2016/679 (GDPR)"
privacyPolicyUrl: "URL della informativa privacy"
tosAndPrivacyPolicy: "Condizioni d'uso e informativa privacy"
_announcement:
@ -2007,6 +2008,7 @@ _exportOrImport:
userLists: "Liste"
excludeMutingUsers: "Escludere gli utenti silenziati"
excludeInactiveUsers: "Escludere i profili inutilizzati"
withReplies: "Includere le risposte da profili importati nella Timeline"
_charts:
federation: "Federazione"
apRequest: "Richieste"
@ -2176,7 +2178,7 @@ _deck:
list: "Liste"
channel: "Canale"
mentions: "Menzioni"
direct: "Diretta"
direct: "Note Dirette"
roleTimeline: "Timeline Ruolo"
_dialog:
charactersExceeded: "Hai superato il limite di {max} caratteri! ({corrente})"

View file

@ -1,5 +1,7 @@
_lang_: "日本語"
previewNoteProfile: "プロフィールを表示"
noteEdited: "ノートを編集しました。"
removeModalBgColorForBlur: "モーダル背景色を削除"
skipThisVersion: "このリリースをスキップする"
enableReceivePrerelease: "プレリリース版の通知を受け取る"
@ -365,6 +367,7 @@ folderName: "フォルダー名"
createFolder: "フォルダーを作成"
renameFolder: "フォルダー名を変更"
deleteFolder: "フォルダーを削除"
folder: "フォルダー"
addFile: "ファイルを追加"
emptyDrive: "ドライブは空です"
emptyFolder: "フォルダーは空です"
@ -604,6 +607,7 @@ showFixedPostForm: "タイムライン上部に投稿フォームを表示する
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
withRepliesByDefaultForNewlyFollowed: "フォローする際、デフォルトで返信をTLに含むようにする"
newNoteRecived: "新しいノートがあります"
newNoteRecivedCount: "{n}個の新しいノートがあります"
sounds: "サウンド"
sound: "サウンド"
vibrations: "振動"
@ -1216,6 +1220,10 @@ mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする"
hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする"
confirmShowRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか"
confirmHideRepliesAll: "この操作は元の戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか"
externalServices: "外部サービス"
impressum: "運営者情報"
impressumUrl: "運営者情報URL"
@ -1223,6 +1231,16 @@ impressumDescription: "ドイツなどの一部の国と地域では表示が義
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
avatarDecorations: "アイコンデコレーション"
attach: "付ける"
detach: "外す"
angle: "角度"
flip: "反転"
showAvatarDecorations: "アイコンのデコレーションを表示"
releaseToRefresh: "離してリロード"
refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
showUnreadNotificationCount: "未読の通知の数を表示する"
showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可"
@ -1366,6 +1384,7 @@ _serverSettings:
manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。"
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
@ -2018,6 +2037,7 @@ _theme:
_sfx:
note: "ノート"
noteMy: "ノート(自分)"
noteEdited: "ノートを編集"
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
@ -2499,6 +2519,9 @@ _moderationLogTypes:
createAd: "広告を作成"
deleteAd: "広告を削除"
updateAd: "広告を更新"
createAvatarDecoration: "アイコンデコレーションを作成"
updateAvatarDecoration: "アイコンデコレーションを更新"
deleteAvatarDecoration: "アイコンデコレーションを削除"
_fileViewer:
title: "ファイルの詳細"
@ -2534,3 +2557,46 @@ _imageCompressionMode:
noResizeCompress: "縮小せず再圧縮する"
resizeCompressLossy: "縮小して非可逆圧縮する"
noResizeCompressLossy: "縮小せず非可逆圧縮する"
_externalResourceInstaller:
title: "外部サイトからインストール"
checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
_plugin:
title: "このプラグインをインストールしますか?"
metaTitle: "プラグイン情報"
_theme:
title: "このテーマをインストールしますか?"
metaTitle: "テーマ情報"
_meta:
base: "基本のカラースキーム"
_vendorInfo:
title: "配布元情報"
endpoint: "参照したエンドポイント"
hashVerify: "ファイル整合性の確認"
hashVerified: "確認済み"
_errors:
_invalidParams:
title: "パラメータが不足しています"
description: "外部サイトからデータを取得するために必要な情報が不足しています。URLをお確かめください。"
_resourceTypeNotSupported:
title: "この外部リソースには対応していません"
description: "この外部サイトから取得したリソースの種別には対応していません。サイト管理者にお問い合わせください。"
_failedToFetch:
title: "データの取得に失敗しました"
fetchErrorDescription: "外部サイトとの通信に失敗しました。もう一度試しても改善しない場合、サイト管理者にお問い合わせください。"
parseErrorDescription: "外部サイトから取得したデータが読み取れませんでした。サイト管理者にお問い合わせください。"
_hashUnmatched:
title: "正しいデータが取得できませんでした"
description: "提供されたデータの整合性の確認に失敗しました。セキュリティ上、インストールは続行できません。サイト管理者にお問い合わせください。"
_pluginParseFailed:
title: "AiScript エラー"
description: "データは取得できたものの、AiScriptの解析時にエラーがあったため読み込めませんでした。プラグインの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。"
_pluginInstallFailed:
title: "プラグインのインストールに失敗しました"
description: "プラグインのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"
_themeParseFailed:
title: "テーマ解析エラー"
description: "データは取得できたものの、テーマファイルの解析時にエラーがあったため読み込めませんでした。テーマの作者にお問い合わせください。エラーの詳細はJavascriptコンソールをご確認ください。"
_themeInstallFailed:
title: "テーマのインストールに失敗しました"
description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。"

View file

@ -45,6 +45,7 @@ pin: "ピン留めしとく"
unpin: "やっぱピン留めせん"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
copyLinkRenote: "リノートのリンクをコピーするで?"
delete: "ほかす"
deleteAndEdit: "ほかして直す"
deleteAndEditConfirm: "このートをほかしてもっかい直すこのートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん"
@ -194,6 +195,7 @@ perHour: "1時間ごと"
perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送をやめる"
blockThisInstance: "このサーバーをブロックすんで"
silenceThisInstance: "サーバーサイレンスすんで?"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
@ -213,6 +215,8 @@ clearCachedFiles: "キャッシュをほかす"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
silencedInstances: "サーバーサイレンスされてんねん"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定すんで。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなんねん。ブロックしたインスタンスには影響せーへんで。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
@ -410,12 +414,14 @@ aboutMisskey: "CherryPickってなんや"
administrator: "管理者"
token: "トークン"
2fa: "二要素認証"
setupOf2fa: "二要素認証のセットアップ"
totp: "認証アプリ"
totpDescription: "認証アプリ使うてワンタイムパスワードを入れる"
moderator: "モデレーター"
moderation: "モデレーション"
moderationNote: "モデレーションノート"
addModerationNote: "モデレーションノートを追加するで"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿"
securityKeyAndPasskey: "セキュリティキー・パスキー"
securityKey: "セキュリティキー"
@ -540,6 +546,7 @@ serverLogs: "サーバーログ"
deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
showFixedPostFormInChannel: "タイムラインの上の方で投稿できるようにするわ(チャンネル)"
withRepliesByDefaultForNewlyFollowed: "フォローする時、デフォルトで返信をタイムラインに含むようにしよか"
newNoteRecived: "新しいノートがあるで"
sounds: "サウンド"
sound: "サウンド"
@ -598,7 +605,7 @@ poll: "アンケート"
useCw: "内容を隠す"
enablePlayer: "プレイヤーを開く"
disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する"
expandTweet: "ポストを展開する"
themeEditor: "テーマエディター"
description: "説明"
describeFile: "キャプションを付ける"
@ -667,6 +674,7 @@ behavior: "動作"
sample: "サンプル"
abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノート苦情だすで?"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "細かい通報理由を書いてなー。対象ートがある時はそのURLも書いといてなー。"
abuseReported: "無事内容が送信されたみたいやで。おおきに〜。"
@ -719,6 +727,7 @@ lockedAccountInfo: "フォローを承認制にしとっても、ノートの公
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで"
loadRawImages: "添付画像のサムネイルをオリジナル画質にするで"
disableShowingAnimatedImages: "アニメーション画像を再生せんとくで"
highlightSensitiveMedia: "メディアがセンシティブなことをめっっちゃわかりやすく表紙"
verificationEmailSent: "無事確認のメールを送れたで。メールに書いてあるリンクにアクセスして、設定を完了してなー。"
notSet: "未設定"
emailVerified: "メールアドレスは確認されたで"
@ -1035,6 +1044,7 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
reactionsDisplaySize: "リアクションの表示のでかさ"
noteIdOrUrl: "ートIDかURL"
video: "動画"
videos: "動画"
@ -1121,8 +1131,28 @@ replies: "返事"
renotes: "Renote"
loadReplies: "返信を見るで"
loadConversation: "会話を見るで"
pinnedList: "ピン留めしはったリスト"
keepScreenOn: "デバイスの画面を常にオンにすんで"
verifiedLink: "このリンク先の所有者であることが確認されたで。"
notifyNotes: "投稿を通知"
unnotifyNotes: "投稿の通知を解除すんで"
authentication: "認証"
authenticationRequiredToContinue: "続けるには認証をやってや。"
dateAndTime: "日時"
showRenotes: "リノートを表示"
edited: "編集し終わってる"
notificationRecieveConfig: "通知を受け取るかの設定"
mutualFollow: "お互いフォローしてんで"
fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "タイムラインに他の人への返信とかも含めんで"
hideRepliesToOthersInTimeline: "タイムラインに他の人への返信とかは見ーへんで"
externalServices: "他のサイトのサービス"
impressum: "運営者の情報"
impressumUrl: "運営者の情報URL"
impressumDescription: "ドイツなどのほんま1部の国と地域ではな、表示が義務付けられててん。(Impressum)"
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
_announcement:
forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
@ -1155,6 +1185,8 @@ _serverSettings:
appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。"
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。"
manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
@ -1410,6 +1442,9 @@ _achievements:
title: "Brain Diver"
description: "Brain Diverへのリンクを投稿したった"
flavor: "CherryPick-CherryPick La-Tu-Ma"
_smashTestNotificationButton:
title: "テスト過剰"
description: "通知テストをごく短時間のうちに連続して行ったねん"
_role:
new: "ロールの作成"
edit: "ロールの編集"
@ -1467,6 +1502,7 @@ _role:
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか"
canUseTranslator: "翻訳機能の利用"
_condition:
isLocal: "ローカルユーザー"
isRemote: "リモートユーザー"
@ -1515,6 +1551,10 @@ _ad:
reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで"
hide: "表示せん"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されるで。"
adsSettings: "広告配信設定"
notesPerOneAd: "リアタイ更新中に広告を出す間隔(ノートの個数な)"
setZeroToDisable: "0でリアタイ更新時の広告配信を無効にすんで"
adsTooClose: "広告を出す間隔がめっちゃ短いから、ユーザー体験が著しく損なわれる可能性があんで。"
_forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
@ -1782,6 +1822,7 @@ _2fa:
step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。"
step2: "次に、ここにあるQRコードをアプリでスキャンしてな。"
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
step2Uri: "デスクトップアプリを使う時は次のURIを入れるで"
step3Title: "確認コードを入れてーや"
step3: "アプリに表示されているトークンを入力して終わりや。"
setupCompleted: "設定が完了したで。"
@ -1800,6 +1841,7 @@ _2fa:
renewTOTPOk: "もっかい設定する"
renewTOTPCancel: "やめとく"
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。"
backupCodes: "バックアップコード"
backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。"
backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。"
backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。"
@ -1856,6 +1898,7 @@ _antennaSources:
users: "選らんだ一人か複数のユーザーのノート"
userList: "選んだリストのユーザーのノート"
userGroup: "選んだグループのユーザーのノート"
userBlacklist: "選んだ1人か複数のユーザーのノート"
_weekday:
sunday: "日曜日"
monday: "月曜日"
@ -1955,6 +1998,7 @@ _profile:
metadataContent: "内容"
changeAvatar: "アバター画像を変更するで"
changeBanner: "バナー画像を変更するで"
verifiedLinkDescription: "内容をURLに設定すると、リンク先のwebサイトに自分のプロフのリンクが含まれてる場合に所有者確認済みアイコンを表示させることができるで。"
_exportOrImport:
allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート"
@ -1964,6 +2008,7 @@ _exportOrImport:
userLists: "リスト"
excludeMutingUsers: "ミュートしてるユーザーは入れんとくわ"
excludeInactiveUsers: "使われてなさそうなアカウントは入れんとくわ"
withReplies: "インポートした人による返信をTLに含むようにすんで。"
_charts:
federation: "連合"
apRequest: "リクエスト"
@ -2077,14 +2122,17 @@ _notification:
yourFollowRequestAccepted: "フォローさせてもろたで"
youWereInvitedToGroup: "グループに招待されとるで"
pollEnded: "アンケートの結果が出たみたいや"
newNote: "さらの投稿"
unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
achievementEarned: "実績を獲得しとるで"
testNotification: "通知テスト"
checkNotificationBehavior: "通知の表示を確かめるで"
sendTestNotification: "テスト通知を送信するで"
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
_types:
all: "すべて"
note: "あんたらの新規投稿"
follow: "フォロー"
mention: "メンション"
reply: "リプライ"
@ -2120,6 +2168,7 @@ _deck:
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで"
flexible: "幅を自動調整"
_columns:
main: "メイン"
widgets: "ウィジェット"
@ -2155,6 +2204,41 @@ _webhookSettings:
reaction: "ツッコミがあるとき~!"
mention: "メンションがあるとき~!"
_moderationLogTypes:
createRole: "ロールを追加すんで"
deleteRole: "ロールほかす"
updateRole: "ロールの更新すんで"
assignRole: "ロールへアサイン"
unassignRole: "ロールのアサインほかす"
suspend: "凍結"
unsuspend: "凍結解除"
addCustomEmoji: "自由な絵文字追加されたで"
updateCustomEmoji: "自由な絵文字更新されたで"
deleteCustomEmoji: "自由な絵文字消されたで"
updateServerSettings: "サーバー設定更新すんねん"
updateUserNote: "モデレーションノート更新"
deleteDriveFile: "ファイルをほかす"
deleteNote: "ノートを削除"
createGlobalAnnouncement: "みんなへの通告を作成したで"
createUserAnnouncement: "あんたらへの通告を作成したで"
updateGlobalAnnouncement: "みんなへの通告更新したったで"
updateUserAnnouncement: "あんたらへの通告更新したったで"
deleteGlobalAnnouncement: "みんなへの通告消したったで"
deleteUserAnnouncement: "あんたらへのお知らせを削除"
resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを止めんで"
unsuspendRemoteInstance: "リモートサーバーを再開すんで"
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "苦情を解決"
createInvitation: "招待コードを作成"
createAd: "広告を作んで"
deleteAd: "広告ほかす"
updateAd: "広告を更新"
_fileViewer:
title: "ファイルの詳しい情報"
type: "ファイルの種類"
size: "ファイルのでかさ"
url: "URL"
uploadedAt: "追加した日"
attachedNotes: "ファイルがついてきてるノート"
thisPageCanBeSeenFromTheAuthor: "このページはこのファイルをアップした人しか見れへんねん。"

View file

@ -1,5 +1,7 @@
---
_lang_: "한국어"
previewNoteProfile: "프로필 표시"
noteEdited: "노트를 편집했어요!"
removeModalBgColorForBlur: "모달 배경색 제거"
skipThisVersion: "이 릴리즈 건너뛰기"
enableReceivePrerelease: "출시 전 버전 알림 받기"
@ -362,6 +364,7 @@ folderName: "폴더명"
createFolder: "폴더 만들기"
renameFolder: "폴더 이름 바꾸기"
deleteFolder: "폴더 삭제"
folder: "폴더"
addFile: "파일 추가"
emptyDrive: "드라이브에 아무것도 없어요!"
emptyFolder: "폴더에 아무것도 없어요!"
@ -662,7 +665,7 @@ poll: "투표"
useCw: "내용 숨기기"
enablePlayer: "플레이어 열기"
disablePlayer: "플레이어 닫기"
expandTweet: "트윗 확장하기"
expandTweet: "게시물 확장하기"
themeEditor: "테마 에디터"
description: "설명"
describeFile: "캡션 추가"
@ -1209,6 +1212,10 @@ edited: "수정됨"
notificationRecieveConfig: "알림 설정"
mutualFollow: "맞팔로우"
fileAttachedOnly: "파일이 포함된 노트만"
releaseToRefresh: "놓아서 새로 고침"
refreshing: "새로 고침 중"
pullDownToRefresh: "당겨서 새로 고침"
disableStreamingTimeline: "타임라인 실시간 업데이트 비활성화"
showUnreadNotificationCount: "읽지 않은 알림 수 표시"
showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
@ -1954,6 +1961,7 @@ _theme:
_sfx:
note: "새 노트"
noteMy: "내 노트"
noteEdited: "노트 편집"
notification: "알림"
chat: "대화"
chatBg: "대화 (백그라운드)"
@ -2396,3 +2404,45 @@ _imageCompressionMode:
noResizeCompress: "해상도를 축소하지 않고 압축"
resizeCompressLossy: "해상도 축소 및 손실 압축"
noResizeCompressLossy: "해상도를 축소하지 않고 손실 압축"
_externalResourceInstaller:
title: "외부 사이트에서 설치"
checkVendorBeforeInstall: "배포처가 신뢰할 수 있는 곳인지 확인 후 설치하세요."
_plugin:
title: "이 플러그인을 설치할까요?"
metaTitle: "플러그인 정보"
_theme:
title: "이 테마를 설치할까요?"
metaTitle: "테마 정보"
_meta:
base: "기본 색 구성표"
_vendorInfo:
title: "배포처 정보"
endpoint: "참조된 엔드포인트"
hashVerify: "파일 무결성 확인"
hashVerified: "확인됨"
_errors:
_invalidParams:
title: "매개변수가 부족해요"
description: "외부 사이트에서 데이터를 가져오는 데 필요한 정보가 부족해요. URL을 확인해 주세요."
_resourceTypeNotSupported:
title: "이 외부 리소스는 지원되지 않아요"
description: "이 외부 사이트에서 가져온 리소스의 종류는 지원되지 않아요. 사이트 관리자에게 문의해 주세요."
_failedToFetch:
title: "데이터 수집에 실패했어요"
fetchErrorDescription: "외부 사이트와의 통신에 실패했어요. 다시 시도 하거나 사이트 관리자에게 문의해 주세요."
parseErrorDescription: "외부 사이트에서 가져온 데이터를 읽을 수 없어요. 사이트 관리자에게 문의해 주세요."
_hashUnmatched:
title: "올바른 데이터를 얻지 못했어요"
description: "제공된 데이터의 무결성을 확인하지 못했어요. 보안을 위해 설치가 중단되었어요. 사이트 관리자에게 문의해 주세요."
_pluginParseFailed:
title: "AiScript 오류"
description: "데이터는 가져올 수 있었으나, AiScript 구문 분석에서 오류가 발생해 불러오지 못했어요. 플러그인 제작자에게 문의해 주세요. 오류에 대한 자세한 내용은 자바스크립트 콘솔에서 확인할 수 있어요."
_pluginInstallFailed:
title: "플러그인 설치에 실패했어요"
description: "플러그인 설치 중 문제가 발생했어요. 다시 시도해 주세요. 오류에 대한 자세한 내용은 자바스크립트 콘솔을 참조해 주세요."
_themeParseFailed:
title: "테마 분석 오류"
description: "데이터는 가져올 수 있었으나, 테마 파일 분석 중 오류가 발생해 불러오지 못했어요. 테마 제작자에게 문의해 주세요. 오류에 대한 자세한 내용은 자바스크립트 콘솔에서 확인할 수 있어요."
_themeInstallFailed:
title: "테마 설치에 실패했어요"
description: "테마를 설치하는 동안 문제가 발생했어요. 다시 시도해 주세요. 오류에 대한 자세한 내용은 자바스크립트 콘솔을 참고해 주세요."

View file

@ -1830,8 +1830,8 @@ _2fa:
renewTOTPOk: "ตั้งค่าคอนฟิกใหม่"
renewTOTPCancel: "ไม่เป็นไร"
backupCodes: "รหัสสำรองข้อมูล"
backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีกต่อไป"
backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้วถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
backupCodeUsedWarning: "มีการใช้รหัสสำรองแล้ว โปรดกรุณากำหนดค่าการตรวจสอบสิทธิ์แบบสองปัจจัยโดยเร็วที่สุดถ้าหากคุณยังไม่สามารถใช้งานได้อีก"
backupCodesExhaustedWarning: "รหัสสำรองทั้งหมดถูกใช้แล้ว ถ้าหากคุณยังสูญเสียการเข้าถึงแอปการตรวจสอบสิทธิ์แบบสองปัจจัยคุณจะยังไม่สามารถเข้าถึงบัญชีนี้ได้ กรุณากำหนดค่าการรับรองความถูกต้องด้วยการยืนยันสองชั้น"
_permissions:
"read:account": "ดูข้อมูลบัญชีของคุณ"
"write:account": "แก้ไขข้อมูลบัญชีของคุณ"

View file

@ -1,4 +1,19 @@
---
_lang_: "ياپونچە"
headlineMisskey: "خاتىرە ئارقىلىق ئۇلانغان تور"
monthAndDay: "{day}-{month}"
search: "ئىزدەش"
ok: "ماقۇل"
noThankYou: "ئۇنى توختىتىڭ"
profile: "profile"
login: "كىرىش"
loggingIn: "كىرىش"
pin: "pinned"
delete: "ئۆچۈرۈش"
pinned: "pinned"
remove: "ئۆچۈرۈش"
searchByGoogle: "ئىزدەش"
_2fa:
renewTOTPCancel: "ئۇنى توختىتىڭ"
_widgets:
profile: "profile"

View file

@ -195,6 +195,7 @@ perHour: "每小时"
perDay: "每天"
stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此服务器向本服务器推流"
silenceThisInstance: "使服务器静音"
operations: "操作"
software: "软件"
version: "版本"
@ -214,6 +215,8 @@ clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被封锁的服务器"
blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。"
silencedInstances: "沉默的服务器"
silencedInstancesDescription: "设置要静音的服务器的主机,以换行符分隔。属于静默服务器的所有帐户都将被视为“静默”,所有关注都将成为请求,并且您将无法提及非关注者的本地帐户。被阻止的实例不受影响。"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户"
blockedUsers: "已拉黑的用户"
@ -2216,3 +2219,6 @@ _moderationLogTypes:
createAd: "创建了广告"
deleteAd: "删除了广告"
updateAd: "更新了广告"
_fileViewer:
url: "URL"
uploadedAt: "添加日期"

View file

@ -195,6 +195,7 @@ perHour: "每小時"
perDay: "每日"
stopActivityDelivery: "停止發送活動"
blockThisInstance: "封鎖此伺服器"
silenceThisInstance: "禁言此伺服器"
operations: "操作"
software: "軟體"
version: "版本"
@ -214,6 +215,8 @@ clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?"
blockedInstances: "已封鎖的伺服器"
blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。"
silencedInstances: "被禁言的伺服器"
silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。"
muteAndBlock: "靜音和封鎖"
mutedUsers: "被靜音的使用者"
blockedUsers: "被封鎖的使用者"
@ -543,6 +546,7 @@ serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框"
showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)"
withRepliesByDefaultForNewlyFollowed: "在追隨其他人後,預設在時間軸納入回覆的貼文"
newNoteRecived: "發現新貼文"
sounds: "音效"
sound: "音效"
@ -2005,6 +2009,7 @@ _exportOrImport:
userLists: "清單"
excludeMutingUsers: "排除被靜音的使用者"
excludeInactiveUsers: "排除不活躍帳戶"
withReplies: "將被匯入的追隨中清單的貼文回覆包含在時間軸"
_charts:
federation: "聯邦宇宙"
apRequest: "請求"

View file

@ -1,7 +1,7 @@
{
"name": "cherrypick",
"version": "4.4.0-phater.0",
"basedMisskeyVersion": "2023.10.2-beta.1",
"version": "4.5.0-phater.0",
"basedMisskeyVersion": "2023.11.0-beta.3",
"codename": "nasubi",
"repository": {
"type": "git",
@ -24,6 +24,7 @@
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate",
"revert": "cd packages/backend && pnpm revert",
"check:connect": "cd packages/backend && pnpm check:connect",
"migrateandstart": "pnpm migrate && pnpm start",
"migrateandstart:docker": "pnpm migrate && exec pnpm start:docker",
@ -39,7 +40,8 @@
"test-and-coverage": "pnpm -r test-and-coverage",
"clean": "node ./scripts/clean.js",
"clean-all": "node ./scripts/clean-all.js",
"cleanall": "pnpm clean-all"
"cleanall": "pnpm clean-all",
"schemasync": "cd packages/backend && pnpm typeorm schema:sync -d ormconfig.js"
},
"resolutions": {
"chokidar": "3.5.3",
@ -50,15 +52,15 @@
"cssnano": "6.0.1",
"js-yaml": "4.1.0",
"postcss": "8.4.31",
"terser": "5.21.0",
"terser": "5.22.0",
"typescript": "5.2.2"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"cross-env": "7.0.3",
"cypress": "13.3.1",
"eslint": "8.51.0",
"cypress": "13.3.2",
"eslint": "8.52.0",
"start-server-and-test": "2.0.1"
},
"optionalDependencies": {

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteUpdateAtHistory1696318192428 {
name = 'NoteUpdateAtHistory1696318192428'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAtHistory" TIMESTAMP WITH TIME ZONE array`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP "updatedAtHistory"`);
}
}

View file

@ -0,0 +1,12 @@
export class PollVotePoll1696604572677 {
name = 'PollVotePoll1696604572677';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_poll_vote_poll" FOREIGN KEY ("noteId") REFERENCES "poll"("noteId") ON DELETE CASCADE`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_poll_vote_poll"`);
}
}

View file

@ -8,11 +8,7 @@ export class UserListUserId1696807733453 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD "userListUserId" character varying(32) NOT NULL DEFAULT ''`);
const memberships = await queryRunner.query(`SELECT "id", "userListId" FROM "user_list_membership"`);
for(let i = 0; i < memberships.length; i++) {
const userList = await queryRunner.query(`SELECT "userId" FROM "user_list" WHERE "id" = $1`, [memberships[i].userListId]);
await queryRunner.query(`UPDATE "user_list_membership" SET "userListUserId" = $1 WHERE "id" = $2`, [userList[0].userId, memberships[i].id]);
}
await queryRunner.query(`UPDATE "user_list_membership" SET "userListUserId" = "user_list"."userId" FROM "user_list" WHERE "user_list_membership"."userListId" = "user_list"."id"`);
}
async down(queryRunner) {

View file

@ -21,7 +21,6 @@ export class DeleteCreatedAt1697420555911 {
await queryRunner.query(`DROP INDEX "public"."IDX_582f8fab771a9040a12961f3e7"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8f1a239bd077c8864a20c62c2c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f86d57fbca33c7a4e6897490cc"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
await queryRunner.query(`DROP INDEX "public"."IDX_fbb4297c927a9b85e9cefa2eb1"`);
await queryRunner.query(`DROP INDEX "public"."IDX_0fb627e1c2f753262a74f0562d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_149d2e44785707548c82999b01"`);
@ -125,7 +124,6 @@ export class DeleteCreatedAt1697420555911 {
await queryRunner.query(`CREATE INDEX "IDX_149d2e44785707548c82999b01" ON "flash" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_0fb627e1c2f753262a74f0562d" ON "poll_vote" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_f86d57fbca33c7a4e6897490cc" ON "muting" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_582f8fab771a9040a12961f3e7" ON "following" ("createdAt") `);

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NoteReactionAndUserPairCache1697673894459 {
name = 'NoteReactionAndUserPairCache1697673894459'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "reactionAndUserPairCache" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "reactionAndUserPairCache"`);
}
}

View file

@ -0,0 +1,29 @@
export class DeleteCreatedAt1697737204579 {
name = 'DeleteCreatedAt1697737204579'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_e21cd3646e52ef9c94aaf17c2e"`);
await queryRunner.query(`DROP INDEX "public"."IDX_20e30aa35180e317e133d75316"`);
await queryRunner.query(`DROP INDEX "public"."IDX_fdd74ab625ed0f6a30c47b00e0"`);
await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "createdAt"`);
await queryRunner.query(`ALTER TABLE "user_group" DROP COLUMN "createdAt"`);
await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP COLUMN "createdAt"`);
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP COLUMN "createdAt"`);
await queryRunner.query(`ALTER TABLE "user_group_joining" DROP COLUMN "createdAt"`);
await queryRunner.query(`ALTER TABLE "abuse_report_resolver" DROP COLUMN "createdAt"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "abuse_report_resolver" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`ALTER TABLE "user_group_joining" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`ALTER TABLE "user_group" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`ALTER TABLE "messaging_message" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
await queryRunner.query(`CREATE INDEX "IDX_fdd74ab625ed0f6a30c47b00e0" ON "abuse_report_resolver" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_e21cd3646e52ef9c94aaf17c2e" ON "messaging_message" ("createdAt") `);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AvatarDecoration1697847397844 {
name = 'AvatarDecoration1697847397844'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "avatar_decoration" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "url" character varying(1024) NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(2048) NOT NULL, "roleIdsThatCanBeUsedThisDecoration" character varying(128) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_b6de9296f6097078e1dc53f7603" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`DROP TABLE "avatar_decoration"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AvatarDecoration21697941908548 {
name = 'AvatarDecoration21697941908548'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" jsonb NOT NULL DEFAULT '[]'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarDecorations"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarDecorations" character varying(512) array NOT NULL DEFAULT '{}'`);
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class EnableFtt1698041201306 {
name = 'EnableFtt1698041201306'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimeline" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimeline"`);
}
}

View file

@ -10,6 +10,7 @@
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js",
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
"check:connect": "node ./check_connect.js",
"build": "swc src -d built -D",
"watch:swc": "swc src -d built -D -w",
@ -22,7 +23,8 @@
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage"
"test-and-coverage": "pnpm jest-and-coverage",
"schemasync": "pnpm typeorm schema:sync -d ormconfig.js"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
@ -78,9 +80,9 @@
"@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.3.2",
"@sinonjs/fake-timers": "11.1.0",
"@sinonjs/fake-timers": "11.2.2",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.93",
"@swc/core": "1.3.94",
"@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
@ -89,7 +91,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.12.4",
"bullmq": "4.12.5",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -102,7 +104,7 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.24.2",
"fastify": "4.24.3",
"feed": "4.2.2",
"file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2",
@ -127,7 +129,7 @@
"nanoid": "5.0.2",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.6",
"nodemailer": "6.9.7",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
@ -145,7 +147,7 @@
"qrcode": "1.5.3",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.20.3",
"re2": "1.20.4",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
@ -159,7 +161,7 @@
"stringz": "2.1.0",
"strip-ansi": "^7.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.12",
"systeminformation": "5.21.13",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
@ -184,43 +186,43 @@
"@types/cbor": "6.0.0",
"@types/color-convert": "2.0.2",
"@types/content-disposition": "0.5.7",
"@types/fluent-ffmpeg": "2.1.22",
"@types/http-link-header": "1.0.3",
"@types/jest": "29.5.5",
"@types/js-yaml": "4.0.7",
"@types/jsdom": "21.1.3",
"@types/jsonld": "1.5.10",
"@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.2",
"@types/ms": "0.7.32",
"@types/node": "20.8.6",
"@types/fluent-ffmpeg": "2.1.23",
"@types/http-link-header": "1.0.4",
"@types/jest": "29.5.6",
"@types/js-yaml": "4.0.8",
"@types/jsdom": "21.1.4",
"@types/jsonld": "1.5.11",
"@types/jsrsasign": "10.5.11",
"@types/mime-types": "2.1.3",
"@types/ms": "0.7.33",
"@types/node": "20.8.7",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0",
"@types/pg": "8.10.5",
"@types/pug": "2.0.7",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.2",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/rename": "1.0.5",
"@types/sanitize-html": "2.9.2",
"@types/semver": "7.5.3",
"@types/nodemailer": "6.4.13",
"@types/oauth": "0.9.3",
"@types/oauth2orize": "1.11.2",
"@types/oauth2orize-pkce": "0.1.1",
"@types/pg": "8.10.7",
"@types/pug": "2.0.8",
"@types/punycode": "2.1.1",
"@types/qrcode": "1.5.4",
"@types/random-seed": "0.3.4",
"@types/ratelimiter": "3.4.5",
"@types/rename": "1.0.6",
"@types/sanitize-html": "2.9.3",
"@types/semver": "7.5.4",
"@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.5",
"@types/sinonjs__fake-timers": "8.1.3",
"@types/tinycolor2": "1.4.4",
"@types/tmp": "0.2.4",
"@types/vary": "1.1.1",
"@types/web-push": "3.6.1",
"@types/ws": "8.5.7",
"@types/simple-oauth2": "5.0.6",
"@types/sinonjs__fake-timers": "8.1.4",
"@types/tinycolor2": "1.4.5",
"@types/tmp": "0.2.5",
"@types/vary": "1.1.2",
"@types/web-push": "3.6.2",
"@types/ws": "8.5.8",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.51.0",
"eslint": "8.52.0",
"eslint-plugin-import": "2.28.1",
"execa": "8.0.1",
"jest": "29.7.0",

View file

@ -149,6 +149,13 @@ export type Config = {
relashionshipJobPerSec: number | undefined;
deliverJobMaxAttempts: number | undefined;
inboxJobMaxAttempts: number | undefined;
cloudLogging?: {
projectId: string;
saKeyPath: string;
logName?: string;
}
apFileBaseUrl: string | undefined;
proxyRemoteFiles: boolean | undefined;
signToActivityPubGet: boolean | undefined;

View file

@ -80,8 +80,8 @@ export class AnnouncementService {
const packed = (await this.packMany([announcement]))[0];
if (announcement.isActive) {
if (announcement.userId) {
this.globalEventService.publishMainStream(announcement.userId, 'announcementCreated', {
if (values.userId) {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
announcement: packed,
});

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { AvatarDecorationsRepository, MiAvatarDecoration, MiUser } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MemorySingleCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@Injectable()
export class AvatarDecorationService implements OnApplicationShutdown {
public cache: MemorySingleCache<MiAvatarDecoration[]>;
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.avatarDecorationsRepository)
private avatarDecorationsRepository: AvatarDecorationsRepository,
private idService: IdService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'avatarDecorationCreated':
case 'avatarDecorationUpdated':
case 'avatarDecorationDeleted': {
this.cache.delete();
break;
}
default:
break;
}
}
}
@bindThis
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
const created = await this.avatarDecorationsRepository.insert({
id: this.idService.gen(),
...options,
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);
if (moderator) {
this.moderationLogService.log(moderator, 'createAvatarDecoration', {
avatarDecorationId: created.id,
avatarDecoration: created,
});
}
return created;
}
@bindThis
public async update(id: MiAvatarDecoration['id'], params: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<void> {
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
const date = new Date();
await this.avatarDecorationsRepository.update(avatarDecoration.id, {
updatedAt: date,
...params,
});
const updated = await this.avatarDecorationsRepository.findOneByOrFail({ id: avatarDecoration.id });
this.globalEventService.publishInternalEvent('avatarDecorationUpdated', updated);
if (moderator) {
this.moderationLogService.log(moderator, 'updateAvatarDecoration', {
avatarDecorationId: avatarDecoration.id,
before: avatarDecoration,
after: updated,
});
}
}
@bindThis
public async delete(id: MiAvatarDecoration['id'], moderator?: MiUser): Promise<void> {
const avatarDecoration = await this.avatarDecorationsRepository.findOneByOrFail({ id });
await this.avatarDecorationsRepository.delete({ id: avatarDecoration.id });
this.globalEventService.publishInternalEvent('avatarDecorationDeleted', avatarDecoration);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteAvatarDecoration', {
avatarDecorationId: avatarDecoration.id,
avatarDecoration: avatarDecoration,
});
}
}
@bindThis
public async getAll(noCache = false): Promise<MiAvatarDecoration[]> {
if (noCache) {
this.cache.delete();
}
return this.cache.fetch(() => this.avatarDecorationsRepository.find());
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View file

@ -11,6 +11,7 @@ import { AnnouncementService } from './AnnouncementService.js';
import { AntennaService } from './AntennaService.js';
import { AppLockService } from './AppLockService.js';
import { AchievementService } from './AchievementService.js';
import { AvatarDecorationService } from './AvatarDecorationService.js';
import { CaptchaService } from './CaptchaService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js';
@ -32,6 +33,7 @@ import { MetaService } from './MetaService.js';
import { MfmService } from './MfmService.js';
import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js';
import { NoteUpdateService } from './NoteUpdateService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js';
import { NoteReadService } from './NoteReadService.js';
@ -145,6 +147,7 @@ const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExis
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
@ -166,6 +169,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
@ -283,6 +287,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
AntennaService,
AppLockService,
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
@ -304,6 +309,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
MfmService,
ModerationLogService,
NoteCreateService,
NoteUpdateService,
NoteDeleteService,
NotePiningService,
NoteReadService,
@ -414,6 +420,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$AntennaService,
$AppLockService,
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
@ -435,6 +442,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$MfmService,
$ModerationLogService,
$NoteCreateService,
$NoteUpdateService,
$NoteDeleteService,
$NotePiningService,
$NoteReadService,
@ -546,6 +554,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
AntennaService,
AppLockService,
AchievementService,
AvatarDecorationService,
CaptchaService,
CreateSystemUserService,
CustomEmojiService,
@ -567,6 +576,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
MfmService,
ModerationLogService,
NoteCreateService,
NoteUpdateService,
NoteDeleteService,
NotePiningService,
NoteReadService,
@ -676,6 +686,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$AntennaService,
$AppLockService,
$AchievementService,
$AvatarDecorationService,
$CaptchaService,
$CreateSystemUserService,
$CustomEmojiService,
@ -697,6 +708,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$MfmService,
$ModerationLogService,
$NoteCreateService,
$NoteUpdateService,
$NoteDeleteService,
$NotePiningService,
$NoteReadService,

View file

@ -20,7 +20,7 @@ import type { MiSignin } from '@/models/Signin.js';
import type { MiPage } from '@/models/Page.js';
import type { MiWebhook } from '@/models/Webhook.js';
import type { MiMeta } from '@/models/Meta.js';
import { MiRole, MiRoleAssignment } from '@/models/_.js';
import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@ -82,7 +82,13 @@ export interface MainEventTypes {
unreadAntenna: MiAntenna;
readAllAnnouncements: undefined;
myTokenRegenerated: undefined;
signin: MiSignin;
signin: {
id: MiSignin['id'];
createdAt: string;
ip: string;
headers: Record<string, any>;
success: boolean;
};
registryUpdated: {
scope?: string[];
key: string;
@ -115,7 +121,7 @@ export interface NoteEventTypes {
};
updated: {
cw: string | null;
text: string;
text: string | null;
};
reacted: {
reaction: string;
@ -219,6 +225,9 @@ export interface InternalEventTypes {
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
avatarDecorationCreated: MiAvatarDecoration;
avatarDecorationDeleted: MiAvatarDecoration;
avatarDecorationUpdated: MiAvatarDecoration;
metaUpdated: MiMeta;
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };

View file

@ -56,11 +56,8 @@ export class MessagingService {
@bindThis
public async createMessage(user: { id: MiUser['id']; host: MiUser['host']; }, recipientUser: MiUser | null, recipientGroup: MiUserGroup | null, text: string | null | undefined, file: MiDriveFile | null, uri?: string) {
const data = new Date();
const message = {
id: this.idService.genId(data),
createdAt: data,
id: this.idService.gen(Date.now()),
fileId: file ? file.id : null,
recipientId: recipientUser ? recipientUser.id : null,
groupId: recipientGroup ? recipientGroup.id : null,
@ -135,7 +132,6 @@ export class MessagingService {
const note = {
id: message.id,
createdAt: message.createdAt,
fileIds: message.fileId ? [message.fileId] : [],
text: message.text,
userId: message.userId,
@ -298,7 +294,7 @@ export class MessagingService {
.where('message.groupId = :groupId', { groupId: groupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.andWhere('message.id > :joinedAt', { joinedAt: this.idService.parse(joining.id) }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null);
if (!unreadExist) {

View file

@ -56,8 +56,8 @@ import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { nyaize } from '@/misc/nyaize.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -129,6 +129,7 @@ type MinimumUser = {
type Option = {
createdAt?: Date | null;
updatedAt?: Date | null;
name?: string | null;
text?: string | null;
reply?: MiNote | null;
@ -220,6 +221,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart,
private utilityService: UtilityService,
private userBlockingService: UserBlockingService,
) { }
@bindThis
@ -230,8 +232,6 @@ export class NoteCreateService implements OnApplicationShutdown {
isBot: MiUser['isBot'];
isCat: MiUser['isCat'];
}, data: Option, silent = false): Promise<MiNote> {
let patsedText: mfm.MfmNode[] | null = null;
// チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@ -299,6 +299,18 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
// Check blocking
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
if (blocked) {
throw new Error('blocked');
}
}
}
}
// 返信対象がpublicではないならhomeにする
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
@ -319,25 +331,6 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
if (user.isCat) {
patsedText = mfm.parse(data.text);
function nyaizeNode(node: mfm.MfmNode) {
if (node.type === 'quote') return;
if (node.type === 'text') {
node.props.text = nyaize(node.props.text);
}
if (node.children) {
for (const child of node.children) {
nyaizeNode(child);
}
}
}
for (const node of patsedText) {
nyaizeNode(node);
}
data.text = mfm.toString(patsedText);
}
} else {
data.text = null;
}
@ -348,7 +341,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
const tokens = patsedText ?? (data.text ? mfm.parse(data.text)! : []);
const tokens = (data.text ? mfm.parse(data.text)! : []);
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
@ -607,7 +600,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true });
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
this.globalEventService.publishNotesStream(noteObj);
@ -870,6 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch();
if (!meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline();
@ -913,7 +907,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (note.visibility === 'followers') {
// TODO: 重そうだから何とかしたい Set 使う?
userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId));
userListMemberships = userListMemberships.filter(x => x.userListUserId === user.id || followings.some(f => f.followerId === x.userListUserId));
}
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする

View file

@ -0,0 +1,297 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setImmediate } from 'node:timers/promises';
import { In, DataSource } from 'typeorm';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
import type { NotesRepository, UsersRepository } from '@/models/_.js';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { RelayService } from '@/core/RelayService.js';
import { DI } from '@/di-symbols.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { SearchService } from '@/core/SearchService.js';
import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import { MiDriveFile } from '@/models/_.js';
import { MiPoll, IPoll } from '@/models/Poll.js';
import * as mfm from "cherrypick-mfm-js";
import { concat } from "@/misc/prelude/array.js";
import { extractHashtags } from "@/misc/extract-hashtags.js";
import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js";
import util from 'util';
type MinimumUser = {
id: MiUser['id'];
host: MiUser['host'];
username: MiUser['username'];
uri: MiUser['uri'];
};
type Option = {
updatedAt?: Date | null;
files?: MiDriveFile[] | null;
name?: string | null;
text?: string | null;
cw?: string | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
poll?: IPoll | null;
};
@Injectable()
export class NoteUpdateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private relayService: RelayService,
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
private searchService: SearchService,
private activeUsersChart: ActiveUsersChart,
) { }
@bindThis
public async update(user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
}, data: Option, note: MiNote, silent = false): Promise<MiNote | null> {
if (data.updatedAt == null) data.updatedAt = new Date();
if (data.text) {
if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
} else {
data.text = null;
}
let tags = data.apHashtags;
let emojis = data.apEmojis;
// Parse MFM if needed
if (!tags || !emojis) {
const tokens = data.text ? mfm.parse(data.text)! : [];
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
tags = data.apHashtags ?? extractHashtags(combinedTokens);
emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens);
}
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
const updatedNote = await this.updateNote(user, note, data, tags, emojis);
if (updatedNote) {
setImmediate('post updated', { signal: this.#shutdownController.signal }).then(
() => this.postNoteUpdated(updatedNote, user, silent),
() => { /* aborted, ignore this */ },
);
}
return updatedNote;
}
@bindThis
private async updateNote(user: {
id: MiUser['id']; host: MiUser['host'];
}, note: MiNote, data: Option, tags: string[], emojis: string[]): Promise<MiNote | null> {
const updatedAtHistory = note.updatedAtHistory ? note.updatedAtHistory : [];
const values = new MiNote({
updatedAt: data.updatedAt!,
fileIds: data.files ? data.files.map(file => file.id) : [],
text: data.text,
hasPoll: data.poll != null,
cw: data.cw ?? null,
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
updatedAtHistory: [...updatedAtHistory, new Date()],
noteEditHistory: [...note.noteEditHistory, (note.cw ? note.cw + '\n' : '') + note.text!],
});
// 投稿を更新
try {
if (note.hasPoll && values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
if (values.hasPoll) {
const old_poll = await transactionalEntityManager.findOneBy(MiPoll, { noteId: note.id });
if (old_poll!.choices.toString() !== data.poll!.choices.toString() || old_poll!.multiple !== data.poll!.multiple) {
await transactionalEntityManager.delete(MiPoll, { noteId: note.id });
const poll = new MiPoll({
noteId: note.id,
choices: data.poll!.choices,
expiresAt: data.poll!.expiresAt,
multiple: data.poll!.multiple,
votes: new Array(data.poll!.choices.length).fill(0),
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
});
await transactionalEntityManager.insert(MiPoll, poll);
}
}
});
} else if (!note.hasPoll && values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
if (values.hasPoll) {
const poll = new MiPoll({
noteId: note.id,
choices: data.poll!.choices,
expiresAt: data.poll!.expiresAt,
multiple: data.poll!.multiple,
votes: new Array(data.poll!.choices.length).fill(0),
noteVisibility: note.visibility,
userId: user.id,
userHost: user.host,
});
await transactionalEntityManager.insert(MiPoll, poll);
}
});
} else if (note.hasPoll && !values.hasPoll) {
// Start transaction
await this.db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.update(MiNote, {id: note.id}, values);
if (!values.hasPoll) {
await transactionalEntityManager.delete(MiPoll, {noteId: note.id});
}
});
} else {
await this.notesRepository.update({ id: note.id }, values);
}
return await this.notesRepository.findOneBy({ id: note.id });
} catch (e) {
console.error(e);
throw e;
}
}
@bindThis
private async postNoteUpdated(note: MiNote, user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
}, silent: boolean) {
if (!silent) {
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text });
//#region AP deliver
if (this.userEntityService.isLocalUser(user)) {
await (async () => {
// @ts-ignore
const noteActivity = await this.renderNoteActivity(note, user);
await this.deliverToConcerned(user, note, noteActivity);
})();
}
//#endregion
}
// Register to search database
this.reIndex(note);
}
@bindThis
private async renderNoteActivity(note: MiNote, user: MiUser) {
const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user);
return this.apRendererService.addContext(content);
}
@bindThis
private async getMentionedRemoteUsers(note: MiNote) {
const where = [] as any[];
// mention / reply / dm
const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
if (uris.length > 0) {
where.push(
{ uri: In(uris) },
);
}
// renote / quote
if (note.renoteUserId) {
where.push({
id: note.renoteUserId,
});
}
if (where.length === 0) return [];
return await this.usersRepository.find({
where,
}) as MiRemoteUser[];
}
@bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
console.log('deliverToConcerned', util.inspect(content, { depth: null }));
await this.apDeliverManagerService.deliverToFollowers(user, content);
await this.relayService.deliverToRelays(user, content);
const remoteUsers = await this.getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
await this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
}
}
@bindThis
private reIndex(note: MiNote) {
if (note.text == null && note.cw == null) return;
this.searchService.unindexNote(note);
this.searchService.indexNote(note);
}
@bindThis
public dispose(): void {
this.#shutdownController.abort();
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}

View file

@ -40,7 +40,7 @@ export class QueryService {
) {
}
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

View file

@ -30,6 +30,7 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = {
'like': '👍',
@ -187,6 +188,9 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
} : {}),
})
.where('id = :id', { id: note.id })
.execute();
@ -293,6 +297,7 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
})
.where('id = :id', { id: note.id })
.execute();

View file

@ -231,6 +231,12 @@ export class RoleService implements OnApplicationShutdown {
}
}
@bindThis
public async getRoles() {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
return roles;
}
@bindThis
public async getUserAssigns(userId: MiUser['id']) {
const now = Date.now();

View file

@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
@ -78,6 +79,7 @@ export class ApInboxService {
private notePiningService: NotePiningService,
private userBlockingService: UserBlockingService,
private noteCreateService: NoteCreateService,
private noteUpdateService: NoteUpdateService,
private noteDeleteService: NoteDeleteService,
private appLockService: AppLockService,
private apResolverService: ApResolverService,
@ -774,11 +776,13 @@ export class ApInboxService {
@bindThis
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
const uri = getApId(activity);
if (actor.uri !== activity.actor) {
return 'skip: invalid actor';
}
this.logger.debug('Update');
this.logger.debug(`Update: ${uri}`);
const resolver = this.apResolverService.createResolver();
@ -793,11 +797,48 @@ export class ApInboxService {
} else if (getApType(object) === 'Question') {
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
return 'ok: Question updated';
} else if (getApType(object) === 'Note') {
await this.updateNote(resolver, actor, object, false, activity);
return 'ok: Note updated';
} else {
return `skip: Unknown type: ${getApType(object)}`;
}
}
@bindThis
private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise<string> {
const uri = getApId(note);
if (typeof note === 'object') {
if (actor.uri !== note.attributedTo) {
return 'skip: actor.uri !== note.attributedTo';
}
if (typeof note.id === 'string') {
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
return 'skip: host in actor.uri !== note.id';
}
}
}
const unlock = await this.appLockService.getApLock(uri);
try {
const target = await this.notesRepository.findOneBy({uri: uri});
if (!target) return `skip: target note not located: ${uri}`;
await this.apNoteService.updateNote(note, target, resolver, silent);
return 'ok';
} catch (err) {
if (err instanceof StatusError && err.isClientError) {
return `skip ${err.statusCode}`;
} else {
throw err;
}
} finally {
unlock();
}
}
@bindThis
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
// fetch the new and old accounts

View file

@ -112,6 +112,7 @@ export class ApRendererService {
actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Announce',
published: this.idService.parse(note.id).date.toISOString(),
updated: note.updatedAt?.toISOString() ?? undefined,
to,
cc,
object,
@ -460,6 +461,7 @@ export class ApRendererService {
_misskey_quote: quote,
quoteUrl: quote,
published: this.idService.parse(note.id).date.toISOString(),
updated: note.updatedAt?.toISOString() ?? undefined,
to,
cc,
inReplyTo,

View file

@ -7,7 +7,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/_.js';
import type { EmojisRepository, MessagingMessagesRepository, NotesRepository, PollsRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
@ -25,10 +25,12 @@ import { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import type { IObject, IPost } from '../type.js';
import { getApId, getApType, getOneApHrefNullable, getOneApId, isEmoji, validPost } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js';
import type { Resolver } from '../ApResolverService.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApAudienceService } from '../ApAudienceService.js';
import { ApPersonService } from './ApPersonService.js';
@ -37,8 +39,7 @@ import { ApMentionService } from './ApMentionService.js';
import { ApQuestionService } from './ApQuestionService.js';
import { ApEventService } from './ApEventService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
@Injectable()
export class ApNoteService {
@ -57,6 +58,9 @@ export class ApNoteService {
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private idService: IdService,
private apMfmService: ApMfmService,
private apResolverService: ApResolverService,
@ -76,6 +80,7 @@ export class ApNoteService {
private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
private noteUpdateService: NoteUpdateService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
) {
@ -302,6 +307,7 @@ export class ApNoteService {
try {
return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null,
updatedAt: note.updated ? new Date(note.updated) : null,
files,
reply,
renote: quote,
@ -333,6 +339,85 @@ export class ApNoteService {
}
}
@bindThis
public async updateNote(value: string | IObject, target: MiNote, resolver?: Resolver, silent = false): Promise<MiNote | null> {
if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value);
const entryUri = getApId(value);
const err = this.validateNote(object, entryUri);
if (err) {
this.logger.error(err.message, {
resolver: { history: resolver.getHistory() },
value,
object,
});
throw new Error('invalid note');
}
const note = object as IPost;
// 投稿者をフェッチ
if (note.attributedTo == null) {
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
}
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
}
const limit = promiseLimit<MiDriveFile>(2);
const files = (await Promise.all(toArray(note.attachment).map(attach => (
limit(() => this.apImageService.resolveImage(actor, {
...attach,
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
}))
))));
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag);
}
const apHashtags = extractApHashtags(note.tag);
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`);
return [];
});
const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
try {
return await this.noteUpdateService.update(actor, {
updatedAt: note.updated ? new Date(note.updated) : null,
files,
name: note.name,
cw,
text,
apHashtags,
apEmojis,
poll,
}, target, silent);
} catch (err: any) {
this.logger.warn(`note update failed: ${err}`);
return err;
}
}
/**
* Noteを解決します
*

View file

@ -13,6 +13,7 @@ export interface IObject {
name?: string | null;
summary?: string;
published?: string;
updated?: string;
cc?: ApObject;
to?: ApObject;
attributedTo?: ApObject;

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js';

View file

@ -10,6 +10,7 @@ import type { Packed } from '@/misc/json-schema.js';
import type { MiUser } from '@/models/User.js';
import type { MiMessagingMessage } from '@/models/MessagingMessage.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { UserGroupEntityService } from './UserGroupEntityService.js';
@ -20,6 +21,7 @@ export class MessagingMessageEntityService {
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private idService: IdService,
private userEntityService: UserEntityService,
private userGroupEntityService: UserGroupEntityService,
private driveFileEntityService: DriveFileEntityService,
@ -44,7 +46,7 @@ export class MessagingMessageEntityService {
return {
id: message.id,
createdAt: message.createdAt.toISOString(),
createdAt: this.idService.parse(message.id).date.toISOString(),
text: message.text,
userId: message.userId,
user: await this.userEntityService.pack(message.user ?? message.userId, me),

View file

@ -76,7 +76,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示
@ -86,7 +86,7 @@ export class NoteEntityService implements OnModuleInit {
} else if (meId === packedNote.userId) {
hide = false;
} else {
// 指定されているかどうか
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
if (specified) {
@ -184,27 +184,37 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], string | null>;
}) {
if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(noteId);
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return this.reactionService.convertLegacyReaction(reaction);
} else {
return undefined;
}
}
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) return undefined;
if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
if (pair) {
return this.reactionService.convertLegacyReaction(pair.split('/')[1]);
} else {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) {
if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
return undefined;
}
const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId,
noteId: noteId,
noteId: note.id,
});
if (reaction) {
@ -290,8 +300,9 @@ export class NoteEntityService implements OnModuleInit {
options?: {
detail?: boolean;
skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
};
},
@ -299,6 +310,7 @@ export class NoteEntityService implements OnModuleInit {
const opts = Object.assign({
detail: true,
skipHide: false,
withReactionAndUserPairCache: false,
}, options);
const meId = me ? me.id : null;
@ -327,6 +339,7 @@ export class NoteEntityService implements OnModuleInit {
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
updatedAtHistory: note.updatedAtHistory ? note.updatedAtHistory.map(x => x.toISOString()) : undefined,
noteEditHistory: note.noteEditHistory.length ? note.noteEditHistory : undefined,
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
@ -343,6 +356,7 @@ export class NoteEntityService implements OnModuleInit {
repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds,
@ -365,19 +379,23 @@ export class NoteEntityService implements OnModuleInit {
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
detail: true,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
event: note.hasEvent ? this.populateEvent(note) : undefined,
...(meId ? {
myReaction: this.populateMyReaction(note.id, meId, options?._hint_),
...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note, meId, options?._hint_),
} : {}),
} : {}),
});
@ -401,19 +419,48 @@ export class NoteEntityService implements OnModuleInit {
if (notes.length === 0) return [];
const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>();
const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000);
const targets = [...notes.filter(n => n.id < oldId).map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.renote.id);
}
} else {
if (note.id < oldId) {
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.id);
}
} else {
myReactionsMap.set(note.id, null);
}
}
}
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(Array.from(idsNeedFetchMyReaction)),
}) : [];
for (const id of idsNeedFetchMyReaction) {
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
}
}

View file

@ -7,10 +7,12 @@ import { Injectable } from '@nestjs/common';
import type { } from '@/models/Blocking.js';
import type { MiSignin } from '@/models/Signin.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class SigninEntityService {
constructor(
private idService: IdService,
) {
}
@ -18,7 +20,13 @@ export class SigninEntityService {
public async pack(
src: MiSignin,
) {
return src;
return {
id: src.id,
createdAt: this.idService.parse(src.id).date.toISOString(),
ip: src.ip,
headers: src.headers,
success: src.success,
};
}
}

View file

@ -22,9 +22,10 @@ import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { AnnouncementService } from '../AnnouncementService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
@ -63,6 +64,7 @@ export class UserEntityService implements OnModuleInit {
private roleService: RoleService;
private federatedInstanceService: FederatedInstanceService;
private idService: IdService;
private avatarDecorationService: AvatarDecorationService;
constructor(
private moduleRef: ModuleRef,
@ -133,6 +135,7 @@ export class UserEntityService implements OnModuleInit {
this.roleService = this.moduleRef.get('RoleService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.idService = this.moduleRef.get('IdService');
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
}
//#region Validators
@ -233,7 +236,7 @@ export class UserEntityService implements OnModuleInit {
.where('message.groupId = :groupId', { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.andWhere('message.id > :joinedAt', { joinedAt: this.idService.parse(j.id) }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
@ -376,12 +379,14 @@ export class UserEntityService implements OnModuleInit {
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
const unreadAnnouncements = isMe && opts.detail ? await this.announcementService.getUnreadAnnouncements(user) : null;
const unreadAnnouncements = isMe && opts.detail ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(),
...announcement,
})) : null;
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
const falsy = opts.detail ? false : undefined;
const packed = {
id: user.id,
name: user.name,
@ -389,6 +394,12 @@ export class UserEntityService implements OnModuleInit {
host: user.host,
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
avatarBlurhash: user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
id: ud.id,
angle: ud.angle || undefined,
flipH: ud.flipH || undefined,
url: decorations.find(d => d.id === ud.id)!.url,
}))) : [],
isBot: user.isBot,
isCat: user.isCat,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {

View file

@ -9,6 +9,7 @@ import type { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models
import type { Packed } from '@/misc/json-schema.js';
import type { MiUserGroup } from '@/models/UserGroup.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class UserGroupEntityService {
@ -18,6 +19,8 @@ export class UserGroupEntityService {
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private idService: IdService,
) {
}
@ -33,7 +36,7 @@ export class UserGroupEntityService {
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
createdAt: this.idService.parse(userGroup.id).date.toISOString(),
name: userGroup.name,
ownerId: userGroup.userId,
userIds: users.map((x: { userId: any; }) => x.userId),

View file

@ -20,6 +20,7 @@ export const DI = {
announcementsRepository: Symbol('announcementsRepository'),
announcementReadsRepository: Symbol('announcementReadsRepository'),
appsRepository: Symbol('appsRepository'),
avatarDecorationsRepository: Symbol('avatarDecorationsRepository'),
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
noteReactionsRepository: Symbol('noteReactionsRepository'),

View file

@ -11,12 +11,6 @@ export class MiAbuseReportResolver {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of AbuseReportResolver',
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The updated date of AbuseReportResolver',

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { id } from './util/id.js';
@Entity('avatar_decoration')
export class MiAvatarDecoration {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
nullable: true,
})
public updatedAt: Date | null;
@Column('varchar', {
length: 1024,
})
public url: string;
@Column('varchar', {
length: 256,
})
public name: string;
@Column('varchar', {
length: 2048,
})
public description: string;
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
@Column('varchar', {
array: true, length: 128, default: '{}',
})
public roleIdsThatCanBeUsedThisDecoration: string[];
}

View file

@ -14,12 +14,6 @@ export class MiMessagingMessage {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the MessagingMessage.',
})
public createdAt: Date;
@Index()
@Column({
...id(),

View file

@ -607,6 +607,11 @@ export class MiMeta {
})
public preservedUsernames: string[];
@Column('boolean', {
default: true,
})
public enableFanoutTimeline: boolean;
@Column('integer', {
default: 300,
})

View file

@ -23,6 +23,12 @@ export class MiNote {
})
public updatedAt: Date | null;
@Column('timestamp with time zone', {
array: true,
default: null,
})
public updatedAtHistory: Date[] | null;
@Column('varchar', {
length: 3000,
array: true,
@ -186,6 +192,11 @@ export class MiNote {
})
public mentionedRemoteUsers: string;
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public reactionAndUserPairCache: string[];
@Column('varchar', {
length: 128, array: true, default: '{}',
})

View file

@ -5,7 +5,7 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MiAbuseReportResolver, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMessagingMessage, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserGroup, MiUserGroupJoining, MiUserGroupInvitation, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
import { MiAbuseReportResolver, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMessagingMessage, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserGroup, MiUserGroupJoining, MiUserGroupInvitation, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@ -39,6 +39,12 @@ const $appsRepository: Provider = {
inject: [DI.db],
};
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
@ -438,6 +444,7 @@ const $abuseReportResolversRepository: Provider = {
$announcementsRepository,
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
@ -510,6 +517,7 @@ const $abuseReportResolversRepository: Provider = {
$announcementsRepository,
$announcementReadsRepository,
$appsRepository,
$avatarDecorationsRepository,
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,

View file

@ -138,6 +138,15 @@ export class MiUser {
})
public bannerBlurhash: string | null;
@Column('jsonb', {
default: [],
})
public avatarDecorations: {
id: string;
angle: number;
flipH: boolean;
}[];
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}',

View file

@ -12,12 +12,6 @@ export class MiUserGroup {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroup.',
})
public createdAt: Date;
@Column('varchar', {
length: 256,
})

View file

@ -14,11 +14,6 @@ export class MiUserGroupInvitation {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupInvitation.',
})
public createdAt: Date;
@Index()
@Column({
...id(),

View file

@ -14,11 +14,6 @@ export class MiUserGroupJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupJoining.',
})
public createdAt: Date;
@Index()
@Column({
...id(),

View file

@ -11,6 +11,7 @@ import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js';
import { MiApp } from '@/models/App.js';
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiAuthSession } from '@/models/AuthSession.js';
import { MiBlocking } from '@/models/Blocking.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
@ -84,6 +85,7 @@ export {
MiAnnouncementRead,
MiAntenna,
MiApp,
MiAvatarDecoration,
MiAuthSession,
MiBlocking,
MiChannelFollowing,
@ -156,6 +158,7 @@ export type AnnouncementsRepository = Repository<MiAnnouncement>;
export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
export type AntennasRepository = Repository<MiAntenna>;
export type AppsRepository = Repository<MiApp>;
export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
export type AuthSessionsRepository = Repository<MiAuthSession>;
export type BlockingsRepository = Repository<MiBlocking>;
export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;

View file

@ -94,7 +94,7 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true,
},
isSilenced: {
type: "boolean",
type: 'boolean',
optional: false,
nullable: false,
},

View file

@ -22,6 +22,15 @@ export const packedNoteSchema = {
optional: true, nullable: true,
format: 'date-time',
},
updatedAtHistory: {
type: 'array',
optional: true, nullable: true,
items: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
},
noteEditHistory: {
type: 'array',
optional: true, nullable: false,
@ -187,6 +196,14 @@ export const packedNoteSchema = {
type: 'string',
optional: true, nullable: false,
},
reactionAndUserPairCache: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
myReaction: {
type: 'object',

View file

@ -37,6 +37,34 @@ export const packedUserLiteSchema = {
type: 'string',
nullable: true, optional: false,
},
avatarDecorations: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
angle: {
type: 'number',
nullable: false, optional: true,
},
flipH: {
type: 'boolean',
nullable: false, optional: true,
},
},
},
},
isAdmin: {
type: 'boolean',
nullable: false, optional: true,

View file

@ -19,6 +19,7 @@ import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js';
import { MiApp } from '@/models/App.js';
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiAuthSession } from '@/models/AuthSession.js';
import { MiBlocking } from '@/models/Blocking.js';
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
@ -136,6 +137,7 @@ export const entities = [
MiMeta,
MiInstance,
MiApp,
MiAvatarDecoration,
MiAuthSession,
MiAccessToken,
MiUser,

View file

@ -34,7 +34,7 @@ export class RelationshipProcessorService {
@bindThis
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? "with replies" : "without replies"}`);
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? 'with replies' : 'without replies'}`);
await this.userFollowingService.follow(job.data.from, job.data.to, {
requestId: job.data.requestId,
silent: job.data.silent,

View file

@ -73,7 +73,7 @@ export class ServerService implements OnApplicationShutdown {
public async launch(): Promise<void> {
const fastify = Fastify({
trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
logger: false,
});
this.#fastify = fastify;

View file

@ -320,8 +320,9 @@ export class ApiCallService implements OnApplicationShutdown {
}
if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy]) {
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({
message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED',

View file

@ -8,6 +8,10 @@ import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import { ServerStatsService } from '@/daemons/ServerStatsService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
@ -19,10 +23,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@ -168,6 +172,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_update_all from './endpoints/following/update-all.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -183,6 +188,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
import * as ep___hashtags_list from './endpoints/hashtags/list.js';
import * as ep___hashtags_search from './endpoints/hashtags/search.js';
import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@ -381,12 +387,17 @@ import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___users_translate from './endpoints/users/translate.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common';
const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
const $admin_abuseReportResolver_create: Provider = { provide: 'ep:admin/abuse-report-resolver/create', useClass: ep___admin_abuseReportResolver_create.default };
const $admin_abuseReportResolver_update: Provider = { provide: 'ep:admin/abuse-report-resolver/update', useClass: ep___admin_abuseReportResolver_update.default };
const $admin_abuseReportResolver_list: Provider = { provide: 'ep:admin/abuse-report-resolver/list', useClass: ep___admin_abuseReportResolver_list.default };
const $admin_abuseReportResolver_delete: Provider = { provide: 'ep:admin/abuse-report-resolver/delete', useClass: ep___admin_abuseReportResolver_delete.default };
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
@ -398,10 +409,10 @@ const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements
const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
const $admin_abuseReportResolver_create: Provider = { provide: 'ep:admin/abuse-report-resolver/create', useClass: ep___admin_abuseReportResolver_create.default };
const $admin_abuseReportResolver_update: Provider = { provide: 'ep:admin/abuse-report-resolver/update', useClass: ep___admin_abuseReportResolver_update.default };
const $admin_abuseReportResolver_list: Provider = { provide: 'ep:admin/abuse-report-resolver/list', useClass: ep___admin_abuseReportResolver_list.default };
const $admin_abuseReportResolver_delete: Provider = { provide: 'ep:admin/abuse-report-resolver/delete', useClass: ep___admin_abuseReportResolver_delete.default };
const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default };
const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
@ -547,6 +558,7 @@ const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass:
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default };
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
@ -562,6 +574,7 @@ const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useCla
const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default };
const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default };
const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default };
const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default };
const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default };
const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default };
const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default };
@ -760,6 +773,7 @@ const $users_achievements: Provider = { provide: 'ep:users/achievements', useCla
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
const $users_translate: Provider = { provide: 'ep:users/translate', useClass: ep___users_translate.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@Module({
@ -771,6 +785,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
ApiLoggerService,
ServerStatsService,
$admin_meta,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_abuseUserReports,
$admin_accounts_create,
$admin_accounts_delete,
@ -782,10 +800,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_announcements_delete,
$admin_announcements_list,
$admin_announcements_update,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_avatarDecorations_create,
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,
@ -931,6 +949,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$following_create,
$following_delete,
$following_update,
$following_update_all,
$following_invalidate,
$following_requests_accept,
$following_requests_cancel,
@ -946,6 +965,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$gallery_posts_unlike,
$gallery_posts_update,
$getOnlineUsersCount,
$getAvatarDecorations,
$hashtags_list,
$hashtags_search,
$hashtags_show,
@ -1144,10 +1164,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_updateMemo,
$users_translate,
$fetchRss,
$fetchExternalResources,
$retention,
],
exports: [
$admin_meta,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_abuseUserReports,
$admin_accounts_create,
$admin_accounts_delete,
@ -1159,10 +1184,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_announcements_delete,
$admin_announcements_list,
$admin_announcements_update,
$admin_abuseReportResolver_create,
$admin_abuseReportResolver_delete,
$admin_abuseReportResolver_list,
$admin_abuseReportResolver_update,
$admin_avatarDecorations_create,
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_deleteAllFilesOfAUser,
$admin_drive_cleanRemoteFiles,
$admin_drive_cleanup,
@ -1308,6 +1333,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$following_create,
$following_delete,
$following_update,
$following_update_all,
$following_invalidate,
$following_requests_accept,
$following_requests_cancel,
@ -1323,6 +1349,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$gallery_posts_unlike,
$gallery_posts_update,
$getOnlineUsersCount,
$getAvatarDecorations,
$hashtags_list,
$hashtags_search,
$hashtags_show,
@ -1517,6 +1544,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_updateMemo,
$users_translate,
$fetchRss,
$fetchExternalResources,
$retention,
],
})

View file

@ -7,6 +7,10 @@ import type { Schema } from '@/misc/json-schema.js';
import { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
@ -18,10 +22,10 @@ import * as ep___admin_announcements_create from './endpoints/admin/announcement
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_abuseReportResolver_create from './endpoints/admin/abuse-report-resolver/create.js';
import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-report-resolver/update.js';
import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
@ -167,6 +171,7 @@ import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_update_all from './endpoints/following/update-all.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -182,6 +187,7 @@ import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
import * as ep___hashtags_list from './endpoints/hashtags/list.js';
import * as ep___hashtags_search from './endpoints/hashtags/search.js';
import * as ep___hashtags_show from './endpoints/hashtags/show.js';
@ -380,10 +386,15 @@ import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___users_translate from './endpoints/users/translate.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js';
const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-report-resolver/create', ep___admin_abuseReportResolver_create],
['admin/abuse-report-resolver/list', ep___admin_abuseReportResolver_list],
['admin/abuse-report-resolver/delete', ep___admin_abuseReportResolver_delete],
['admin/abuse-report-resolver/update', ep___admin_abuseReportResolver_update],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete],
@ -395,10 +406,10 @@ const eps = [
['admin/announcements/delete', ep___admin_announcements_delete],
['admin/announcements/list', ep___admin_announcements_list],
['admin/announcements/update', ep___admin_announcements_update],
['admin/abuse-report-resolver/create', ep___admin_abuseReportResolver_create],
['admin/abuse-report-resolver/list', ep___admin_abuseReportResolver_list],
['admin/abuse-report-resolver/delete', ep___admin_abuseReportResolver_delete],
['admin/abuse-report-resolver/update', ep___admin_abuseReportResolver_update],
['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
['admin/drive/cleanup', ep___admin_drive_cleanup],
@ -544,6 +555,7 @@ const eps = [
['following/create', ep___following_create],
['following/delete', ep___following_delete],
['following/update', ep___following_update],
['following/update-all', ep___following_update_all],
['following/invalidate', ep___following_invalidate],
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],
@ -559,6 +571,7 @@ const eps = [
['gallery/posts/unlike', ep___gallery_posts_unlike],
['gallery/posts/update', ep___gallery_posts_update],
['get-online-users-count', ep___getOnlineUsersCount],
['get-avatar-decorations', ep___getAvatarDecorations],
['hashtags/list', ep___hashtags_list],
['hashtags/search', ep___hashtags_search],
['hashtags/show', ep___hashtags_show],
@ -757,6 +770,7 @@ const eps = [
['users/update-memo', ep___users_updateMemo],
['users/translate', ep___users_translate],
['fetch-rss', ep___fetchRss],
['fetch-external-resources', ep___fetchExternalResources],
['retention', ep___retention],
];

View file

@ -124,8 +124,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
ps.expiresAt === '1year' ? function () { expirationDate!.setUTCFullYear(expirationDate!.getUTCFullYear() + 1); } : function () { expirationDate = null; })();
return await this.abuseReportResolverRepository.insert({
id: this.idService.genId(),
createdAt: now,
id: this.idService.gen(),
updatedAt: now,
name: ps.name,
targetUserPattern: ps.targetUserPattern,

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
url: { type: 'string', minLength: 1 },
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
},
required: ['name', 'description', 'url'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.create({
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
});
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.delete(ps.id, me);
});
}
}

View file

@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const avatarDecorations = await this.avatarDecorationService.getAll(true);
return avatarDecorations.map(avatarDecoration => ({
id: avatarDecoration.id,
createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(),
updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null,
name: avatarDecoration.name,
description: avatarDecoration.description,
url: avatarDecoration.url,
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
}));
});
}
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
url: { type: 'string', minLength: 1 },
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.update(ps.id, {
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
});
}
}

View file

@ -8,7 +8,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ApiError } from '../../../error.js';
@ -25,6 +24,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
},
},
} as const;
@ -62,7 +66,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
@ -70,6 +73,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${secureRndstr(8)}_`;
const isDuplicate = await this.customEmojiService.checkDuplicate(name);
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
const emoji = await this.customEmojiService.add({
driveFile,
name,
@ -80,11 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
localOnly: ps.localOnly ?? false,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id,
});
}, me);
return this.emojiEntityService.packDetailed(emoji);
});

View file

@ -110,11 +110,11 @@ export const meta = {
optional: false, nullable: true,
},
silencedHosts: {
type: "array",
type: 'array',
optional: true,
nullable: false,
items: {
type: "string",
type: 'string',
optional: false,
nullable: false,
},
@ -351,6 +351,10 @@ export const meta = {
type: 'object',
optional: false, nullable: false,
},
enableFanoutTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
perLocalUserUserTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
@ -516,6 +520,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
emailToReceiveAbuseReport: instance.emailToReceiveAbuseReport,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride,
enableFanoutTimeline: instance.enableFanoutTimeline,
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,

View file

@ -143,6 +143,7 @@ export const paramDef = {
serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } },
manifestJsonOverride: { type: 'string' },
enableFanoutTimeline: { type: 'boolean' },
perLocalUserUserTimelineCacheMax: { type: 'integer' },
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
@ -595,6 +596,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride;
}
if (ps.enableFanoutTimeline !== undefined) {
set.enableFanoutTimeline = ps.enableFanoutTimeline;
}
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
}

View file

@ -36,7 +36,7 @@ export const paramDef = {
blocked: { type: 'boolean', nullable: true },
notResponding: { type: 'boolean', nullable: true },
suspended: { type: 'boolean', nullable: true },
silenced: { type: "boolean", nullable: true },
silenced: { type: 'boolean', nullable: true },
federating: { type: 'boolean', nullable: true },
subscribing: { type: 'boolean', nullable: true },
publishing: { type: 'boolean', nullable: true },
@ -103,18 +103,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (typeof ps.silenced === "boolean") {
if (typeof ps.silenced === 'boolean') {
const meta = await this.metaService.fetch(true);
if (ps.silenced) {
if (meta.silencedHosts.length === 0) {
return [];
}
query.andWhere("instance.host IN (:...silences)", {
query.andWhere('instance.host IN (:...silences)', {
silences: meta.silencedHosts,
});
} else if (meta.silencedHosts.length > 0) {
query.andWhere("instance.host NOT IN (:...silences)", {
query.andWhere('instance.host NOT IN (:...silences)', {
silences: meta.silencedHosts,
});
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createHash } from 'crypto';
import ms from 'ms';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { ApiError } from '../error.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 50,
},
errors: {
invalidSchema: {
message: 'External resource returned invalid schema.',
code: 'EXT_RESOURCE_RETURNED_INVALID_SCHEMA',
id: 'bb774091-7a15-4a70-9dc5-6ac8cf125856',
},
hashUnmached: {
message: 'Hash did not match.',
code: 'EXT_RESOURCE_HASH_DIDNT_MATCH',
id: '693ba8ba-b486-40df-a174-72f8279b56a4',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
url: { type: 'string' },
hash: { type: 'string' },
},
required: ['url', 'hash'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps) => {
const res = await this.httpRequestService.getJson<{
type: string;
data: string;
}>(ps.url);
if (!res.data || !res.type) {
throw new ApiError(meta.errors.invalidSchema);
}
const resHash = createHash('sha512').update(res.data.replace(/\r\n/g, '\n')).digest('hex');
if (resHash !== ps.hash) {
throw new ApiError(meta.errors.hashUnmached);
}
return {
type: res.type,
data: res.data,
};
});
}
}

View file

@ -71,7 +71,7 @@ export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean' }
withReplies: { type: 'boolean' },
},
required: ['userId'],
} as const;

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 10,
},
requireCredential: true,
kind: 'write:following',
} as const;
export const paramDef = {
type: 'object',
properties: {
notify: { type: 'string', enum: ['normal', 'none'] },
withReplies: { type: 'boolean' },
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
) {
super(meta, paramDef, async (ps, me) => {
await this.followingsRepository.update({
followerId: me.id,
}, {
notify: ps.notify != null ? (ps.notify === 'none' ? null : ps.notify) : undefined,
withReplies: ps.withReplies != null ? ps.withReplies : undefined,
});
return;
});
}
}

View file

@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['users'],
requireCredential: false,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const decorations = await this.avatarDecorationService.getAll(true);
const allRoles = await this.roleService.getRoles();
return decorations.map(decoration => ({
id: decoration.id,
name: decoration.name,
description: decoration.description,
url: decoration.url,
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
}));
});
}
}

View file

@ -32,6 +32,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
@ -131,6 +132,15 @@ export const paramDef = {
birthday: { ...birthdaySchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
avatarId: { type: 'string', format: 'misskey:id', nullable: true },
avatarDecorations: { type: 'array', maxItems: 1, items: {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 },
flipH: { type: 'boolean', nullable: true },
},
required: ['id'],
} },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
fields: {
type: 'array',
@ -207,6 +217,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
) {
super(meta, paramDef, async (ps, _user, token, flashToken) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@ -296,6 +307,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.bannerBlurhash = null;
}
if (ps.avatarDecorations) {
const decorations = await this.avatarDecorationService.getAll(true);
const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles();
const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id,
angle: d.angle ?? 0,
flipH: d.flipH ?? false,
}));
}
if (ps.pinnedPageId) {
const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
@ -421,9 +447,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const myLink = `${this.config.url}/@${user.username}`;
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
if (includesMyLink) {
const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({

View file

@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
: history.map(m => (m.userId === me.id) ? m.recipientId! : m.userId!);
const query = this.messagingMessagesRepository.createQueryBuilder('message')
.orderBy('message.createdAt', 'DESC');
.orderBy('message.id', 'DESC');
if (ps.group) {
query.where('message.groupId IN (:...groups)', { groups: groups });

View file

@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.sinceDate && ps.untilDate && ps.sinceDate > ps.untilDate) throw new ApiError(meta.errors.invalidParam);
if (ps.sinceDate || ps.sortBy !== 'createdAt') {
if (ps.sinceDate ?? ps.sortBy !== 'createdAt') {
const sinceDate = ps.sinceDate ? new Date(ps.sinceDate) : new Date();
query.andWhere('event.start > :sinceDate', { sinceDate: sinceDate })
.andWhere('(event.end IS NULL OR event.end > :sinceDate)', { sinceDate: sinceDate });
@ -155,7 +155,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.sortBy === 'createdAt') {
query.orderBy('note.createdAt', 'DESC');
query.orderBy('note.id', 'DESC');
} else {
query.orderBy('event.start', 'ASC');
}

View file

@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@ -17,6 +17,8 @@ import { CacheService } from '@/core/CacheService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -68,6 +70,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private noteEntityService: NoteEntityService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
@ -76,6 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -86,163 +92,226 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.stlDisabled);
}
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
const serverSettings = await this.metaService.fetch();
let noteIds: string[];
let shouldFallbackToDb = false;
if (serverSettings.enableFanoutTimeline) {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.renoteMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]);
if (ps.withFiles) {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
shouldFallbackToDb = htlNoteIds.length === 0;
}
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
if (!shouldFallbackToDb) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
let timeline = await query.getMany();
timeline = timeline.filter(note => {
if (note.userId === me.id) {
return true;
}
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
return true;
});
// TODO: フィルタした結果件数が足りなかった場合の対応
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
const followees = await this.userFollowingService.getFollowees(me.id);
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
let noteIds: string[];
let shouldFallbackToDb = false;
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
shouldFallbackToDb = htlNoteIds.length === 0;
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
process.nextTick(() => {
this.activeUsersChart.read(me);
});
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
return await this.noteEntityService.packMany(timeline, me);
let redisTimeline: MiNote[] = [];
if (!shouldFallbackToDb) {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
redisTimeline = await query.getMany();
redisTimeline = redisTimeline.filter(note => {
if (note.userId === me.id) {
return true;
}
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
if (note.renoteId) {
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
if (ps.withRenotes === false) return false;
}
}
return true;
});
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
}
if (redisTimeline.length > 0) {
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(redisTimeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withCats: ps.withCats,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
withCats: ps.withCats,
}, me);
}
});
}
private async getFromDb(ps: {
untilId: string | null,
sinceId: string | null,
limit: number,
includeMyRenotes: boolean,
includeRenotedMyNotes: boolean,
includeLocalRenotes: boolean,
withFiles: boolean,
withReplies: boolean,
withCats: boolean,
}, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
const followingChannels = await this.channelFollowingsRepository.find({
where: {
followerId: me.id,
},
});
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (followingChannels.length > 0) {
const followingChannelIds = followingChannels.map(x => x.followeeId);
query.andWhere('note.channelId IN (:...followingChannelIds) OR note.channelId IS NULL', { followingChannelIds });
} else {
query.andWhere('note.channelId IS NULL');
}
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteUserHost IS NOT NULL');
qb.orWhere('note.renoteId IS NULL');
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.withCats) {
query.andWhere('(select "isCat" from "user" where id = note."userId")');
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
this.activeUsersChart.read(me);
});
return await this.noteEntityService.packMany(timeline, me);
}
}

Some files were not shown because too many files have changed in this diff Show more