Merge branch 'fix-msg-room' of https://github.com/misskey-dev/misskey into fix-msg-room

This commit is contained in:
tamaina 2022-05-08 20:14:52 +09:00
commit 09e515aec9
334 changed files with 7340 additions and 8445 deletions

View File

@ -8,18 +8,32 @@ on:
pull_request: pull_request:
jobs: jobs:
lint: backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: 12.x node-version: 18.x
- uses: actions/cache@v2 cache: 'yarn'
with: cache-dependency-path: |
path: '**/node_modules' packages/backend/yarn.lock
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: yarn install - run: yarn install
- run: yarn lint - run: yarn --cwd ./packages/backend lint
client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
cache-dependency-path: |
packages/client/yarn.lock
- run: yarn install
- run: yarn --cwd ./packages/client lint

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [18.x]
services: services:
postgres: postgres:
@ -33,9 +33,13 @@ jobs:
with: with:
submodules: true submodules: true
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'yarn'
cache-dependency-path: |
packages/backend/yarn.lock
packages/client/yarn.lock
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
- name: Check yarn.lock - name: Check yarn.lock
@ -53,7 +57,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [16.x] node-version: [18.x]
browser: [chrome] browser: [chrome]
services: services:
@ -80,13 +84,13 @@ jobs:
#- uses: browser-actions/setup-firefox@latest #- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }} # if: ${{ matrix.browser == 'firefox' }}
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- uses: actions/cache@v2 cache: 'yarn'
with: cache-dependency-path: |
path: '**/node_modules' packages/backend/yarn.lock
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} packages/client/yarn.lock
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
- name: Check yarn.lock - name: Check yarn.lock

View File

@ -1 +1 @@
v16.14.0 v18.0.0

View File

@ -2,7 +2,6 @@
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### Improvements ### Improvements
-
### Bugfixes ### Bugfixes
- -
@ -11,14 +10,69 @@ You should also include the user name that made the change.
--> -->
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### NOTE
- From this version, Node 18.0.0 or later is required.
### Improvements ### Improvements
- Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo - enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- Check that installed Node.js version fulfills version requirement @ThatOneCalculator - enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
- Server: performance improvements @syuilo - enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
### Bugfixes ### Bugfixes
- Client: fix settings page @tamaina
- Client: fix profile tabs @futchitwo
- Server: await promises when following or unfollowing users @Johann150
- Client: fix abuse reports page to be able to show all reports @Johann150
- Federation: Add rel attribute to host-meta @mei23
## 12.110.1 (2022/04/23)
### Bugfixes
- Fix GOP rendering @syuilo
- Improve performance of antenna, clip, and list @xianonn
## 12.110.0 (2022/04/11)
### Improvements
- Improve webhook @syuilo
- Client: Show loading icon on splash screen @syuilo
### Bugfixes
- API: parameter validation of users/show was wrong
- Federation: リモートインスタンスへのダイレクト投稿が届かない問題を修正 @syuilo
## 12.109.2 (2022/04/03)
### Bugfixes
- API: admin/update-meta was not working @syuilo
- Client: テーマを切り替えたり読み込んだりするとmeta[name="theme-color"]のcontentがundefinedになる問題を修正 @tamaina
## 12.109.1 (2022/04/02)
### Bugfixes
- API: Renoteが行えない問題を修正
## 12.109.0 (2022/04/02)
### Improvements
- Webhooks @syuilo
- Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo
- Bull Dashboardを開くには、最初だけ一旦ログアウトしてから再度管理者権限を持つアカウントでログインする必要があります
- Check that installed Node.js version fulfills version requirement @ThatOneCalculator
- Server: overall performance improvements @syuilo
- Federation: avoid duplicate activity delivery @Johann150
- Federation: limit federation of reactions on direct notes @Johann150
- Client: タッチパッド・タッチスクリーンでのデッキの操作性を向上 @tamaina
### Bugfixes
- email address validation was not working @ybw2016v
- API: fix endpoint endpoint @Johann150 - API: fix endpoint endpoint @Johann150
- API: fix admin/meta endpoint @syuilo
- API: improved validation and documentation for endpoints that accept different variants of input @Johann150
- API: `notes/create`: The `mediaIds` property is now deprecated. @Johann150
- Use `fileIds` instead, it has the same behaviour.
- Client: URIエンコーディングが異常でdecodeURIComponentが失敗するとURLが表示できなくなる問題を修正 @tamaina
## 12.108.1 (2022/03/12) ## 12.108.1 (2022/03/12)

View File

@ -62,6 +62,21 @@ Be willing to comment on the good points and not just the things you want fixed
- Are there any omissions or gaps? - Are there any omissions or gaps?
- Does it check for anomalies? - Does it check for anomalies?
## Merge
For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase.
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
## Release
For now, basically only @syuilo has the authority to release Misskey.
However, in case of emergency, a release can be made at the discretion of a contributor.
### Release Instructions
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
2. follow the `master` branch to the `develop` branch.
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
- The target branch must be `master`
- The tag name must be the version
## Localization (l10n) ## Localization (l10n)
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.
You can improve our translations with your Crowdin account. You can improve our translations with your Crowdin account.

View File

@ -1,4 +1,4 @@
FROM node:16.14.0-alpine3.15 AS base FROM node:18.0.0-alpine3.15 AS base
ENV NODE_ENV=production ENV NODE_ENV=production

116
README.md
View File

@ -50,119 +50,3 @@
<div align="center"> <div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a> <a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div> </div>
## Backers
<!-- PATREON_START -->
<table><tr>
<td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/27648259" alt="みなしま " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=20832595">Roujo </a></td>
<td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td>
<td><a href="https://www.patreon.com/weepjp">weepjp </a></td>
<td><a href="https://www.patreon.com/user?u=19045173">kiritan </a></td>
<td><a href="https://www.patreon.com/user?u=27648259">みなしま </a></td>
<td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/4.jpg?token-time=2145916800&token-hash=BslMqDjTjz8KYANLvxL87agHTugHa0dMPUzT-hwR6Vk%3D" alt="Nesakko" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Demogrognard" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3075183/c2ae575c604e420297f000ccc396e395/1.jpeg?token-time=2145916800&token-hash=O9qmPtpo6wWb0OuvnkEekhk_1WO2MTdytLr7ZgsAr80%3D" alt="Liaizon Wakest" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon " width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/36813045/29876ea679d443bcbba3c3f16edab8c2/2.jpeg?token-time=2145916800&token-hash=YCKWnIhrV9rjUCV9KqtJnEqjy_uGYF3WMXftjUdpi7o%3D" alt="Wataru Manji (manji0)" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Demogrognard</a></td>
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
<td><a href="https://www.patreon.com/user?u=557245">mkatze </a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y </a></td>
<td><a href="https://www.patreon.com/AureoleArk">AureoleArk </a></td>
<td><a href="https://www.patreon.com/osapon">osapon </a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ </a></td>
<td><a href="https://www.patreon.com/user?u=36813045">Wataru Manji (manji0)</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/38837364/9421361c54c645ac8f5fc442a40c32e9/1.png?token-time=2145916800&token-hash=TUZB48Nem3BeUPLBH6s3P6WyKBnQOy0xKaDSTBBUNzA%3D" alt="xianon" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro " width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61 </a></td>
<td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
<td><a href="https://www.patreon.com/user?u=28779508">S Y</a></td>
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin </a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
<td><a href="https://www.patreon.com/user?u=38837364">xianon</a></td>
<td><a href="https://www.patreon.com/user?u=26340354">totokoro </a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26144593/9514b10a5c1b42a3af58621aee213d1d/1.png?token-time=2145916800&token-hash=v1PYRsjzu4c_mndN4Hvi_dlispZJsuGRCQeNS82pUSM%3D" alt="EBISUME" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo " width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s </a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
<td><a href="https://www.patreon.com/user?u=20494440">axtuki1 </a></td>
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
<td><a href="https://www.patreon.com/takimura">takimura </a></td>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
<td><a href="https://www.patreon.com/user?u=9109588">nafuchoco </a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/user?u=26144593">EBISUME</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo </a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/14661394" alt="Chandler " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Corset">CG </a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
<td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
<td><a href="https://www.patreon.com/user?u=14661394">Chandler </a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi </a></td>
<td><a href="https://www.patreon.com/efertone">Efertone </a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Sun, 26 Jul 2020 07:00:10 UTC
<!-- PATREON_END -->
[backer-url]: #backers
[backer-badge]: https://opencollective.com/misskey/backers/badge.svg
[backers-image]: https://opencollective.com/misskey/backers.svg
[sponsor-url]: #sponsors
[sponsor-badge]: https://opencollective.com/misskey/sponsors/badge.svg
[sponsors-image]: https://opencollective.com/misskey/sponsors.svg
[support-url]: https://opencollective.com/misskey#support
[syuilo-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70

View File

@ -26,3 +26,11 @@ Once Phase 1 is complete and an environment conducive to the development of a st
Once the development of the feature has settled down, this may be an opportunity to make larger modifications. Once the development of the feature has settled down, this may be an opportunity to make larger modifications.
- Rewriting in Rust? - Rewriting in Rust?
## (4) Change the world
It is time to promote Misskey and change the world.
- Become more major than services such as Twitter and become critical infrastructure for the world
- MiOS will be developed and integrated into various systems - What is MiOS?
- Letting Ai-chan interfere with the real world
- Make Misskey a member of GAFA; Misskey's office must be a reinforced concrete brutalist building with a courtyard.

View File

@ -37,7 +37,6 @@ gulp.task('copy:client:locales', cb => {
gulp.task('build:backend:script', () => { gulp.task('build:backend:script', () => {
return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js'])
.pipe(replace('VERSION', JSON.stringify(meta.version)))
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) .pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(terser({ .pipe(terser({
toplevel: true toplevel: true

View File

@ -189,7 +189,7 @@ clearCachedFiles: "امسح التخزين المؤقت"
clearCachedFilesConfirm: "أتريد حذف التخزين المؤقت للملفات البعيدة؟" clearCachedFilesConfirm: "أتريد حذف التخزين المؤقت للملفات البعيدة؟"
blockedInstances: "المثلاء المحجوبون" blockedInstances: "المثلاء المحجوبون"
blockedInstancesDescription: "قائمة بالمثلاء التي تريد حظرها بحيث كل نطاق في سطر لوحده. بعد إدراجهم لن يتمكنوا من التفاعل مع هذا المثيل." blockedInstancesDescription: "قائمة بالمثلاء التي تريد حظرها بحيث كل نطاق في سطر لوحده. بعد إدراجهم لن يتمكنوا من التفاعل مع هذا المثيل."
muteAndBlock: "تم كتمها / تم حجبها" muteAndBlock: "المكتومون والمحجوبون"
mutedUsers: "الحسابات المكتومة" mutedUsers: "الحسابات المكتومة"
blockedUsers: "الحسابات المحجوبة" blockedUsers: "الحسابات المحجوبة"
noUsers: "ليس هناك مستخدمون" noUsers: "ليس هناك مستخدمون"
@ -490,7 +490,7 @@ none: "لا شيء"
showInPage: "اعرض في الصفحة" showInPage: "اعرض في الصفحة"
popout: "منبثقة" popout: "منبثقة"
volume: "مستوى الصوت" volume: "مستوى الصوت"
masterVolume: "القرص الرئيسي" masterVolume: "حجم الصوت الرئيس"
details: "التفاصيل" details: "التفاصيل"
chooseEmoji: "اختر إيموجي" chooseEmoji: "اختر إيموجي"
unableToProcess: "يتعذر إكمال العملية" unableToProcess: "يتعذر إكمال العملية"
@ -521,6 +521,7 @@ divider: "فاصل"
addItem: "إضافة عنصر" addItem: "إضافة عنصر"
relays: "المُرَحلات" relays: "المُرَحلات"
addRelay: "إضافة مُرحّل" addRelay: "إضافة مُرحّل"
inboxUrl: "رابط صندوق الوارد"
addedRelays: "المرحلات المضافة" addedRelays: "المرحلات المضافة"
serviceworkerInfo: "يجب أن يفعل لإرسال الإشعارات." serviceworkerInfo: "يجب أن يفعل لإرسال الإشعارات."
deletedNote: "ملاحظة محذوفة" deletedNote: "ملاحظة محذوفة"
@ -533,6 +534,8 @@ enablePlayer: "افتح مشغل الفيديو"
disablePlayer: "أغلق مشغل الفيديو" disablePlayer: "أغلق مشغل الفيديو"
themeEditor: "مصمم القوالب" themeEditor: "مصمم القوالب"
description: "الوصف" description: "الوصف"
describeFile: "أضف تعليقًا توضيحيًا"
enterFileDescription: "أدخل تعليقًا توضيحيًا"
author: "الكاتب" author: "الكاتب"
leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟" leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
manage: "إدارة " manage: "إدارة "
@ -564,6 +567,9 @@ smtpPass: "الكلمة السرية"
emptyToDisableSmtpAuth: "اترك اسم المستخدم وكلمة المرور فارغين لتعطيل التحقق من SMTP" emptyToDisableSmtpAuth: "اترك اسم المستخدم وكلمة المرور فارغين لتعطيل التحقق من SMTP"
smtpSecureInfo: "عطل هذا الخيار عند استخدام STARTTLS" smtpSecureInfo: "عطل هذا الخيار عند استخدام STARTTLS"
wordMute: "حظر الكلمات" wordMute: "حظر الكلمات"
regexpError: "خطأ في التعبير النمطي"
instanceMute: "المثلاء المكتومون"
userSaysSomething: "كتب {name} شيءً"
makeActive: "تفعيل" makeActive: "تفعيل"
display: "المظهر" display: "المظهر"
copy: "نسخ" copy: "نسخ"
@ -590,10 +596,16 @@ reportAbuse: "أبلغ"
reportAbuseOf: "أبلغ عن {name}" reportAbuseOf: "أبلغ عن {name}"
fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها." fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
abuseReported: "أُرسل البلاغ، شكرًا لك" abuseReported: "أُرسل البلاغ، شكرًا لك"
reporter: "المُبلّغ"
reporteeOrigin: "أصل البلاغ"
reporterOrigin: "أصل المُبلّغ"
forwardReport: "وجّه البلاغ إلى المثيل البعيد"
forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول."
send: "أرسل" send: "أرسل"
abuseMarkAsResolved: "علّم البلاغ كمحلول" abuseMarkAsResolved: "علّم البلاغ كمحلول"
openInNewTab: "افتح في لسان جديد" openInNewTab: "افتح في لسان جديد"
defaultNavigationBehaviour: "سلوك الملاحة الافتراضي" defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك"
instanceTicker: "معلومات المثيل الأصلي للملاحظات" instanceTicker: "معلومات المثيل الأصلي للملاحظات"
waitingFor: "في انتظار {x}" waitingFor: "في انتظار {x}"
random: "عشوائي" random: "عشوائي"
@ -624,10 +636,15 @@ no: "لا"
driveFilesCount: "عدد الملفات في قرص التخزين" driveFilesCount: "عدد الملفات في قرص التخزين"
driveUsage: "المستغل من قرص التخزين" driveUsage: "المستغل من قرص التخزين"
noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه." noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه."
alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس"
loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات"
disableShowingAnimatedImages: "لا تشغّل الصور المتحركة" disableShowingAnimatedImages: "لا تشغّل الصور المتحركة"
verificationEmailSent: "أُرسل بريد التحقق. أنقر على الرابط المضمن لإكمال التحقق."
notSet: "لم يعيّن" notSet: "لم يعيّن"
emailVerified: "تُحقّق من بريدك الإلكتروني" emailVerified: "تُحقّق من بريدك الإلكتروني"
noteFavoritesCount: "عدد الملاحظات المفضلة" noteFavoritesCount: "عدد الملاحظات المفضلة"
pageLikesCount: "عدد الصفحات التي أعجبت بها"
pageLikedCount: "عدد صفحاتك المُعجب بها"
contact: "التواصل" contact: "التواصل"
useSystemFont: "استخدم الخط الافتراضية للنظام" useSystemFont: "استخدم الخط الافتراضية للنظام"
clips: "مشابك" clips: "مشابك"
@ -635,6 +652,7 @@ experimentalFeatures: "ميّزات اختبارية"
developer: "المطور" developer: "المطور"
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\"" makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\"" makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني"
wide: "عريض" wide: "عريض"
narrow: "رفيع" narrow: "رفيع"
reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟" reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟"
@ -782,6 +800,7 @@ tenMinutes: "10 دقائق"
oneHour: "ساعة" oneHour: "ساعة"
oneDay: "يوم" oneDay: "يوم"
oneWeek: "أسبوع" oneWeek: "أسبوع"
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
_emailUnavailable: _emailUnavailable:
used: "هذا البريد الإلكتروني مستخدم" used: "هذا البريد الإلكتروني مستخدم"
format: "صيغة البريد الإلكتروني غير صالحة" format: "صيغة البريد الإلكتروني غير صالحة"
@ -860,6 +879,7 @@ _mfm:
centerDescription: "يمركز المحتوى في الوَسَط." centerDescription: "يمركز المحتوى في الوَسَط."
quote: "اقتبس" quote: "اقتبس"
emoji: "إيموجي مخصص" emoji: "إيموجي مخصص"
emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي."
search: "البحث" search: "البحث"
flip: "اقلب" flip: "اقلب"
flipDescription: "يقلب المحتوى عموديًا أو أفقيًا" flipDescription: "يقلب المحتوى عموديًا أو أفقيًا"
@ -871,15 +891,28 @@ _mfm:
jumpDescription: "يمنح للمحتوى حركة قفز." jumpDescription: "يمنح للمحتوى حركة قفز."
bounce: "تأثير (ارتداد)" bounce: "تأثير (ارتداد)"
bounceDescription: "يمنح للمحتوى حركة ارتدادية" bounceDescription: "يمنح للمحتوى حركة ارتدادية"
shake: "تأثير (اهتزاز)"
shakeDescription: "يمنح المحتوى حركة اهتزازية."
spin: "تأثير (دوران)"
spinDescription: "يمنح المحتوى حركة دورانية."
x2: "كبير" x2: "كبير"
x2Description: "يُكبر المحتوى"
x3: "كبير جداً" x3: "كبير جداً"
x3Description: "يُضخم المحتوى"
x4: "هائل"
x4Description: "يُضخم المحتوى أكثر مما سبق."
blur: "طمس" blur: "طمس"
blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح."
font: "الخط" font: "الخط"
fontDescription: "الخط المستخدم لعرض المحتوى."
rainbow: "قوس قزح" rainbow: "قوس قزح"
rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف" rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
rotate: "تدوير" rotate: "تدوير"
rotateDescription: "يُدير المحتوى بزاوية معيّنة."
_instanceTicker: _instanceTicker:
none: "لا تظهره بتاتًا"
remote: "أظهر للمستخدمين البِعاد" remote: "أظهر للمستخدمين البِعاد"
always: "أظهره دائمًا"
_serverDisconnectedBehavior: _serverDisconnectedBehavior:
reload: "إعادة تحميل تلقائية" reload: "إعادة تحميل تلقائية"
dialog: "أظهر مربع حوار التحذيرات" dialog: "أظهر مربع حوار التحذيرات"
@ -899,12 +932,18 @@ _menuDisplay:
hide: "إخفاء" hide: "إخفاء"
_wordMute: _wordMute:
muteWords: "الكلمات المحظورة" muteWords: "الكلمات المحظورة"
muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"."
muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية"
softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني." softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني."
hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط." hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط."
soft: "لينة" soft: "لينة"
hard: "قاسية" hard: "قاسية"
mutedNotes: "الملاحظات المكتومة" mutedNotes: "الملاحظات المكتومة"
_instanceMute:
instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب."
instanceMuteDescription2: "مدخلة لكل سطر"
title: "يخفي ملاحظات الخوادم المسرودة."
heading: "قائمة الخوادم المحجوبة"
_theme: _theme:
explore: "استكشف قوالب المظهر" explore: "استكشف قوالب المظهر"
install: "تنصيب قالب" install: "تنصيب قالب"
@ -1198,6 +1237,8 @@ _pages:
font: "الخط" font: "الخط"
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"
eyeCatchingImageSet: "عيّن صورة مصغّرة"
eyeCatchingImageRemove: "احذف صورة مصغّرة"
chooseBlock: "إضافة كتلة" chooseBlock: "إضافة كتلة"
selectType: "اختر النوع" selectType: "اختر النوع"
enterVariableName: "أدخل اسم المتغيّر" enterVariableName: "أدخل اسم المتغيّر"
@ -1458,6 +1499,7 @@ _notification:
pollVote: "مصوِت شارك في الاستطلاع" pollVote: "مصوِت شارك في الاستطلاع"
receiveFollowRequest: "طلبات المتابعة المتلقاة" receiveFollowRequest: "طلبات المتابعة المتلقاة"
followRequestAccepted: "طلبات المتابعة المقبولة" followRequestAccepted: "طلبات المتابعة المقبولة"
groupInvited: "دعوات الفريق"
app: "إشعارات التطبيقات المرتبطة" app: "إشعارات التطبيقات المرتبطة"
_deck: _deck:
alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا" alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا"

View File

@ -832,6 +832,10 @@ size: "আকার"
numberOfColumn: "কলামের সংখ্যা" numberOfColumn: "কলামের সংখ্যা"
searchByGoogle: "গুগল" searchByGoogle: "গুগল"
indefinitely: "অনির্দিষ্ট" indefinitely: "অনির্দিষ্ট"
tenMinutes: "১০ মিনিট"
oneHour: "১ ঘণ্টা"
oneDay: "একদিন"
oneWeek: "এক সপ্তাহ"
_emailUnavailable: _emailUnavailable:
used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে" used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে"
format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি" format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি"

View File

@ -478,8 +478,8 @@ promote: "Werbung schalten"
numberOfDays: "Anzahl der Tage" numberOfDays: "Anzahl der Tage"
hideThisNote: "Diese Notiz verstecken" hideThisNote: "Diese Notiz verstecken"
showFeaturedNotesInTimeline: "Beliebte Notizen in der Chronik anzeigen" showFeaturedNotesInTimeline: "Beliebte Notizen in der Chronik anzeigen"
objectStorage: "Objektspeicher" objectStorage: "Object Storage"
useObjectStorage: "Objektspeicher verwenden" useObjectStorage: "Object Storage verwenden"
objectStorageBaseUrl: "Basis-URL" objectStorageBaseUrl: "Basis-URL"
objectStorageBaseUrlDesc: "Die als Referenz verwendete URL. Verwendest du einen CDN oder Proxy, gib dessen URL an. Für S3 verwende 'https://<bucket>.s3.amazonaws.com'. Für GCS o.ä. verwende 'https://storage.googleapis.com/<bucket>'." objectStorageBaseUrlDesc: "Die als Referenz verwendete URL. Verwendest du einen CDN oder Proxy, gib dessen URL an. Für S3 verwende 'https://<bucket>.s3.amazonaws.com'. Für GCS o.ä. verwende 'https://storage.googleapis.com/<bucket>'."
objectStorageBucket: "Bucket" objectStorageBucket: "Bucket"
@ -827,7 +827,7 @@ overridedDeviceKind: "Gerätetyp"
smartphone: "Smartphone" smartphone: "Smartphone"
tablet: "Tablet" tablet: "Tablet"
auto: "Automatisch" auto: "Automatisch"
themeColor: "Instanzfarbe" themeColor: "Farbe der Instanz-Information"
size: "Größe" size: "Größe"
numberOfColumn: "Spaltenanzahl" numberOfColumn: "Spaltenanzahl"
searchByGoogle: "Googlen" searchByGoogle: "Googlen"
@ -840,6 +840,8 @@ tenMinutes: "10 Minuten"
oneHour: "Eine Stunde" oneHour: "Eine Stunde"
oneDay: "Einen Tag" oneDay: "Einen Tag"
oneWeek: "Eine Woche" oneWeek: "Eine Woche"
reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt."
failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden"
_emailUnavailable: _emailUnavailable:
used: "Diese Email-Adresse wird bereits verwendet" used: "Diese Email-Adresse wird bereits verwendet"
format: "Das Format dieser Email-Adresse ist ungültig" format: "Das Format dieser Email-Adresse ist ungültig"

View File

@ -827,7 +827,7 @@ overridedDeviceKind: "Device type"
smartphone: "Smartphone" smartphone: "Smartphone"
tablet: "Tablet" tablet: "Tablet"
auto: "Auto" auto: "Auto"
themeColor: "Theme Color" themeColor: "Instance Ticker Color"
size: "Size" size: "Size"
numberOfColumn: "Number of columns" numberOfColumn: "Number of columns"
searchByGoogle: "Google" searchByGoogle: "Google"
@ -840,6 +840,8 @@ tenMinutes: "10 minutes"
oneHour: "One hour" oneHour: "One hour"
oneDay: "One day" oneDay: "One day"
oneWeek: "One week" oneWeek: "One week"
reflectMayTakeTime: "It may take some time for this to be reflected."
failedToFetchAccountInformation: "Could not fetch account information"
_emailUnavailable: _emailUnavailable:
used: "This email address is already being used" used: "This email address is already being used"
format: "The format of this email address is invalid" format: "The format of this email address is invalid"

File diff suppressed because it is too large Load Diff

View File

@ -1216,7 +1216,7 @@ _poll:
votesCount: "{n} votes" votesCount: "{n} votes"
totalVotes: "{n} votes au total" totalVotes: "{n} votes au total"
vote: "Voter" vote: "Voter"
showResult: "Voir les résultats" showResult: "Voir résultats"
voted: "Déjà voté" voted: "Déjà voté"
closed: "Terminé" closed: "Terminé"
remainingDays: "{d} jours, {h} heures restantes" remainingDays: "{d} jours, {h} heures restantes"

View File

@ -592,6 +592,7 @@ smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP"
smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS"
testEmail: "Tes pengiriman surel" testEmail: "Tes pengiriman surel"
wordMute: "Bisukan kata" wordMute: "Bisukan kata"
regexpError: "Kesalahan ekspresi reguler"
instanceMute: "Bisuka instansi" instanceMute: "Bisuka instansi"
userSaysSomething: "{name} mengatakan sesuatu" userSaysSomething: "{name} mengatakan sesuatu"
makeActive: "Aktifkan" makeActive: "Aktifkan"
@ -825,8 +826,20 @@ overridedDeviceKind: "Tipe perangkat"
smartphone: "Ponsel" smartphone: "Ponsel"
tablet: "Tablet" tablet: "Tablet"
auto: "Otomatis" auto: "Otomatis"
themeColor: "Warna Tema"
size: "Ukuran"
numberOfColumn: "Jumlah per kolom"
searchByGoogle: "Penelusuran" searchByGoogle: "Penelusuran"
instanceDefaultLightTheme: "Bawaan instan tema terang"
instanceDefaultDarkTheme: "Bawaan instan tema gelap"
instanceDefaultThemeDescription: "Masukkan kode tema di format obyek."
mutePeriod: "Batas waktu bisu"
indefinitely: "Selamanya" indefinitely: "Selamanya"
tenMinutes: "10 Menit"
oneHour: "1 Jam"
oneDay: "1 Hari"
oneWeek: "1 Bulan"
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
_emailUnavailable: _emailUnavailable:
used: "Alamat surel ini telah digunakan" used: "Alamat surel ini telah digunakan"
format: "Format tidak valid." format: "Format tidak valid."
@ -1599,6 +1612,7 @@ _notification:
youReceivedFollowRequest: "Kamu menerima permintaan mengikuti" youReceivedFollowRequest: "Kamu menerima permintaan mengikuti"
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
youWereInvitedToGroup: "Telah diundang ke grup" youWereInvitedToGroup: "Telah diundang ke grup"
pollEnded: "Hasil Kuesioner telah keluar"
_types: _types:
all: "Semua" all: "Semua"
follow: "Ikuti" follow: "Ikuti"
@ -1608,6 +1622,7 @@ _notification:
quote: "Kutip" quote: "Kutip"
reaction: "Reaksi" reaction: "Reaksi"
pollVote: "Memilih di angket" pollVote: "Memilih di angket"
pollEnded: "Jajak pendapat berakhir"
receiveFollowRequest: "Permintaan mengikuti diterima" receiveFollowRequest: "Permintaan mengikuti diterima"
followRequestAccepted: "Permintaan mengikuti disetujui" followRequestAccepted: "Permintaan mengikuti disetujui"
groupInvited: "Diundang ke grup" groupInvited: "Diundang ke grup"

View File

@ -19,7 +19,6 @@ const languages = [
'da-DK', 'da-DK',
'de-DE', 'de-DE',
'en-US', 'en-US',
'eo-UY',
'es-ES', 'es-ES',
'fr-FR', 'fr-FR',
'id-ID', 'id-ID',

View File

@ -356,7 +356,7 @@ antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する" notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ" withFileAntenna: "ファイルが添付されたノートのみ"
enableServiceworker: "ServiceWorkerを有効にする" enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します" antennaUsersDescription: "ユーザー名を改行で区切って指定します"
caseSensitive: "大文字小文字を区別する" caseSensitive: "大文字小文字を区別する"
withReplies: "返信を含む" withReplies: "返信を含む"
@ -1668,8 +1668,9 @@ _notification:
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
youWereInvitedToGroup: "グループに招待されました" youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
_types: _types:
all: "すべて" all: "すべて"
@ -1686,6 +1687,11 @@ _notification:
groupInvited: "グループに招待された" groupInvited: "グループに招待された"
app: "連携アプリからの通知" app: "連携アプリからの通知"
_actions:
followBack: "フォローバック"
reply: "返信"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ" columnAlign: "カラムの寄せ"

View File

@ -119,6 +119,23 @@ unblock: "Deblokkeren"
suspend: "Opschorten" suspend: "Opschorten"
unsuspend: "Heractiveren" unsuspend: "Heractiveren"
blockConfirm: "Weet je zeker dat je dit account wil blokkeren?" blockConfirm: "Weet je zeker dat je dit account wil blokkeren?"
unblockConfirm: "Ben je zeker dat je deze account wil blokkeren?"
suspendConfirm: "Ben je zeker dat je deze account wil suspenderen?"
unsuspendConfirm: "Ben je zeker dat je deze account wil opnieuw aanstellen?"
flagAsBot: "Markeer dit account als een robot."
flagAsBotDescription: "Als dit account van een programma wordt beheerd, zet deze vlag aan. Het aanzetten helpt andere ontwikkelaars om bijvoorbeeld onbedoelde feedback loops te doorbreken of om Misskey meer geschikt te maken."
flagAsCat: "Markeer dit account als een kat."
flagAsCatDescription: "Zet deze vlag aan als je wilt aangeven dat dit account een kat is."
flagShowTimelineReplies: "Toon antwoorden op de tijdlijn."
flagShowTimelineRepliesDescription: "Als je dit vlag aanzet, toont de tijdlijn ook antwoorden op andere en niet alleen jouw eigen notities."
autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker al volgt."
addAccount: "Account toevoegen"
loginFailed: "Aanmelding mislukt."
showOnRemote: "Toon op de externe instantie."
general: "Algemeen"
wallpaper: "Achtergrond"
setWallpaper: "Achtergrond instellen"
removeWallpaper: "Achtergrond verwijderen"
searchWith: "Zoeken: {q}" searchWith: "Zoeken: {q}"
youHaveNoLists: "Je hebt geen lijsten" youHaveNoLists: "Je hebt geen lijsten"
followConfirm: "Weet je zeker dat je {name} wilt volgen?" followConfirm: "Weet je zeker dat je {name} wilt volgen?"
@ -205,6 +222,8 @@ resetAreYouSure: "Resetten?"
saved: "Opgeslagen" saved: "Opgeslagen"
messaging: "Chat" messaging: "Chat"
upload: "Uploaden" upload: "Uploaden"
keepOriginalUploading: "Origineel beeld behouden."
keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert."
fromDrive: "Van schijf" fromDrive: "Van schijf"
fromUrl: "Van URL" fromUrl: "Van URL"
uploadFromUrl: "Uploaden vanaf een URL" uploadFromUrl: "Uploaden vanaf een URL"
@ -245,9 +264,36 @@ renameFile: "Wijzig bestandsnaam"
folderName: "Mapnaam" folderName: "Mapnaam"
createFolder: "Map aanmaken" createFolder: "Map aanmaken"
renameFolder: "Map hernoemen" renameFolder: "Map hernoemen"
deleteFolder: "Map verwijderen"
addFile: "Bestand toevoegen"
emptyDrive: "Jouw Drive is leeg."
emptyFolder: "Deze map is leeg"
unableToDelete: "Kan niet worden verwijderd"
inputNewFileName: "Voer een nieuwe naam in"
copyUrl: "URL kopiëren"
rename: "Hernoemen"
avatar: "Avatar"
banner: "Banner"
nsfw: "NSFW" nsfw: "NSFW"
whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken"
disconnectedFromServer: "Verbinding met de server onderbroken."
inMb: "in megabytes"
pinnedNotes: "Vastgemaakte notitie" pinnedNotes: "Vastgemaakte notitie"
userList: "Lijsten" userList: "Lijsten"
aboutMisskey: "Over Misskey"
administrator: "Beheerder"
token: "Token"
securityKeyName: "Sleutelnaam"
registerSecurityKey: "Zekerheids-Sleutel registreren"
lastUsed: "Laatst gebruikt"
unregister: "Uitschrijven"
passwordLessLogin: "Inloggen zonder wachtwoord"
resetPassword: "Wachtwoord terugzetten"
newPasswordIs: "Het nieuwe wachtwoord is „{password}”."
reduceUiAnimation: "Verminder beweging in de UI"
share: "Delen"
notFound: "Niet gevonden"
cacheClear: "Cache verwijderen"
smtpHost: "Server" smtpHost: "Server"
smtpUser: "Gebruikersnaam" smtpUser: "Gebruikersnaam"
smtpPass: "Wachtwoord" smtpPass: "Wachtwoord"

View File

@ -1,6 +1,7 @@
--- ---
_lang_: "Português" _lang_: "Português"
headlineMisskey: "Rede conectada por notas" headlineMisskey: "Rede conectada por notas"
introMisskey: "Bem-vindo! Misskey é um serviço de microblogue descentralizado de código aberto.\nCria \"notas\" e partilha o que te ocorre com todos à tua volta. 📡\nCom \"reações\" podes também expressar logo o que sentes às notas de todos. 👍\nExploremos um novo mundo! 🚀"
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Pesquisar" search: "Pesquisar"
notifications: "Notificações" notifications: "Notificações"
@ -22,6 +23,7 @@ otherSettings: "Outras configurações"
openInWindow: "Abrir numa janela" openInWindow: "Abrir numa janela"
profile: "Perfil" profile: "Perfil"
timeline: "Timeline" timeline: "Timeline"
noAccountDescription: "Este usuário não tem uma descrição."
login: "Iniciar sessão" login: "Iniciar sessão"
loggingIn: "Iniciando sessão…" loggingIn: "Iniciando sessão…"
logout: "Sair" logout: "Sair"
@ -29,8 +31,12 @@ signup: "Registrar-se"
uploading: "Enviando…" uploading: "Enviando…"
save: "Guardar" save: "Guardar"
users: "Usuários" users: "Usuários"
addUser: "Adicionar usuário"
favorite: "Favoritar" favorite: "Favoritar"
favorites: "Favoritar" favorites: "Favoritar"
unfavorite: "Remover dos favoritos"
favorited: "Adicionado aos favoritos."
alreadyFavorited: "Já adicionado aos favoritos."
showMore: "Ver mais" showMore: "Ver mais"
youGotNewFollower: "Você tem um novo seguidor" youGotNewFollower: "Você tem um novo seguidor"
followRequestAccepted: "Pedido de seguir aceito" followRequestAccepted: "Pedido de seguir aceito"

View File

@ -449,6 +449,56 @@ groupInvited: "Ai fost invitat într-un grup"
aboutX: "Despre {x}" aboutX: "Despre {x}"
useOsNativeEmojis: "Folosește emojiuri native OS-ului" useOsNativeEmojis: "Folosește emojiuri native OS-ului"
disableDrawer: "Nu folosi meniuri în stil sertar" disableDrawer: "Nu folosi meniuri în stil sertar"
youHaveNoGroups: "Nu ai niciun grup"
joinOrCreateGroup: "Primește o invitație într-un grup sau creează unul nou."
noHistory: "Nu există istoric"
signinHistory: "Istoric autentificări"
disableAnimatedMfm: "Dezactivează MFM cu animații"
doing: "Se procesează..."
category: "Categorie"
tags: "Etichete"
docSource: "Sursa acestui document"
createAccount: "Creează un cont"
existingAccount: "Cont existent"
regenerate: "Regenerează"
fontSize: "Mărimea fontului"
noFollowRequests: "Nu ai nicio cerere de urmărire în așteptare"
openImageInNewTab: "Deschide imaginile în taburi noi"
dashboard: "Panou de control"
local: "Local"
remote: "Extern"
total: "Total"
weekOverWeekChanges: "Schimbări până săptămâna trecută"
dayOverDayChanges: "Schimbări până ieri"
appearance: "Aspect"
clientSettings: "Setări client"
accountSettings: "Setări cont"
promotion: "Promovat"
promote: "Promovează"
numberOfDays: "Numărul zilelor"
hideThisNote: "Ascunde această notă"
showFeaturedNotesInTimeline: "Arată notele recomandate în cronologii"
objectStorage: "Object Storage"
useObjectStorage: "Folosește Object Storage"
objectStorageBaseUrl: "URL de bază"
objectStorageBaseUrlDesc: "URL-ul este folosit pentru referință. Specifică URL-ul CDN-ului sau Proxy-ului tău dacă folosești unul. Pentru S3 folosește 'https://<bucket>.s3.amazonaws.com' și pentru GCS sau servicii echivalente folosește 'https://storage.googleapis.com/<bucket>', etc."
objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Te rog specifică numele bucket-ului furnizorului tău."
objectStoragePrefix: "Prefix"
objectStoragePrefixDesc: "Fișierele vor fi stocate sub directoare cu acest prefix."
objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Lasă acest câmp gol dacă folosești AWS S3, dacă nu specifică endpoint-ul ca '<host>' sau '<host>:<port>', depinzând de ce serviciu folosești."
objectStorageRegion: "Regiune"
objectStorageRegionDesc: "Specifică o regiune precum 'xx-east-1'. Dacă serviciul tău nu face distincția între regiuni lasă acest câmp gol sau introdu 'us-east-1'."
objectStorageUseSSL: "Folosește SSl"
objectStorageUseSSLDesc: "Oprește această opțiune dacă nu vei folosi HTTPS pentru conexiunile API-ului"
objectStorageUseProxy: "Conectează-te prin Proxy"
objectStorageUseProxyDesc: "Oprește această opțiune dacă vei nu folosi un Proxy pentru conexiunile API-ului"
objectStorageSetPublicRead: "Setează \"public-read\" pentru încărcare"
serverLogs: "Loguri server"
deleteAll: "Șterge tot"
showFixedPostForm: "Arată caseta de postare în vârful cronologie"
newNoteRecived: "Sunt note noi"
sounds: "Sunete" sounds: "Sunete"
listen: "Ascultă" listen: "Ascultă"
none: "Nimic" none: "Nimic"
@ -471,12 +521,54 @@ sort: "Sortează"
ascendingOrder: "Crescător" ascendingOrder: "Crescător"
descendingOrder: "Descrescător" descendingOrder: "Descrescător"
scratchpad: "Scratchpad" scratchpad: "Scratchpad"
scratchpadDescription: "Scratchpad-ul oferă un mediu de experimentare în AiScript. Poți scrie, executa și verifica rezultatele acestuia interacționând cu Misskey în el."
output: "Ieșire"
script: "Script"
disablePagesScript: "Dezactivează AiScript în Pagini"
updateRemoteUser: "Actualizează informațiile utilizatorului extern"
deleteAllFiles: "Șterge toate fișierele"
deleteAllFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele?"
removeAllFollowing: "Dezurmărește toți utilizatorii urmăriți"
removeAllFollowingDescription: "Asta va dez-urmări toate conturile din {host}. Te rog execută asta numai dacă instanța, de ex., nu mai există."
userSuspended: "Acest utilizator a fost suspendat."
userSilenced: "Acest utilizator a fost setat silențios."
yourAccountSuspendedTitle: "Acest cont a fost suspendat"
yourAccountSuspendedDescription: "Acest cont a fost suspendat din cauza încălcării termenilor de serviciu al serverului sau ceva similar. Contactează administratorul dacă ai dori să afli un motiv mai detaliat. Te rog nu crea un cont nou."
menu: "Meniu"
divider: "Separator"
addItem: "Adaugă element"
relays: "Relee"
addRelay: "Adaugă Releu"
inboxUrl: "URL-ul inbox-ului"
addedRelays: "Relee adăugate"
serviceworkerInfo: "Trebuie să fie activat pentru notificări push."
deletedNote: "Notă ștearsă"
invisibleNote: "Note ascunse"
enableInfiniteScroll: "Încarcă mai mult automat"
visibility: "Vizibilitate"
poll: "Sondaj"
useCw: "Ascunde conținutul"
enablePlayer: "Deschide player-ul video"
disablePlayer: "Închide player-ul video"
expandTweet: "Expandează tweet"
themeEditor: "Editor de teme"
description: "Descriere"
describeFile: "Adaugă titrări"
enterFileDescription: "Introdu titrările"
author: "Autor"
leaveConfirm: "Ai schimbări nesalvate. Vrei să renunți la ele?"
manage: "Gestionare"
plugins: "Pluginuri"
deck: "Deck"
undeck: "Părăsește Deck"
useBlurEffectForModal: "Folosește efect de blur pentru modale"
smtpHost: "Gazdă" smtpHost: "Gazdă"
smtpUser: "Nume de utilizator" smtpUser: "Nume de utilizator"
smtpPass: "Parolă" smtpPass: "Parolă"
clearCache: "Golește cache-ul" clearCache: "Golește cache-ul"
info: "Despre" info: "Despre"
user: "Utilizatori" user: "Utilizatori"
administration: "Gestionare"
searchByGoogle: "Caută" searchByGoogle: "Caută"
_email: _email:
_follow: _follow:
@ -487,9 +579,11 @@ _mfm:
emoji: "Emoji personalizat" emoji: "Emoji personalizat"
search: "Caută" search: "Caută"
_theme: _theme:
description: "Descriere"
keys: keys:
mention: "Mențiune" mention: "Mențiune"
renote: "Re-notează" renote: "Re-notează"
divider: "Separator"
_sfx: _sfx:
note: "Note" note: "Note"
notification: "Notificări" notification: "Notificări"

View File

@ -839,6 +839,8 @@ tenMinutes: "10 minút"
oneHour: "1 hodina" oneHour: "1 hodina"
oneDay: "1 deň" oneDay: "1 deň"
oneWeek: "1 týždeň" oneWeek: "1 týždeň"
reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia."
failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte."
_emailUnavailable: _emailUnavailable:
used: "Táto emailová adresa sa už používa" used: "Táto emailová adresa sa už používa"
format: "Formát emailovej adresy je nesprávny" format: "Formát emailovej adresy je nesprávny"

View File

@ -8,12 +8,12 @@ notifications: "通知"
username: "用户名" username: "用户名"
password: "密码" password: "密码"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "联合查询" fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK" ok: "OK"
gotIt: "我明白了" gotIt: "我明白了"
cancel: "取消" cancel: "取消"
enterUsername: "输入用户名" enterUsername: "输入用户名"
renotedBy: "由 {user} 转" renotedBy: "由 {user} 转"
noNotes: "没有帖文" noNotes: "没有帖文"
noNotifications: "无通知" noNotifications: "无通知"
instance: "实例" instance: "实例"
@ -69,7 +69,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导
importRequested: "导入请求已提交,这可能需要花一点时间。" importRequested: "导入请求已提交,这可能需要花一点时间。"
lists: "列表" lists: "列表"
noLists: "列表为空" noLists: "列表为空"
note: "" note: "帖"
notes: "帖子" notes: "帖子"
following: "关注中" following: "关注中"
followers: "关注者" followers: "关注者"
@ -85,7 +85,7 @@ serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。" youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称" enterListName: "输入列表名称"
privacy: "隐私" privacy: "隐私"
makeFollowManuallyApprove: "关注者的关注请求需要批准" makeFollowManuallyApprove: "关注请求需要批准"
defaultNoteVisibility: "默认可见性" defaultNoteVisibility: "默认可见性"
follow: "关注" follow: "关注"
followRequest: "关注申请" followRequest: "关注申请"
@ -143,7 +143,7 @@ flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。" flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。"
flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注" autoAcceptFollowed: "自动允许关注者的关注"
addAccount: "添加账户" addAccount: "添加账户"
loginFailed: "登录失败" loginFailed: "登录失败"
showOnRemote: "转到所在实例显示" showOnRemote: "转到所在实例显示"
@ -162,7 +162,7 @@ recipient: "收件人"
annotation: "注解" annotation: "注解"
federation: "联合" federation: "联合"
instances: "实例" instances: "实例"
registeredAt: "初次观" registeredAt: "初次观"
latestRequestSentAt: "上次发送的请求" latestRequestSentAt: "上次发送的请求"
latestRequestReceivedAt: "上次收到的请求" latestRequestReceivedAt: "上次收到的请求"
latestStatus: "最后状态" latestStatus: "最后状态"
@ -254,7 +254,7 @@ agreeTo: "{0}人同意"
tos: "服务条款" tos: "服务条款"
start: "开始" start: "开始"
home: "首页" home: "首页"
remoteUserCaution: "由于是远程用户,信息不完整。" remoteUserCaution: "由于此用户来自其它实例,显示的信息可能不完整。"
activity: "活动" activity: "活动"
images: "图片" images: "图片"
birthday: "生日" birthday: "生日"
@ -372,7 +372,7 @@ recentlyUpdatedUsers: "最近投稿的用户"
recentlyRegisteredUsers: "最近登录的用户" recentlyRegisteredUsers: "最近登录的用户"
recentlyDiscoveredUsers: "最近发现的用户" recentlyDiscoveredUsers: "最近发现的用户"
exploreUsersCount: "有{count}个用户" exploreUsersCount: "有{count}个用户"
exploreFediverse: "探索Fediverse" exploreFediverse: "探索联邦宇宙"
popularTags: "热门标签" popularTags: "热门标签"
userList: "列表" userList: "列表"
about: "关于" about: "关于"
@ -561,7 +561,7 @@ manage: "管理"
plugins: "插件" plugins: "插件"
deck: "Deck" deck: "Deck"
undeck: "取消Deck" undeck: "取消Deck"
useBlurEffectForModal: "模态框使用模糊效果" useBlurEffectForModal: "对话框使用模糊效果"
useFullReactionPicker: "使用全功能的回应工具栏" useFullReactionPicker: "使用全功能的回应工具栏"
width: "宽度" width: "宽度"
height: "高度" height: "高度"
@ -840,6 +840,8 @@ tenMinutes: "10分钟"
oneHour: "1小时" oneHour: "1小时"
oneDay: "1天" oneDay: "1天"
oneWeek: "1周" oneWeek: "1周"
reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
failedToFetchAccountInformation: "获取账户信息失败"
_emailUnavailable: _emailUnavailable:
used: "已经被使用过" used: "已经被使用过"
format: "无效的格式" format: "无效的格式"
@ -904,7 +906,7 @@ _nsfw:
_mfm: _mfm:
cheatSheet: "MFM代码速查表" cheatSheet: "MFM代码速查表"
intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。" intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。"
dummy: "通过Misskey扩展Fediverse的世界" dummy: "通过Misskey扩展联邦宇宙的世界"
mention: "提及" mention: "提及"
mentionDescription: "可以使用 @+用户名 来指示特定用户" mentionDescription: "可以使用 @+用户名 来指示特定用户"
hashtag: "话题标签" hashtag: "话题标签"
@ -967,7 +969,7 @@ _mfm:
rotateDescription: "旋转指定的角度。" rotateDescription: "旋转指定的角度。"
_instanceTicker: _instanceTicker:
none: "不显示" none: "不显示"
remote: "仅显示远程用户" remote: "仅远程用户"
always: "始终显示" always: "始终显示"
_serverDisconnectedBehavior: _serverDisconnectedBehavior:
reload: "自动重载" reload: "自动重载"
@ -1051,7 +1053,7 @@ _theme:
mention: "提及" mention: "提及"
mentionMe: "提及" mentionMe: "提及"
renote: "转发" renote: "转发"
modalBg: "模态框背景" modalBg: "对话框背景"
divider: "分割线" divider: "分割线"
scrollbarHandle: "滚动条" scrollbarHandle: "滚动条"
scrollbarHandleHover: "滚动条(悬停)" scrollbarHandleHover: "滚动条(悬停)"
@ -1238,7 +1240,7 @@ _visibility:
publicDescription: "您的帖子将出现在全局时间线上" publicDescription: "您的帖子将出现在全局时间线上"
home: "首页" home: "首页"
homeDescription: "仅发送至首页的时间线" homeDescription: "仅发送至首页的时间线"
followers: "关注者" followers: "关注者"
followersDescription: "仅发送至关注者" followersDescription: "仅发送至关注者"
specified: "指定用户" specified: "指定用户"
specifiedDescription: "仅发送至指定用户" specifiedDescription: "仅发送至指定用户"

View File

@ -81,6 +81,8 @@ somethingHappened: "發生錯誤"
retry: "重試" retry: "重試"
pageLoadError: "載入頁面失敗" pageLoadError: "載入頁面失敗"
pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試" pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試"
serverIsDead: "伺服器沒有回應。請稍等片刻,然後重試。"
youShouldUpgradeClient: "請重新載入以使用新版本的客戶端顯示此頁面"
enterListName: "輸入清單名稱" enterListName: "輸入清單名稱"
privacy: "隱私" privacy: "隱私"
makeFollowManuallyApprove: "手動審核追隨請求" makeFollowManuallyApprove: "手動審核追隨請求"
@ -104,6 +106,7 @@ clickToShow: "按一下以顯示"
sensitive: "敏感內容" sensitive: "敏感內容"
add: "新增" add: "新增"
reaction: "情感" reaction: "情感"
reactionSetting: "在選擇器中顯示反應"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
rememberNoteVisibility: "記住貼文可見性" rememberNoteVisibility: "記住貼文可見性"
attachCancel: "移除附件" attachCancel: "移除附件"
@ -138,6 +141,7 @@ flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Misskey內部系統將本帳戶識別為機器人" flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Misskey內部系統將本帳戶識別為機器人"
flagAsCat: "此使用者是貓" flagAsCat: "此使用者是貓"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
autoAcceptFollowed: "自動追隨中使用者的追隨請求" autoAcceptFollowed: "自動追隨中使用者的追隨請求"
addAccount: "添加帳戶" addAccount: "添加帳戶"
loginFailed: "登入失敗" loginFailed: "登入失敗"
@ -599,6 +603,9 @@ reportAbuse: "檢舉"
reportAbuseOf: "檢舉{name}" reportAbuseOf: "檢舉{name}"
fillAbuseReportDescription: "請填寫檢舉的詳細理由。可以的話請附上針對的URL網址。" fillAbuseReportDescription: "請填寫檢舉的詳細理由。可以的話請附上針對的URL網址。"
abuseReported: "回報已送出。感謝您的報告。" abuseReported: "回報已送出。感謝您的報告。"
reporter: "檢舉者"
reporteeOrigin: "檢舉來源"
reporterOrigin: "檢舉者來源"
send: "發送" send: "發送"
abuseMarkAsResolved: "處理完畢" abuseMarkAsResolved: "處理完畢"
openInNewTab: "在新分頁中開啟" openInNewTab: "在新分頁中開啟"
@ -734,6 +741,7 @@ postToGallery: "發佈到相簿"
gallery: "相簿" gallery: "相簿"
recentPosts: "最新貼文" recentPosts: "最新貼文"
popularPosts: "熱門的貼文" popularPosts: "熱門的貼文"
shareWithNote: "在貼文中分享"
ads: "廣告" ads: "廣告"
expiration: "期限" expiration: "期限"
memo: "備忘錄" memo: "備忘錄"
@ -743,14 +751,35 @@ middle: "中"
low: "低" low: "低"
emailNotConfiguredWarning: "沒有設定電子郵件地址" emailNotConfiguredWarning: "沒有設定電子郵件地址"
ratio: "%" ratio: "%"
previewNoteText: "預覽文本"
customCss: "自定義 CSS"
global: "公開" global: "公開"
sent: "發送" sent: "發送"
received: "收取"
searchResult: "搜尋結果"
hashtags: "#tag" hashtags: "#tag"
troubleshooting: "故障排除"
useBlurEffect: "在 UI 上使用模糊效果"
misskeyUpdated: "Misskey 更新完成!"
translate: "翻譯"
translatedFrom: "從 {x} 翻譯"
accountDeletionInProgress: "正在刪除帳戶"
pubSub: "Pub/Sub 帳戶"
resolved: "已解決"
unresolved: "未解決"
breakFollow: "移除追蹤者"
hide: "隱藏" hide: "隱藏"
leaveGroupConfirm: "確定離開「{name}」?"
auto: "自動"
searchByGoogle: "搜尋" searchByGoogle: "搜尋"
indefinitely: "無期限" indefinitely: "無期限"
_ffVisibility: _ffVisibility:
public: "發佈" public: "發佈"
private: "私密"
_signup:
almostThere: "即將完成"
_accountDelete:
inProgress: "正在刪除"
_ad: _ad:
back: "返回" back: "返回"
reduceFrequencyOfThisAd: "降低此廣告的頻率 " reduceFrequencyOfThisAd: "降低此廣告的頻率 "

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "12.108.1", "version": "12.110.1",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,8 +30,6 @@
"cleanall": "npm run clean-all" "cleanall": "npm run clean-all"
}, },
"dependencies": { "dependencies": {
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"execa": "5.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
@ -41,9 +39,11 @@
"js-yaml": "4.1.0" "js-yaml": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "5.16.0", "@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.18.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "9.5.2", "cypress": "9.5.3",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"typescript": "4.6.3" "typescript": "4.6.3"
} }

View File

@ -0,0 +1,21 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}]
},
};

View File

@ -1,9 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
};

View File

@ -2,5 +2,9 @@
"typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsdk": "node_modules\\typescript\\lib",
"path-intellisense.mappings": { "path-intellisense.mappings": {
"@": "${workspaceRoot}/packages/backend/src/" "@": "${workspaceRoot}/packages/backend/src/"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
} }
} }

View File

@ -0,0 +1,19 @@
export class webhook1648548247382 {
name = 'webhook1648548247382'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "webhook" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "on" character varying(128) array NOT NULL DEFAULT '{}', "url" character varying(1024) NOT NULL, "secret" character varying(1024) NOT NULL, "active" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6765510c2d078db49632b59020" PRIMARY KEY ("id")); COMMENT ON COLUMN "webhook"."createdAt" IS 'The created date of the Antenna.'; COMMENT ON COLUMN "webhook"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "webhook"."name" IS 'The name of the Antenna.'`);
await queryRunner.query(`CREATE INDEX "IDX_f272c8c8805969e6a6449c77b3" ON "webhook" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_8063a0586ed1dfbe86e982d961" ON "webhook" ("on") `);
await queryRunner.query(`CREATE INDEX "IDX_5a056076f76b2efe08216ba655" ON "webhook" ("active") `);
await queryRunner.query(`ALTER TABLE "webhook" ADD CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" DROP CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_5a056076f76b2efe08216ba655"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8063a0586ed1dfbe86e982d961"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f272c8c8805969e6a6449c77b3"`);
await queryRunner.query(`DROP TABLE "webhook"`);
}
}

View File

@ -0,0 +1,14 @@
export class webhook21648816172177 {
name = 'webhook21648816172177'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestSentAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestStatus" integer`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestStatus"`);
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestSentAt"`);
}
}

View File

@ -0,0 +1,89 @@
export class foreignKeyReports1651224615271 {
name = 'foreignKeyReports1651224615271'
async up(queryRunner) {
await Promise.all([
queryRunner.query(`ALTER INDEX "public"."IDX_seoignmeoprigmkpodgrjmkpormg" RENAME TO "IDX_c8cc87bd0f2f4487d17c651fbf"`),
queryRunner.query(`DROP INDEX "public"."IDX_note_on_channelId_and_id_desc"`),
// remove unnecessary default null, see also down
queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "followersUri" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "session" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "name" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "description" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "iconUrl" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareName" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareVersion" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "name" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "description" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerName" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerEmail" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "iconUrl" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "faviconUrl" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "themeColor" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "clip" ALTER COLUMN "description" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "channelId" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" DROP DEFAULT`),
queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`),
queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`),
queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => {
queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}),
queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`),
queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`),
queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`),
queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`),
queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`),
queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" TO "FK_a9ca79ad939bf06066b81c9d3aa"`),
queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'pollEnded' AFTER 'pollVote'`),
]);
}
async down(queryRunner) {
await Promise.all([
// There is no ALTER TYPE REMOVE VALUE query, so the reverse operation is a bit more complex
queryRunner.query(`UPDATE "user_profile" SET "mutingNotificationTypes" = array_remove("mutingNotificationTypes", 'pollEnded')`)
.then(() =>
queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`)
).then(() =>
queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`)
).then(() =>
queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`)
).then(() =>
queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`)
).then(() =>
queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`)
).then(() =>
queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`)
),
queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" TO "FK_3126dd7c502c9e4d7597ef7ef10"`),
queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`),
queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`),
queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`),
queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`),
queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`),
queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" SET DEFAULT '{}'`),
queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`),
queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`),
queryRunner.query(`DROP INDEX "public"."IDX_f22169eb10657bded6d875ac8f"`),
queryRunner.query(`DROP INDEX "public"."IDX_315c779174fe8247ab324f036e"`),
/* DEFAULT's are not set again because if the column can be NULL, then DEFAULT NULL is not necessary.
see also https://github.com/typeorm/typeorm/issues/7579#issuecomment-835423615 */
queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("id", "channelId") `),
queryRunner.query(`ALTER INDEX "public"."IDX_c8cc87bd0f2f4487d17c651fbf" RENAME TO "IDX_seoignmeoprigmkpodgrjmkpormg"`),
]);
}
}

View File

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"lint": "eslint --quiet src/**/*.ts", "lint": "eslint --quiet \"src/**/*.ts\"",
"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "npm run mocha" "test": "npm run mocha"
}, },
@ -14,6 +14,7 @@
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"dependencies": { "dependencies": {
"@bull-board/koa": "3.10.4",
"@discordapp/twemoji": "13.1.1", "@discordapp/twemoji": "13.1.1",
"@elastic/elasticsearch": "7.11.0", "@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0", "@koa/cors": "3.1.0",
@ -21,65 +22,19 @@
"@koa/router": "9.0.1", "@koa/router": "9.0.1",
"@sinonjs/fake-timers": "9.1.1", "@sinonjs/fake-timers": "9.1.1",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2", "@typescript-eslint/eslint-plugin": "5.20.0",
"@types/bull": "3.15.8", "@typescript-eslint/parser": "5.20.0",
"@types/cbor": "6.0.0",
"@types/escape-regexp": "0.0.1",
"@types/is-url": "1.2.30",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6",
"@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.2",
"@types/koa-mount": "4.0.1",
"@types/koa-send": "4.1.3",
"@types/koa-views": "7.0.0",
"@types/koa__cors": "3.1.1",
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.0",
"@types/node": "17.0.23",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.3",
"@types/portscanner": "2.1.1",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.3",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.0",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.16.0",
"@typescript-eslint/parser": "5.16.0",
"@bull-board/koa": "3.10.1",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"ajv": "8.11.0", "ajv": "8.11.0",
"archiver": "5.3.0", "archiver": "5.3.1",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1100.0", "aws-sdk": "2.1120.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.10.0", "broadcast-channel": "4.11.0",
"bull": "4.8.1", "bull": "4.8.2",
"cacheable-lookup": "6.0.4", "cacheable-lookup": "6.0.4",
"cafy": "15.2.1",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.0.1", "chalk": "5.0.1",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
@ -89,22 +44,22 @@
"date-fns": "2.28.0", "date-fns": "2.28.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "8.12.0", "eslint": "8.14.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.26.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "17.1.1", "file-type": "17.1.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.0.3", "got": "12.0.3",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"http-signature": "1.3.6", "http-signature": "1.3.6",
"ip-cidr": "3.0.4", "ip-cidr": "3.0.7",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "19.0.0", "jsdom": "19.0.0",
"json5": "2.2.1", "json5": "2.2.1",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "5.2.0", "jsonld": "5.2.0",
"jsrsasign": "8.0.20", "jsrsasign": "10.5.19",
"koa": "2.13.4", "koa": "2.13.4",
"koa-bodyparser": "4.3.0", "koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0", "koa-favicon": "2.1.0",
@ -145,16 +100,15 @@
"rndstr": "1.0.0", "rndstr": "1.0.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.7.0", "sanitize-html": "2.7.0",
"semver": "7.3.5", "semver": "7.3.7",
"sharp": "0.30.3", "sharp": "0.30.4",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"summaly": "2.5.0", "summaly": "2.5.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.11.9", "systeminformation": "5.11.14",
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "9.2.8", "ts-loader": "9.2.8",
@ -162,7 +116,7 @@
"tsc-alias": "1.4.1", "tsc-alias": "1.4.1",
"tsconfig-paths": "3.14.1", "tsconfig-paths": "3.14.1",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.4", "typeorm": "0.3.6",
"typescript": "4.6.3", "typescript": "4.6.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
@ -170,11 +124,56 @@
"web-push": "3.4.5", "web-push": "3.4.5",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.5.0", "ws": "8.5.0",
"xev": "2.0.1" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.90", "@redocly/openapi-core": "1.0.0-beta.93",
"@types/semver": "7.3.9",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.8",
"@types/cbor": "6.0.0",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"@types/is-url": "1.2.30",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6",
"@types/jsrsasign": "10.2.1",
"@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.2",
"@types/koa-mount": "4.0.1",
"@types/koa-send": "4.1.3",
"@types/koa-views": "7.0.0",
"@types/koa__cors": "3.1.1",
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1",
"@types/node": "17.0.25",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.3",
"@types/portscanner": "2.1.1",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.3",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.2",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"execa": "6.1.0" "execa": "6.1.0"
} }

View File

@ -1,5 +1,5 @@
declare module 'http-signature' { declare module 'http-signature' {
import { IncomingMessage, ClientRequest } from 'http'; import { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature { interface ISignature {
keyId: string; keyId: string;

View File

@ -1,800 +0,0 @@
// Attention: Partial Type Definition
declare module 'jsrsasign' {
//// HELPER TYPES
/**
* Attention: The value might be changed by the function.
*/
type Mutable<T> = T;
/**
* Deprecated: The function might be deleted in future release.
*/
type Deprecated<T> = T;
//// COMMON TYPES
/**
* byte number
*/
type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255;
/**
* hexadecimal string /[0-9A-F]/
*/
type HexString = string;
/**
* binary string /[01]/
*/
type BinString = string;
/**
* base64 string /[A-Za-z0-9+/]=+/
*/
type Base64String = string;
/**
* base64 URL encoded string /[A-Za-z0-9_-]/
*/
type Base64URLString = string;
/**
* time value (ex. "151231235959Z")
*/
type TimeValue = string;
/**
* OID string (ex. '1.2.3.4.567')
*/
type OID = string;
/**
* OID name
*/
type OIDName = string;
/**
* PEM formatted string
*/
type PEM = string;
//// ASN1 TYPES
class ASN1Object {
public isModified: boolean;
public hTLV: ASN1TLV;
public hT: ASN1T;
public hL: ASN1L;
public hV: ASN1V;
public getLengthHexFromValue(): HexString;
public getEncodedHex(): ASN1TLV;
public getValueHex(): ASN1V;
public getFreshValueHex(): ASN1V;
}
class DERAbstractStructured extends ASN1Object {
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void;
public appendASN1Object(asn1Object: ASN1Object): void;
}
class DERSequence extends DERAbstractStructured {
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
public getFreshValueHex(): ASN1V;
}
//// ASN1HEX TYPES
/**
* ASN.1 DER encoded data (hexadecimal string)
*/
type ASN1S = HexString;
/**
* index of something
*/
type Idx<T extends { [idx: string]: unknown } | { [idx: number]: unknown }> = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never;
/**
* byte length of something
*/
type ByteLength<T extends { length: unknown }> = T['length'];
/**
* ASN.1 L(length) (hexadecimal string)
*/
type ASN1L = HexString;
/**
* ASN.1 T(tag) (hexadecimal string)
*/
type ASN1T = HexString;
/**
* ASN.1 V(value) (hexadecimal string)
*/
type ASN1V = HexString;
/**
* ASN.1 TLV (hexadecimal string)
*/
type ASN1TLV = HexString;
/**
* ASN.1 object string
*/
type ASN1ObjectString = string;
/**
* nth
*/
type Nth = number;
/**
* ASN.1 DER encoded OID value (hexadecimal string)
*/
type ASN1OIDV = HexString;
class ASN1HEX {
public static getLblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1L>;
public static getL(s: ASN1S, idx: Idx<ASN1S>): ASN1L;
public static getVblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1V>;
public static getVidx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1V>;
public static getV(s: ASN1S, idx: Idx<ASN1S>): ASN1V;
public static getTLV(s: ASN1S, idx: Idx<ASN1S>): ASN1TLV;
public static getNextSiblingIdx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1ObjectString>;
public static getChildIdx(h: ASN1S, pos: Idx<ASN1S>): Idx<ASN1ObjectString>[];
public static getNthChildIdx(h: ASN1S, idx: Idx<ASN1S>, nth: Nth): Idx<ASN1ObjectString>;
public static getIdxbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): Idx<Mutable<Nth[]>>;
public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
// eslint:disable-next-line:bool-param-default
public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
public static hextooidstr(hex: ASN1OIDV): OID;
public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record<string, unknown>, idx?: Idx<ASN1S>, indent?: string): string;
public static isASN1HEX(hex: string): hex is HexString;
public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName;
}
//// BIG INTEGER TYPES (PARTIAL)
class BigInteger {
constructor(a: null);
constructor(a: number, b: SecureRandom);
constructor(a: number, b: number, c: SecureRandom);
constructor(a: unknown);
constructor(a: string, b: number);
public am(i: number, x: number, w: number, j: number, c: number, n: number): number;
public DB: number;
public DM: number;
public DV: number;
public FV: number;
public F1: number;
public F2: number;
protected copyTo(r: Mutable<BigInteger>): void;
protected fromInt(x: number): void;
protected fromString(s: string, b: number): void;
protected clamp(): void;
public toString(b: number): string;
public negate(): BigInteger;
public abs(): BigInteger;
public compareTo(a: BigInteger): number;
public bitLength(): number;
protected dlShiftTo(n: number, r: Mutable<BigInteger>): void;
protected drShiftTo(n: number, r: Mutable<BigInteger>): void;
protected lShiftTo(n: number, r: Mutable<BigInteger>): void;
protected rShiftTo(n: number, r: Mutable<BigInteger>): void;
protected subTo(a: BigInteger, r: Mutable<BigInteger>): void;
protected multiplyTo(a: BigInteger, r: Mutable<BigInteger>): void;
protected squareTo(r: Mutable<BigInteger>): void;
protected divRemTo(m: BigInteger, q: Mutable<BigInteger>, r: Mutable<BigInteger>): void;
public mod(a: BigInteger): BigInteger;
protected invDigit(): number;
protected isEven(): boolean;
protected exp(e: number, z: Classic | Montgomery): BigInteger;
public modPowInt(e: number, m: BigInteger): BigInteger;
public static ZERO: BigInteger;
public static ONE: BigInteger;
}
class Classic {
constructor(m: BigInteger);
public convert(x: BigInteger): BigInteger;
public revert(x: BigInteger): BigInteger;
public reduce(x: Mutable<BigInteger>): void;
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
}
class Montgomery {
constructor(m: BigInteger);
public convert(x: BigInteger): BigInteger;
public revert(x: BigInteger): BigInteger;
public reduce(x: Mutable<BigInteger>): void;
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
}
//// KEYUTIL TYPES
type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
type AlgList = {
'AES-256-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 32; ivlen: 16; };
'AES-192-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 24; ivlen: 16; };
'AES-128-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 16; ivlen: 16; };
'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8; };
'DES-CBC': { 'proc': DecryptDES; 'eproc': EncryptDES; keylen: 8; ivlen: 8; };
};
type AlgName = keyof AlgList;
type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA';
type GetKeyRSAParam = RSAKey | {
n: BigInteger;
e: number;
} | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | {
n: BigInteger;
e: number;
d: BigInteger;
} | {
kty: 'RSA';
} & Record<'n' | 'e', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e' | 'd', Base64URLString>;
type GetKeyECDSAParam = KJUR.crypto.ECDSA | {
curve: KJUR.crypto.CurveName;
xy: HexString;
} | {
curve: KJUR.crypto.CurveName;
d: HexString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
d: Base64URLString;
};
type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>;
type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string;
class KEYUTIL {
public version: '1.0.0';
public parsePKCS5PEM(sPKCS5PEM: PEM): Partial<Record<'type' | 's', string>> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>);
public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>;
public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String;
public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString;
public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM;
public parseHexOfEncryptedPKCS8(sHEX: HexString): {
ciphertext: ASN1V;
encryptionSchemeAlg: 'TripleDES';
encryptionSchemeIV: ASN1V;
pbkdf2Salt: ASN1V;
pbkdf2Iter: number;
};
public getPBKDF2KeyHexFromParam(info: ReturnType<this['parseHexOfEncryptedPKCS8']>, passcode: string): HexString;
private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString;
public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): {
algparam: ASN1V | null;
algoid: ASN1V;
keyidx: Idx<ASN1V>;
};
public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>;
public parsePublicPKCS8Hex(pkcs8PubHex: HexString): {
algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null;
algoid: ASN1V;
key: ASN1V;
};
public static getKey(param: GetKeyRSAParam): RSAKey;
public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA;
public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA;
public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>;
public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>;
public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do
public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>;
public static getJWKFromKey(keyObj: RSAKey): {
kty: 'RSA';
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
kty: 'RSA';
} & Record<'n' | 'e', Base64URLString>;
public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
d: Base64URLString;
} | {
kty: 'EC';
crv: KJUR.crypto.CurveName;
x: Base64URLString;
y: Base64URLString;
};
}
//// KJUR NAMESPACE (PARTIAL)
namespace KJUR {
namespace crypto {
type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1';
class DSA {
public p: BigInteger | null;
public q: BigInteger | null;
public g: BigInteger | null;
public y: BigInteger | null;
public x: BigInteger | null;
public type: 'DSA';
public isPrivate: boolean;
public isPublic: boolean;
public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void;
public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void;
public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void;
public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void;
public signWithMessageHash(sHashHex: HexString): HexString;
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
}
class ECDSA {
constructor(params?: {
curve?: CurveName;
prv?: HexString;
pub?: HexString;
});
public p: BigInteger | null;
public q: BigInteger | null;
public g: BigInteger | null;
public y: BigInteger | null;
public x: BigInteger | null;
public type: 'EC';
public isPrivate: boolean;
public isPublic: boolean;
public getBigRandom(limit: BigInteger): BigInteger;
public setNamedCurve(curveName: CurveName): void;
public setPrivateKeyHex(prvKeyHex: HexString): void;
public setPublicKeyHex(pubKeyHex: HexString): void;
public getPublicKeyXYHex(): Record<'x' | 'y', HexString>;
public getShortNISTPCurveName(): 'P-256' | 'P-384' | null;
public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>;
public signWithMessageHash(hashHex: HexString): HexString;
public signHex(hashHex: HexString, privHex: HexString): HexString;
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>;
public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>;
public static asn1SigToConcatSig(asn1Sig: HexString): HexString;
public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV;
public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV;
public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV;
public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null;
}
class Signature {
constructor(params?: ({
alg: string;
prov?: string;
} | {}) & ({
psssaltlen: number;
} | {}) & ({
prvkeypem: PEM;
prvkeypas?: never;
} | {}));
private _setAlgNames(): void;
private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString;
public setAlgAndProvider(alg: string, prov: string): void;
public init(key: GetKeyParam, pass?: string): void;
public updateString(str: string): void;
public updateHex(hex: HexString): void;
public sign(): HexString;
public signString(str: string): HexString;
public signHex(hex: HexString): HexString;
public verify(hSigVal: string): boolean | 0;
}
}
}
//// RSAKEY TYPES
class RSAKey {
public n: BigInteger | null;
public e: number;
public d: BigInteger | null;
public p: BigInteger | null;
public q: BigInteger | null;
public dmp1: BigInteger | null;
public dmq1: BigInteger | null;
public coeff: BigInteger | null;
public type: 'RSA';
public isPrivate?: boolean;
public isPublic?: boolean;
//// RSA PUBLIC
protected doPublic(x: BigInteger): BigInteger;
public setPublic(N: BigInteger, E: number): void;
public setPublic(N: HexString, E: HexString): void;
public encrypt(text: string): HexString | null;
public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null;
//// RSA PRIVATE
protected doPrivate(x: BigInteger): BigInteger;
public setPrivate(N: BigInteger, E: number, D: BigInteger): void;
public setPrivate(N: HexString, E: HexString, D: HexString): void;
public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void;
public generate(B: number, E: HexString): void;
public decrypt(ctext: HexString): string;
public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null;
//// RSA PEM
public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
public readPrivateKeyFromPEMString(keyPEM: PEM): void;
public readPKCS5PrvKeyHex(h: HexString): void;
public readPKCS8PrvKeyHex(h: HexString): void;
public readPKCS5PubKeyHex(h: HexString): void;
public readPKCS8PubKeyHex(h: HexString): void;
public readCertPubKeyHex(h: HexString, nthPKI: Nth): void;
//// RSA SIGN
public sign(s: string, hashAlg: string): HexString;
public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString;
public signPSS(s: string, hashAlg: string, sLen: number): HexString;
public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString;
public verify(sMsg: string, hSig: HexString): boolean | 0;
public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0;
public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean;
public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean;
public static SALT_LEN_HLEN: -1;
public static SALT_LEN_MAX: -2;
public static SALT_LEN_RECOVER: -2;
}
/// RNG TYPES
class SecureRandom {
public nextBytes(ba: Mutable<ByteNumber[]>): void;
}
//// X509 TYPES
type ExtInfo = {
critical: boolean;
oid: OID;
vidx: Idx<ASN1V>;
};
type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>;
type ExtCertificatePolicy = {
id: OIDName;
} & Partial<{
cps: string;
} | {
unotice: string;
}>;
class X509 {
public hex: HexString | null;
public version: number;
public foffset: number;
public aExtInfo: null;
public getVersion(): number;
public getSerialNumberHex(): ASN1V;
public getSignatureAlgorithmField(): OIDName;
public getIssuerHex(): ASN1TLV;
public getIssuerString(): HexString;
public getSubjectHex(): ASN1TLV;
public getSubjectString(): HexString;
public getNotBefore(): TimeValue;
public getNotAfter(): TimeValue;
public getPublicKeyHex(): ASN1TLV;
public getPublicKeyIdx(): Idx<Mutable<Nth[]>>;
public getPublicKeyContentIdx(): Idx<Mutable<Nth[]>>;
public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public getSignatureAlgorithmName(): OIDName;
public getSignatureValueHex(): ASN1V;
public verifySignature(pubKey: GetKeyParam): boolean | 0;
public parseExt(): void;
public getExtInfo(oidOrName: OID | string): ExtInfo | undefined;
public getExtBasicConstraints(): ExtInfo | {} | {
cA: true;
pathLen?: number;
};
public getExtKeyUsageBin(): BinString;
public getExtKeyUsageString(): string;
public getExtSubjectKeyIdentifier(): ASN1V | undefined;
public getExtAuthorityKeyIdentifier(): {
kid: ASN1V;
} | undefined;
public getExtExtKeyUsageName(): OIDName[] | undefined;
public getExtSubjectAltName(): Deprecated<string[]>;
public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined;
public getExtCRLDistributionPointsURI(): string[] | undefined;
public getExtAIAInfo(): ExtAIAInfo | undefined;
public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined;
public readCertPEM(sCertPEM: PEM): void;
public readCertHex(sCertHex: HexString): void;
public getInfo(): string;
public static hex2dn(hex: HexString, idx?: Idx<HexString>): string;
public static hex2rdn(hex: HexString, idx?: Idx<HexString>): string;
public static hex2attrTypeValue(hex: HexString, idx?: Idx<HexString>): string;
public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): {
algparam: ASN1V | null;
leyhex: ASN1V;
algoid: ASN1V;
};
}
}

View File

@ -1,6 +1,6 @@
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import chalk from 'chalk'; import chalk from 'chalk';
import { default as Xev } from 'xev'; import Xev from 'xev';
import Logger from '@/services/logger.js'; import Logger from '@/services/logger.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
@ -12,7 +12,7 @@ import { workerMain } from './worker.js';
const logger = new Logger('core', 'cyan'); const logger = new Logger('core', 'cyan');
const clusterLogger = logger.createSubLogger('cluster', 'orange', false); const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
const ev = new Xev.default(); const ev = new Xev();
/** /**
* Init process * Init process

View File

@ -25,6 +25,7 @@ const path = process.env.NODE_ENV === 'test'
export default function load() { export default function load() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8'));
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin; const mixin = {} as Mixin;
@ -45,6 +46,7 @@ export default function load() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${meta.version} (${config.url})`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, '');
if (!config.redis.prefix) config.redis.prefix = mixin.host; if (!config.redis.prefix) config.redis.prefix = mixin.host;

View File

@ -80,6 +80,7 @@ export type Mixin = {
authUrl: string; authUrl: string;
driveUrl: string; driveUrl: string;
userAgent: string; userAgent: string;
clientEntry: string;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;

View File

@ -1,7 +1,7 @@
import { default as Xev } from 'xev'; import Xev from 'xev';
import { deliverQueue, inboxQueue } from '../queue/queues.js'; import { deliverQueue, inboxQueue } from '../queue/queues.js';
const ev = new Xev.default(); const ev = new Xev();
const interval = 10000; const interval = 10000;

View File

@ -1,8 +1,8 @@
import si from 'systeminformation'; import si from 'systeminformation';
import { default as Xev } from 'xev'; import Xev from 'xev';
import * as osUtils from 'os-utils'; import * as osUtils from 'os-utils';
const ev = new Xev.default(); const ev = new Xev();
const interval = 2000; const interval = 2000;

View File

@ -73,6 +73,7 @@ import { PasswordResetRequest } from '@/models/entities/password-reset-request.j
import { UserPending } from '@/models/entities/user-pending.js'; import { UserPending } from '@/models/entities/user-pending.js';
import { entities as charts } from '@/services/chart/entities.js'; import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -171,6 +172,7 @@ export const entities = [
Ad, Ad,
PasswordResetRequest, PasswordResetRequest,
UserPending, UserPending,
Webhook,
...charts, ...charts,
]; ];
@ -207,7 +209,11 @@ export const db = new DataSource({
}); });
export async function initDb() { export async function initDb() {
if (db.isInitialized) {
// nop
} else {
await db.connect(); await db.connect();
}
} }
export async function resetDb() { export async function resetDb() {

View File

@ -1,33 +0,0 @@
import { Context } from 'cafy';
// eslint-disable-next-line @typescript-eslint/ban-types
export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
public readonly name = 'ID';
constructor(optional = false, nullable = false) {
super(optional, nullable);
this.push((v: any) => {
if (typeof v !== 'string') {
return new Error('must-be-an-id');
}
return true;
});
}
public getType() {
return super.getType('String');
}
public makeOptional(): ID<undefined> {
return new ID(true, false);
}
public makeNullable(): ID<null> {
return new ID(false, true);
}
public makeOptionalNullable(): ID<undefined | null> {
return new ID(true, true);
}
}

View File

@ -42,7 +42,8 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
headers: { headers: {
'User-Agent': config.userAgent, 'User-Agent': config.userAgent,
}, },
timeout: 10 * 1000, // TODO
//timeout: 10 * 1000,
agent: getAgentByUrl, agent: getAgentByUrl,
}).catch(e => { }).catch(e => {
throw `${e.message || e}`; throw `${e.message || e}`;

View File

@ -1,10 +1,10 @@
import * as http from 'http'; import * as http from 'node:http';
import * as https from 'https'; import * as https from 'node:https';
import { URL } from 'node:url';
import CacheableLookup from 'cacheable-lookup'; import CacheableLookup from 'cacheable-lookup';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { URL } from 'node:url';
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) { export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
const res = await getResponse({ const res = await getResponse({
@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout =
} }
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) { export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
const timeout = args?.timeout || 10 * 1000; const timeout = args.timeout || 10 * 1000;
const controller = new AbortController(); const controller = new AbortController();
setTimeout(() => { setTimeout(() => {
@ -47,7 +47,7 @@ export async function getResponse(args: { url: string, method: string, body?: st
headers: args.headers, headers: args.headers,
body: args.body, body: args.body,
timeout, timeout,
size: args?.size || 10 * 1024 * 1024, size: args.size || 10 * 1024 * 1024,
agent: getAgentByUrl, agent: getAgentByUrl,
signal: controller.signal, signal: controller.signal,
}); });
@ -120,9 +120,9 @@ export const httpsAgent = config.proxy
*/ */
export function getAgentByUrl(url: URL, bypassProxy = false) { export function getAgentByUrl(url: URL, bypassProxy = false) {
if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) { if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
return url.protocol == 'http:' ? _http : _https; return url.protocol === 'http:' ? _http : _https;
} else { } else {
return url.protocol == 'http:' ? httpAgent : httpsAgent; return url.protocol === 'http:' ? httpAgent : httpsAgent;
} }
} }

View File

@ -23,7 +23,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
} }
// ファイルが添付されているとき // ファイルが添付されているとき
if ((note.files || []).length != 0) { if ((note.files || []).length !== 0) {
summary += ` (📎${note.files!.length})`; summary += ` (📎${note.files!.length})`;
} }

View File

@ -63,7 +63,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
const isLocal = emoji.host == null; const isLocal = emoji.host == null;
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
const url = isLocal ? emojiUrl : `${config.url}/proxy/image.png?${query({ url: emojiUrl })}`; const url = isLocal ? emojiUrl : `${config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`;
return { return {
name: emojiName, name: emojiName,

View File

@ -89,7 +89,7 @@ export interface Schema extends OfSchema {
readonly optional?: boolean; readonly optional?: boolean;
readonly items?: Schema; readonly items?: Schema;
readonly properties?: Obj; readonly properties?: Obj;
readonly required?: ReadonlyArray<keyof NonNullable<this['properties']>>; readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
readonly description?: string; readonly description?: string;
readonly example?: any; readonly example?: any;
readonly format?: string; readonly format?: string;
@ -98,6 +98,9 @@ export interface Schema extends OfSchema {
readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null; readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null;
readonly maxLength?: number; readonly maxLength?: number;
readonly minLength?: number; readonly minLength?: number;
readonly maximum?: number;
readonly minimum?: number;
readonly pattern?: string;
} }
type RequiredPropertyNames<s extends Obj> = { type RequiredPropertyNames<s extends Obj> = {
@ -105,24 +108,26 @@ type RequiredPropertyNames<s extends Obj> = {
// K is not optional // K is not optional
s[K]['optional'] extends false ? K : s[K]['optional'] extends false ? K :
// K has default value // K has default value
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K : never s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
never
}[keyof s]; }[keyof s];
export interface Obj { [key: string]: Schema; } export type Obj = Record<string, Schema>;
// https://github.com/misskey-dev/misskey/issues/8535
// To avoid excessive stack depth error,
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
export type ObjType<s extends Obj, RequiredProps extends keyof s> = export type ObjType<s extends Obj, RequiredProps extends keyof s> =
{ -readonly [P in keyof s]?: SchemaType<s[P]> } & UnionToIntersection<
{ -readonly [P in RequiredProps]: SchemaType<s[P]> } & { -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } &
{ -readonly [P in RequiredPropertyNames<s>]: SchemaType<s[P]> }; { -readonly [R in RequiredProps]-?: SchemaType<s[R]> } &
{ -readonly [P in keyof s]?: SchemaType<s[P]> }
>;
type NullOrUndefined<p extends Schema, T> = type NullOrUndefined<p extends Schema, T> =
p['nullable'] extends true | (p['nullable'] extends true ? null : never)
? p['optional'] extends true | (p['optional'] extends true ? undefined : never)
? (T | null | undefined) | T;
: (T | null)
: p['optional'] extends true
? (T | undefined)
: T;
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union

View File

@ -0,0 +1,49 @@
import { Webhooks } from '@/models/index.js';
import { Webhook } from '@/models/entities/webhook.js';
import { subsdcriber } from '../db/redis.js';
let webhooksFetched = false;
let webhooks: Webhook[] = [];
export async function getActiveWebhooks() {
if (!webhooksFetched) {
webhooks = await Webhooks.findBy({
active: true,
});
webhooksFetched = true;
}
return webhooks;
}
subsdcriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message;
switch (type) {
case 'webhookCreated':
if (body.active) {
webhooks.push(body);
}
break;
case 'webhookUpdated':
if (body.active) {
const i = webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
webhooks[i] = body;
} else {
webhooks.push(body);
}
} else {
webhooks = webhooks.filter(a => a.id !== body.id);
}
break;
case 'webhookDeleted':
webhooks = webhooks.filter(a => a.id !== body.id);
break;
default:
break;
}
}
});

View File

@ -15,7 +15,6 @@ export class AccessToken {
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
nullable: true, nullable: true,
default: null,
}) })
public lastUsedAt: Date | null; public lastUsedAt: Date | null;
@ -29,7 +28,6 @@ export class AccessToken {
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
nullable: true, nullable: true,
default: null,
}) })
public session: string | null; public session: string | null;
@ -52,7 +50,6 @@ export class AccessToken {
@Column({ @Column({
...id(), ...id(),
nullable: true, nullable: true,
default: null,
}) })
public appId: App['id'] | null; public appId: App['id'] | null;
@ -65,21 +62,18 @@ export class AccessToken {
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
nullable: true, nullable: true,
default: null,
}) })
public name: string | null; public name: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,
default: null,
}) })
public description: string | null; public description: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,
default: null,
}) })
public iconUrl: string | null; public iconUrl: string | null;

View File

@ -23,7 +23,7 @@ export class AuthSession {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public userId: User['id']; public userId: User['id'] | null;
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',

View File

@ -37,7 +37,7 @@ export class Clip {
public isPublic: boolean; public isPublic: boolean;
@Column('varchar', { @Column('varchar', {
length: 2048, nullable: true, default: null, length: 2048, nullable: true,
comment: 'The description of the Clip.', comment: 'The description of the Clip.',
}) })
public description: string | null; public description: string | null;

View File

@ -79,7 +79,6 @@ export class DriveFile {
}) })
public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index()
@Column('boolean') @Column('boolean')
public storedInternal: boolean; public storedInternal: boolean;

View File

@ -36,6 +36,7 @@ export class Emoji {
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
default: '',
}) })
public publicUrl: string; public publicUrl: string;

View File

@ -107,53 +107,53 @@ export class Instance {
public isSuspended: boolean; public isSuspended: boolean;
@Column('varchar', { @Column('varchar', {
length: 64, nullable: true, default: null, length: 64, nullable: true,
comment: 'The software of the Instance.', comment: 'The software of the Instance.',
}) })
public softwareName: string | null; public softwareName: string | null;
@Column('varchar', { @Column('varchar', {
length: 64, nullable: true, default: null, length: 64, nullable: true,
}) })
public softwareVersion: string | null; public softwareVersion: string | null;
@Column('boolean', { @Column('boolean', {
nullable: true, default: null, nullable: true,
}) })
public openRegistrations: boolean | null; public openRegistrations: boolean | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, default: null, length: 256, nullable: true,
}) })
public name: string | null; public name: string | null;
@Column('varchar', { @Column('varchar', {
length: 4096, nullable: true, default: null, length: 4096, nullable: true,
}) })
public description: string | null; public description: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, default: null, length: 128, nullable: true,
}) })
public maintainerName: string | null; public maintainerName: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, default: null, length: 256, nullable: true,
}) })
public maintainerEmail: string | null; public maintainerEmail: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, default: null, length: 256, nullable: true,
}) })
public iconUrl: string | null; public iconUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, default: null, length: 256, nullable: true,
}) })
public faviconUrl: string | null; public faviconUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 64, nullable: true, default: null, length: 64, nullable: true,
}) })
public themeColor: string | null; public themeColor: string | null;

View File

@ -78,7 +78,7 @@ export class Meta {
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}', length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}',
}) })
public pinnedPages: string[]; public pinnedPages: string[];
@ -346,14 +346,12 @@ export class Meta {
@Column('varchar', { @Column('varchar', {
length: 8192, length: 8192,
default: null,
nullable: true, nullable: true,
}) })
public defaultLightTheme: string | null; public defaultLightTheme: string | null;
@Column('varchar', { @Column('varchar', {
length: 8192, length: 8192,
default: null,
nullable: true, nullable: true,
}) })
public defaultDarkTheme: string | null; public defaultDarkTheme: string | null;

View File

@ -17,7 +17,6 @@ export class Muting {
@Index() @Index()
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
nullable: true, nullable: true,
default: null,
}) })
public expiresAt: Date | null; public expiresAt: Date | null;

View File

@ -53,8 +53,8 @@ export class Note {
}) })
public threadId: string | null; public threadId: string | null;
@Column('varchar', { @Column('text', {
length: 8192, nullable: true, nullable: true,
}) })
public text: string | null; public text: string | null;
@ -179,7 +179,7 @@ export class Note {
@Index() @Index()
@Column({ @Column({
...id(), ...id(),
nullable: true, default: null, nullable: true,
comment: 'The ID of source channel.', comment: 'The ID of source channel.',
}) })
public channelId: Channel['id'] | null; public channelId: Channel['id'] | null;

View File

@ -192,6 +192,7 @@ export class UserProfile {
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],
comment: 'List of instances muted by the user.',
}) })
public mutedInstances: string[]; public mutedInstances: string[];

View File

@ -1,6 +1,6 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { DriveFile } from './drive-file.js';
import { id } from '../id.js'; import { id } from '../id.js';
import { DriveFile } from './drive-file.js';
@Entity() @Entity()
@Index(['usernameLower', 'host'], { unique: true }) @Index(['usernameLower', 'host'], { unique: true })
@ -207,7 +207,7 @@ export class User {
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether to show users replying to other users in the timeline' comment: 'Whether to show users replying to other users in the timeline.',
}) })
public showTimelineReplies: boolean; public showTimelineReplies: boolean;

View File

@ -0,0 +1,73 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user.js';
import { id } from '../id.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
@Entity()
export class Webhook {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Antenna.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the Antenna.',
})
public name: string;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}',
})
public on: (typeof webhookEventTypes)[number][];
@Column('varchar', {
length: 1024,
})
public url: string;
@Column('varchar', {
length: 1024,
})
public secret: string;
@Index()
@Column('boolean', {
default: true,
})
public active: boolean;
/**
*
*/
@Column('timestamp with time zone', {
nullable: true,
})
public latestSentAt: Date | null;
/**
* HTTPステータスコード
*/
@Column('integer', {
nullable: true,
})
public latestStatus: number | null;
}

View File

@ -64,6 +64,7 @@ import { Ad } from './entities/ad.js';
import { PasswordResetRequest } from './entities/password-reset-request.js'; import { PasswordResetRequest } from './entities/password-reset-request.js';
import { UserPending } from './entities/user-pending.js'; import { UserPending } from './entities/user-pending.js';
import { InstanceRepository } from './repositories/instance.js'; import { InstanceRepository } from './repositories/instance.js';
import { Webhook } from './entities/webhook.js';
export const Announcements = db.getRepository(Announcement); export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead); export const AnnouncementReads = db.getRepository(AnnouncementRead);
@ -125,5 +126,6 @@ export const Channels = (ChannelRepository);
export const ChannelFollowings = db.getRepository(ChannelFollowing); export const ChannelFollowings = db.getRepository(ChannelFollowing);
export const ChannelNotePinings = db.getRepository(ChannelNotePining); export const ChannelNotePinings = db.getRepository(ChannelNotePining);
export const RegistryItems = db.getRepository(RegistryItem); export const RegistryItems = db.getRepository(RegistryItem);
export const Webhooks = db.getRepository(Webhook);
export const Ads = db.getRepository(Ad); export const Ads = db.getRepository(Ad);
export const PasswordResetRequests = db.getRepository(PasswordResetRequest); export const PasswordResetRequests = db.getRepository(PasswordResetRequest);

View File

@ -1,6 +1,5 @@
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { Users, DriveFolders } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { toPuny } from '@/misc/convert-host.js'; import { toPuny } from '@/misc/convert-host.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js';
@ -9,6 +8,7 @@ import config from '@/config/index.js';
import { query, appendQuery } from '@/prelude/url.js'; import { query, appendQuery } from '@/prelude/url.js';
import { Meta } from '@/models/entities/meta.js'; import { Meta } from '@/models/entities/meta.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, DriveFolders } from '../index.js';
type PackOptions = { type PackOptions = {
detail?: boolean, detail?: boolean,
@ -29,7 +29,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] { getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) { if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties)); const properties = structuredClone(file.properties);
if (file.properties.orientation >= 5) { if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width]; [properties.width, properties.height] = [properties.height, properties.width];
} }
@ -111,7 +111,40 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
async pack( async pack(
src: DriveFile['id'] | DriveFile, src: DriveFile['id'] | DriveFile,
options?: PackOptions options?: PackOptions,
): Promise<Packed<'DriveFile'>> {
const opts = Object.assign({
detail: false,
self: false,
}, options);
const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll<Packed<'DriveFile'>>({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
});
},
async packNullable(
src: DriveFile['id'] | DriveFile,
options?: PackOptions,
): Promise<Packed<'DriveFile'> | null> { ): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({ const opts = Object.assign({
detail: false, detail: false,
@ -145,9 +178,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
async packMany( async packMany(
files: (DriveFile['id'] | DriveFile)[], files: (DriveFile['id'] | DriveFile)[],
options?: PackOptions options?: PackOptions,
) { ): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.pack(f, options))); const items = await Promise.all(files.map(f => this.packNullable(f, options)));
return items.filter(x => x != null); return items.filter((x): x is Packed<'DriveFile'> => x != null);
}, },
}); });

View File

@ -1,10 +1,10 @@
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { Page } from '@/models/entities/page.js'; import { Page } from '@/models/entities/page.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { Users, DriveFiles, PageLikes } from '../index.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Users, DriveFiles, PageLikes } from '../index.js';
export const PageRepository = db.getRepository(Page).extend({ export const PageRepository = db.getRepository(Page).extend({
async pack( async pack(
@ -14,7 +14,7 @@ export const PageRepository = db.getRepository(Page).extend({
const meId = me ? me.id : null; const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const attachedFiles: Promise<DriveFile | undefined>[] = []; const attachedFiles: Promise<DriveFile | null>[] = [];
const collectFile = (xs: any[]) => { const collectFile = (xs: any[]) => {
for (const x of xs) { for (const x of xs) {
if (x.type === 'image') { if (x.type === 'image') {
@ -73,7 +73,7 @@ export const PageRepository = db.getRepository(Page).extend({
script: page.script, script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
}); });

View File

@ -1,7 +1,6 @@
import { EntityRepository, Repository, In, Not } from 'typeorm'; import { EntityRepository, Repository, In, Not } from 'typeorm';
import Ajv from 'ajv'; import Ajv from 'ajv';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js';
@ -9,8 +8,9 @@ import { populateEmojis } from '@/misc/populate-emojis.js';
import { getAntennas } from '@/misc/antenna-cache.js'; import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { Instance } from '../entities/instance.js';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { Instance } from '../entities/instance.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
@ -112,7 +112,7 @@ export const UserRepository = db.getRepository(User).extend({
const joinings = await UserGroupJoinings.findBy({ userId: userId }); const joinings = await UserGroupJoinings.findBy({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId }) .where('message.groupId = :groupId', { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId }) .andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
@ -204,8 +204,18 @@ export const UserRepository = db.getRepository(User).extend({
); );
}, },
getAvatarUrl(user: User): string { async getAvatarUrl(user: User): Promise<string> {
// TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング if (user.avatar) {
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
} else if (user.avatarId) {
const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId });
return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
},
getAvatarUrlSync(user: User): string {
if (user.avatar) { if (user.avatar) {
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
} else { } else {
@ -223,7 +233,7 @@ export const UserRepository = db.getRepository(User).extend({
options?: { options?: {
detail?: D, detail?: D,
includeSecrets?: boolean, includeSecrets?: boolean,
} },
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> { ): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign({ const opts = Object.assign({
detail: false, detail: false,
@ -274,7 +284,7 @@ export const UserRepository = db.getRepository(User).extend({
name: user.name, name: user.name,
username: user.username, username: user.username,
host: user.host, host: user.host,
avatarUrl: this.getAvatarUrl(user), avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash || null, avatarBlurhash: user.avatar?.blurhash || null,
avatarColor: null, // 後方互換性のため avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin || falsy, isAdmin: user.isAdmin || falsy,
@ -283,7 +293,7 @@ export const UserRepository = db.getRepository(User).extend({
isCat: user.isCat || falsy, isCat: user.isCat || falsy,
instance: user.host ? userInstanceCache.fetch(user.host, instance: user.host ? userInstanceCache.fetch(user.host,
() => Instances.findOneBy({ host: user.host! }), () => Instances.findOneBy({ host: user.host! }),
v => v != null v => v != null,
).then(instance => instance ? { ).then(instance => instance ? {
name: instance.name, name: instance.name,
softwareName: instance.softwareName, softwareName: instance.softwareName,
@ -403,7 +413,7 @@ export const UserRepository = db.getRepository(User).extend({
options?: { options?: {
detail?: D, detail?: D,
includeSecrets?: boolean, includeSecrets?: boolean,
} },
): Promise<IsUserDetailed<D>[]> { ): Promise<IsUserDetailed<D>[]> {
return Promise.all(users.map(u => this.pack(u, me, options))); return Promise.all(users.map(u => this.pack(u, me, options)));
}, },

View File

@ -27,6 +27,7 @@ export const packedEmojiSchema = {
host: { host: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
description: 'The local host is represented with `null`.',
}, },
url: { url: {
type: 'string', type: 'string',

View File

@ -21,6 +21,7 @@ export const packedUserLiteSchema = {
type: 'string', type: 'string',
nullable: true, optional: false, nullable: true, optional: false,
example: 'misskey.example.com', example: 'misskey.example.com',
description: 'The local host is represented with `null`.',
}, },
avatarUrl: { avatarUrl: {
type: 'string', type: 'string',

View File

@ -1,4 +1,5 @@
import httpSignature from 'http-signature'; import httpSignature from 'http-signature';
import { v4 as uuid } from 'uuid';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
@ -8,13 +9,15 @@ import processInbox from './processors/inbox.js';
import processDb from './processors/db/index.js'; import processDb from './processors/db/index.js';
import processObjectStorage from './processors/object-storage/index.js'; import processObjectStorage from './processors/object-storage/index.js';
import processSystemQueue from './processors/system/index.js'; import processSystemQueue from './processors/system/index.js';
import processWebhookDeliver from './processors/webhook-deliver.js';
import { endedPollNotification } from './processors/ended-poll-notification.js'; import { endedPollNotification } from './processors/ended-poll-notification.js';
import { queueLogger } from './logger.js'; import { queueLogger } from './logger.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { getJobInfo } from './get-job-info.js'; import { getJobInfo } from './get-job-info.js';
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js'; import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
import { ThinUser } from './types.js'; import { ThinUser } from './types.js';
import { IActivity } from '@/remote/activitypub/type.js'; import { IActivity } from '@/remote/activitypub/type.js';
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
function renderError(e: Error): any { function renderError(e: Error): any {
return { return {
@ -26,6 +29,7 @@ function renderError(e: Error): any {
const systemLogger = queueLogger.createSubLogger('system'); const systemLogger = queueLogger.createSubLogger('system');
const deliverLogger = queueLogger.createSubLogger('deliver'); const deliverLogger = queueLogger.createSubLogger('deliver');
const webhookLogger = queueLogger.createSubLogger('webhook');
const inboxLogger = queueLogger.createSubLogger('inbox'); const inboxLogger = queueLogger.createSubLogger('inbox');
const dbLogger = queueLogger.createSubLogger('db'); const dbLogger = queueLogger.createSubLogger('db');
const objectStorageLogger = queueLogger.createSubLogger('objectStorage'); const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
@ -70,6 +74,14 @@ objectStorageQueue
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
webhookDeliverQueue
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
export function deliver(user: ThinUser, content: unknown, to: string | null) { export function deliver(user: ThinUser, content: unknown, to: string | null) {
if (content == null) return null; if (content == null) return null;
if (to == null) return null; if (to == null) return null;
@ -251,12 +263,36 @@ export function createCleanRemoteFilesJob() {
}); });
} }
export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) {
const data = {
type,
content,
webhookId: webhook.id,
userId: webhook.userId,
to: webhook.url,
secret: webhook.secret,
createdAt: Date.now(),
eventId: uuid(),
};
return webhookDeliverQueue.add(data, {
attempts: 4,
timeout: 1 * 60 * 1000, // 1min
backoff: {
type: 'apBackoff',
},
removeOnComplete: true,
removeOnFail: true,
});
}
export default function() { export default function() {
if (envOption.onlyServer) return; if (envOption.onlyServer) return;
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
endedPollNotificationQueue.process(endedPollNotification); endedPollNotificationQueue.process(endedPollNotification);
webhookDeliverQueue.process(64, processWebhookDeliver);
processDb(dbQueue); processDb(dbQueue);
processObjectStorage(objectStorageQueue); processObjectStorage(objectStorageQueue);

View File

@ -0,0 +1,59 @@
import { URL } from 'node:url';
import Bull from 'bull';
import Logger from '@/services/logger.js';
import { WebhookDeliverJobData } from '../types.js';
import { getResponse, StatusError } from '@/misc/fetch.js';
import { Webhooks } from '@/models/index.js';
import config from '@/config/index.js';
const logger = new Logger('webhook');
export default async (job: Bull.Job<WebhookDeliverJobData>) => {
try {
logger.debug(`delivering ${job.data.webhookId}`);
const res = await getResponse({
url: job.data.to,
method: 'POST',
headers: {
'User-Agent': 'Misskey-Hooks',
'X-Misskey-Host': config.host,
'X-Misskey-Hook-Id': job.data.webhookId,
'X-Misskey-Hook-Secret': job.data.secret,
},
body: JSON.stringify({
hookId: job.data.webhookId,
userId: job.data.userId,
eventId: job.data.eventId,
createdAt: job.data.createdAt,
type: job.data.type,
body: job.data.content,
}),
});
Webhooks.update({ id: job.data.webhookId }, {
latestSentAt: new Date(),
latestStatus: res.status,
});
return 'Success';
} catch (res) {
Webhooks.update({ id: job.data.webhookId }, {
latestSentAt: new Date(),
latestStatus: res instanceof StatusError ? res.statusCode : 1,
});
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
return `${res.statusCode} ${res.statusMessage}`;
}
// 5xx etc.
throw `${res.statusCode} ${res.statusMessage}`;
} else {
// DNS error, socket error, timeout ...
throw res;
}
}
};

View File

@ -1,6 +1,6 @@
import config from '@/config/index.js'; import config from '@/config/index.js';
import { initialize as initializeQueue } from './initialize.js'; import { initialize as initializeQueue } from './initialize.js';
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js'; import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js';
export const systemQueue = initializeQueue<Record<string, unknown>>('system'); export const systemQueue = initializeQueue<Record<string, unknown>>('system');
export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification'); export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification');
@ -8,6 +8,7 @@ export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.de
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16); export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
export const dbQueue = initializeQueue<DbJobData>('db'); export const dbQueue = initializeQueue<DbJobData>('db');
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage'); export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>('webhookDeliver', 64);
export const queues = [ export const queues = [
systemQueue, systemQueue,
@ -16,4 +17,5 @@ export const queues = [
inboxQueue, inboxQueue,
dbQueue, dbQueue,
objectStorageQueue, objectStorageQueue,
webhookDeliverQueue,
]; ];

View File

@ -1,6 +1,7 @@
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { Note } from '@/models/entities/note'; import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Webhook } from '@/models/entities/webhook';
import { IActivity } from '@/remote/activitypub/type.js'; import { IActivity } from '@/remote/activitypub/type.js';
import httpSignature from 'http-signature'; import httpSignature from 'http-signature';
@ -46,6 +47,17 @@ export type EndedPollNotificationJobData = {
noteId: Note['id']; noteId: Note['id'];
}; };
export type WebhookDeliverJobData = {
type: string;
content: unknown;
webhookId: Webhook['id'];
userId: User['id'];
to: string;
secret: string;
createdAt: number;
eventId: string;
};
export type ThinUser = { export type ThinUser = {
id: User['id']; id: User['id'];
}; };

View File

@ -95,7 +95,7 @@ function genSigningString(request: Request, includeHeaders: string[]) {
function lcObjectKey(src: Record<string, string>) { function lcObjectKey(src: Record<string, string>) {
const dst: Record<string, string> = {}; const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst; return dst;
} }

View File

@ -79,9 +79,13 @@ export default class DeliverManager {
const inboxes = new Set<string>(); const inboxes = new Set<string>();
// build inbox list /*
for (const recipe of this.recipes) { build inbox list
if (isFollowers(recipe)) {
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver // followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
@ -103,13 +107,18 @@ export default class DeliverManager {
const inbox = following.followerSharedInbox || following.followerInbox; const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox); inboxes.add(inbox);
} }
} else if (isDirect(recipe)) {
// direct deliver
const inbox = recipe.to.inbox;
if (inbox) inboxes.add(inbox);
}
} }
this.recipes.filter((recipe): recipe is IDirectRecipe =>
// followers recipes have already been processed
isDirect(recipe)
// check that shared inbox has not been added yet
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
// check that they actually have an inbox
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.add(recipe.to.inbox!));
// deliver // deliver
for (const inbox of inboxes) { for (const inbox of inboxes) {
deliver(this.actor, this.activity, inbox); deliver(this.actor, this.activity, inbox);

View File

@ -18,7 +18,7 @@ export const performReadActivity = async (actor: CacheableRemoteUser, activity:
return `skip: message not found`; return `skip: message not found`;
} }
if (actor.id != message.recipientId) { if (actor.id !== message.recipientId) {
return `skip: actor is not a message recipient`; return `skip: actor is not a message recipient`;
} }

View File

@ -1,6 +1,6 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js'; import cancelRequest from '@/services/following/requests/cancel.js';
import {IAccept} from '../../type.js'; import { IAccept } from '../../type.js';
import { CacheableRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Followings } from '@/models/index.js'; import { Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';

View File

@ -113,7 +113,8 @@ export class LdSignature {
headers: { headers: {
Accept: 'application/ld+json, application/json', Accept: 'application/ld+json, application/json',
}, },
timeout: this.loderTimeout, // TODO
//timeout: this.loderTimeout,
agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent, agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent,
}).then(res => { }).then(res => {
if (!res.ok) { if (!res.ok) {

View File

@ -1,9 +1,9 @@
import { toArray, unique } from '@/prelude/array.js';
import { IObject, isMention, IApMention } from '../type.js';
import { resolvePerson } from './person.js';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import Resolver from '../resolver.js'; import { toArray, unique } from '@/prelude/array.js';
import { CacheableUser, User } from '@/models/entities/user.js'; import { CacheableUser, User } from '@/models/entities/user.js';
import { IObject, isMention, IApMention } from '../type.js';
import Resolver from '../resolver.js';
import { resolvePerson } from './person.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef
const limit = promiseLimit<CacheableUser | null>(2); const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is CacheableUser => x != null); )).filter((x): x is CacheableUser => x != null);
return mentionedUsers; return mentionedUsers;

View File

@ -1,17 +1,8 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import $, { Context } from 'cafy';
import config from '@/config/index.js'; import config from '@/config/index.js';
import Resolver from '../resolver.js';
import { resolveImage } from './image.js';
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
import { fromHtml } from '../../../mfm/from-html.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { resolveNote, extractEmojis } from './note.js';
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
import { extractApHashtags } from './tag.js';
import { apLogger } from '../logger.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js'; import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js';
import { uriPersonCache } from '@/services/user-cache.js'; import { uriPersonCache } from '@/services/user-cache.js';
import { publishInternalEvent } from '@/services/stream.js'; import { publishInternalEvent } from '@/services/stream.js';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { apLogger } from '../logger.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { fromHtml } from '../../../mfm/from-html.js';
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
import Resolver from '../resolver.js';
import { extractApHashtags } from './tag.js';
import { resolveNote, extractEmojis } from './note.js';
import { resolveImage } from './image.js';
const logger = apLogger; const logger = apLogger;
@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor {
throw new Error(`invalid Actor type '${x.type}'`); throw new Error(`invalid Actor type '${x.type}'`);
} }
const validate = (name: string, value: any, validater: Context) => { if (!(typeof x.id === 'string' && x.id.length > 0)) {
const e = validater.test(value); throw new Error('invalid Actor: wrong id');
if (e) throw new Error(`invalid Actor: ${name} ${e.message}`); }
};
validate('id', x.id, $.default.str.min(1)); if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
validate('inbox', x.inbox, $.default.str.min(1)); throw new Error('invalid Actor: wrong inbox');
validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/)); }
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
throw new Error('invalid Actor: wrong username');
}
// These fields are only informational, and some AP software allows these // These fields are only informational, and some AP software allows these
// fields to be very long. If they are too long, we cut them off. This way // fields to be very long. If they are too long, we cut them off. This way
// we can at least see these users and their activities. // we can at least see these users and their activities.
validate('name', truncate(x.name, nameLength), $.default.optional.nullable.str); if (x.name) {
validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str); if (!(typeof x.name === 'string' && x.name.length > 0)) {
throw new Error('invalid Actor: wrong name');
}
x.name = truncate(x.name, nameLength);
}
if (x.summary) {
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
throw new Error('invalid Actor: wrong summary');
}
x.summary = truncate(x.summary, summaryLength);
}
const idHost = toPuny(new URL(x.id!).hostname); const idHost = toPuny(new URL(x.id!).hostname);
if (idHost !== expectHost) { if (idHost !== expectHost) {
@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (Personの場合Remote resolveをせずに更新に利用します) * @param hint Hint of Person object (Personの場合Remote resolveをせずに更新に利用します)
*/ */
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: Record<string, unknown>): Promise<void> { export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
if (resolver == null) resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const object = hint || await resolver.resolve(uri) as any; const object = hint || await resolver.resolve(uri);
const person = validateActor(object, uri); const person = validateActor(object, uri);
@ -403,7 +415,7 @@ const services: {
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
'misskey:authentication:github': (id, login) => ({ id, login }), 'misskey:authentication:github': (id, login) => ({ id, login }),
'misskey:authentication:discord': (id, name) => $discord(id, name), 'misskey:authentication:discord': (id, name) => $discord(id, name),
}; };
const $discord = (id: string, name: string) => { const $discord = (id: string, name: string) => {
if (typeof name !== 'string') { if (typeof name !== 'string') {
@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) {
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`); if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
// Resolve to Object(may be Note) arrays // Resolve to Object(may be Note) arrays
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;

View File

@ -69,7 +69,7 @@ export async function updateQuestion(value: any) {
const oldCount = poll.votes[poll.choices.indexOf(choice)]; const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (oldCount != newCount) { if (oldCount !== newCount) {
changed = true; changed = true;
poll.votes[poll.choices.indexOf(choice)] = newCount; poll.votes[poll.choices.indexOf(choice)] = newCount;
} }

View File

@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js';
// to anonymise reporters, the reporting actor must be a system user // to anonymise reporters, the reporting actor must be a system user
// object has to be a uri or array of uris // object has to be a uri or array of uris
export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => { export const renderFlag = (user: ILocalUser, object: [string], content: string) => {
return { return {
type: 'Flag', type: 'Flag',
actor: `${config.url}/users/${user.id}`, actor: `${config.url}/users/${user.id}`,

View File

@ -1,15 +1,15 @@
import renderDocument from './document.js'; import { In, IsNull } from 'typeorm';
import renderHashtag from './hashtag.js';
import renderMention from './mention.js';
import renderEmoji from './emoji.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import toHtml from '../misc/get-note-html.js';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js';
import { In, IsNull } from 'typeorm';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { Poll } from '@/models/entities/poll.js'; import { Poll } from '@/models/entities/poll.js';
import toHtml from '../misc/get-note-html.js';
import renderEmoji from './emoji.js';
import renderMention from './mention.js';
import renderHashtag from './hashtag.js';
import renderDocument from './document.js';
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> { export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> {
const getPromisedFiles = async (ids: string[]) => { const getPromisedFiles = async (ids: string[]) => {
@ -83,7 +83,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
const text = note.text; const text = note.text;
let poll: Poll | null; let poll: Poll | null = null;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id }); poll = await Polls.findOneBy({ noteId: note.id });
@ -159,7 +159,7 @@ export async function getEmojis(names: string[]): Promise<Emoji[]> {
names.map(name => Emojis.findOneBy({ names.map(name => Emojis.findOneBy({
name, name,
host: IsNull(), host: IsNull(),
})) })),
); );
return emojis.filter(emoji => emoji != null) as Emoji[]; return emojis.filter(emoji => emoji != null) as Emoji[];

View File

@ -2,10 +2,10 @@ import config from '@/config/index.js';
import { getJson } from '@/misc/fetch.js'; import { getJson } from '@/misc/fetch.js';
import { ILocalUser } from '@/models/entities/user.js'; import { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js'; import { getInstanceActor } from '@/services/instance-actor.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { extractDbHost } from '@/misc/convert-host.js'; import { extractDbHost } from '@/misc/convert-host.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
@ -56,13 +56,13 @@ export default class Resolver {
this.user = await getInstanceActor(); this.user = await getInstanceActor();
} }
const object = this.user const object = (this.user
? await signedGet(value, this.user) ? await signedGet(value, this.user)
: await getJson(value, 'application/activity+json, application/ld+json'); : await getJson(value, 'application/activity+json, application/ld+json')) as IObject;
if (object == null || ( if (object == null || (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?
!object['@context'].includes('https://www.w3.org/ns/activitystreams') : !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams' object['@context'] !== 'https://www.w3.org/ns/activitystreams'
)) { )) {
throw new Error('invalid response'); throw new Error('invalid response');

View File

@ -2,7 +2,7 @@ export type obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[]; export type ApObject = IObject | string | (IObject | string)[];
export interface IObject { export interface IObject {
'@context': string | obj | obj[]; '@context': string | string[] | obj | obj[];
type: string | string[]; type: string | string[];
id?: string; id?: string;
summary?: string; summary?: string;
@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string {
export function getApId(value: string | IObject): string { export function getApId(value: string | IObject): string {
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id; if (typeof value.id === 'string') return value.id;
throw new Error(`cannot detemine id`); throw new Error('cannot detemine id');
} }
/** /**
@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string {
export function getApType(value: IObject): string { export function getApType(value: IObject): string {
if (typeof value.type === 'string') return value.type; if (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error(`cannot detect type`); throw new Error('cannot detect type');
} }
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {

View File

@ -15,7 +15,7 @@ type IWebFinger = {
export default async function(query: string): Promise<IWebFinger> { export default async function(query: string): Promise<IWebFinger> {
const url = genUrl(query); const url = genUrl(query);
return await getJson(url, 'application/jrd+json, application/json'); return await getJson(url, 'application/jrd+json, application/json') as IWebFinger;
} }
function genUrl(query: string) { function genUrl(query: string) {

View File

@ -1,32 +1,26 @@
import Router from '@koa/router'; import Router from '@koa/router';
import { FindOptionsWhere, IsNull, LessThan } from 'typeorm';
import config from '@/config/index.js'; import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import * as url from '@/prelude/url.js'; import * as url from '@/prelude/url.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js'; import { Users, Followings, UserProfiles } from '@/models/index.js';
import { IsNull, LessThan } from 'typeorm'; import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user; const userId = ctx.params.user;
// Get 'cursor' parameter const cursor = ctx.request.query.cursor;
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); if (cursor != null && typeof cursor !== 'string') {
// Get 'page' parameter
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
if (cursorErr || pageErr) {
ctx.status = 400; ctx.status = 400;
return; return;
} }
const page = ctx.request.query.page === 'true';
const user = await Users.findOneBy({ const user = await Users.findOneBy({
id: userId, id: userId,
host: IsNull(), host: IsNull(),
@ -57,7 +51,7 @@ export default async (ctx: Router.RouterContext) => {
if (page) { if (page) {
const query = { const query = {
followeeId: user.id, followeeId: user.id,
} as any; } as FindOptionsWhere<Following>;
// カーソルが指定されている場合 // カーソルが指定されている場合
if (cursor) { if (cursor) {
@ -86,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
inStock ? `${partOf}?${url.query({ inStock ? `${partOf}?${url.query({
page: 'true', page: 'true',
cursor: followings[followings.length - 1].id, cursor: followings[followings.length - 1].id,
})}` : undefined })}` : undefined,
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);

View File

@ -1,33 +1,26 @@
import Router from '@koa/router'; import Router from '@koa/router';
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
import config from '@/config/index.js'; import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import * as url from '@/prelude/url.js'; import * as url from '@/prelude/url.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js'; import { Users, Followings, UserProfiles } from '@/models/index.js';
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
import { Following } from '@/models/entities/following.js'; import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user; const userId = ctx.params.user;
// Get 'cursor' parameter const cursor = ctx.request.query.cursor;
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); if (cursor != null && typeof cursor !== 'string') {
// Get 'page' parameter
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
if (cursorErr || pageErr) {
ctx.status = 400; ctx.status = 400;
return; return;
} }
const page = ctx.request.query.page === 'true';
const user = await Users.findOneBy({ const user = await Users.findOneBy({
id: userId, id: userId,
host: IsNull(), host: IsNull(),
@ -87,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
inStock ? `${partOf}?${url.query({ inStock ? `${partOf}?${url.query({
page: 'true', page: 'true',
cursor: followings[followings.length - 1].id, cursor: followings[followings.length - 1].id,
})}` : undefined })}` : undefined,
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);

View File

@ -1,36 +1,37 @@
import Router from '@koa/router'; import Router from '@koa/router';
import { Brackets, IsNull } from 'typeorm';
import config from '@/config/index.js'; import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import { setResponseType } from '../activitypub.js';
import renderNote from '@/remote/activitypub/renderer/note.js'; import renderNote from '@/remote/activitypub/renderer/note.js';
import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderCreate from '@/remote/activitypub/renderer/create.js';
import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
import { countIf } from '@/prelude/array.js'; import { countIf } from '@/prelude/array.js';
import * as url from '@/prelude/url.js'; import * as url from '@/prelude/url.js';
import { Users, Notes } from '@/models/index.js'; import { Users, Notes } from '@/models/index.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { Brackets, IsNull } from 'typeorm';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => { export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user; const userId = ctx.params.user;
// Get 'sinceId' parameter const sinceId = ctx.request.query.since_id;
const [sinceId, sinceIdErr] = $.default.optional.type(ID).get(ctx.request.query.since_id); if (sinceId != null && typeof sinceId !== 'string') {
ctx.status = 400;
return;
}
// Get 'untilId' parameter const untilId = ctx.request.query.until_id;
const [untilId, untilIdErr] = $.default.optional.type(ID).get(ctx.request.query.until_id); if (untilId != null && typeof untilId !== 'string') {
ctx.status = 400;
return;
}
// Get 'page' parameter const page = ctx.request.query.page === 'true';
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters if (countIf(x => x != null, [sinceId, untilId]) > 1) {
if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
ctx.status = 400; ctx.status = 400;
return; return;
} }
@ -52,8 +53,8 @@ export default async (ctx: Router.RouterContext) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId: user.id }) .andWhere('note.userId = :userId', { userId: user.id })
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => { qb
.where(`note.visibility = 'public'`) .where('note.visibility = \'public\'')
.orWhere(`note.visibility = 'home'`); .orWhere('note.visibility = \'home\'');
})) }))
.andWhere('note.localOnly = FALSE'); .andWhere('note.localOnly = FALSE');
@ -76,7 +77,7 @@ export default async (ctx: Router.RouterContext) => {
notes.length ? `${partOf}?${url.query({ notes.length ? `${partOf}?${url.query({
page: 'true', page: 'true',
until_id: notes[notes.length - 1].id, until_id: notes[notes.length - 1].id,
})}` : undefined })}` : undefined,
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
@ -85,7 +86,7 @@ export default async (ctx: Router.RouterContext) => {
// index page // index page
const rendered = renderOrderedCollection(partOf, user.notesCount, const rendered = renderOrderedCollection(partOf, user.notesCount,
`${partOf}?page=true`, `${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000` `${partOf}?page=true&since_id=000000000000000000000000`,
); );
ctx.body = renderActivity(rendered); ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=180');

View File

@ -1,6 +1,6 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import config from '@/config/index.js';
import * as jsrsasign from 'jsrsasign'; import * as jsrsasign from 'jsrsasign';
import config from '@/config/index.js';
const ECC_PRELUDE = Buffer.from([0x04]); const ECC_PRELUDE = Buffer.from([0x04]);
const NULL_BYTE = Buffer.from([0]); const NULL_BYTE = Buffer.from([0]);
@ -121,14 +121,14 @@ export function verifyLogin({
signature: Buffer, signature: Buffer,
challenge: string challenge: string
}) { }) {
if (clientData.type != 'webauthn.get') { if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get'); throw new Error('type is not webauthn.get');
} }
if (hash(clientData.challenge).toString('hex') != challenge) { if (hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch'); throw new Error('challenge mismatch');
} }
if (clientData.origin != config.scheme + '://' + config.host) { if (clientData.origin !== config.scheme + '://' + config.host) {
throw new Error('origin mismatch'); throw new Error('origin mismatch');
} }
@ -145,14 +145,14 @@ export function verifyLogin({
export const procedures = { export const procedures = {
none: { none: {
verify({ publicKey }: {publicKey: Map<number, Buffer>}) { verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
const negThree = publicKey.get(-3); const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
@ -183,7 +183,7 @@ export const procedures = {
rpIdHash: Buffer, rpIdHash: Buffer,
credentialId: Buffer, credentialId: Buffer,
}) { }) {
if (attStmt.alg != -7) { if (attStmt.alg !== -7) {
throw new Error('alg mismatch'); throw new Error('alg mismatch');
} }
@ -196,11 +196,11 @@ export const procedures = {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
const negThree = publicKey.get(-3); const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
@ -263,7 +263,7 @@ export const procedures = {
.map((key: any) => PEMString(key)) .map((key: any) => PEMString(key))
.concat([GSR2]); .concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') { if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name'); throw new Error('invalid common name');
} }
@ -283,11 +283,11 @@ export const procedures = {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
const negThree = publicKey.get(-3); const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
@ -332,11 +332,11 @@ export const procedures = {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
const negThree = publicKey.get(-3); const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
@ -353,7 +353,7 @@ export const procedures = {
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
throw new Error('ECDAA-Verify is not supported'); throw new Error('ECDAA-Verify is not supported');
} else { } else {
if (attStmt.alg != -7) throw new Error('alg mismatch'); if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported'); throw new Error('self attestation is not supported');
} }
@ -377,7 +377,7 @@ export const procedures = {
credentialId: Buffer credentialId: Buffer
}) { }) {
const x5c: Buffer[] = attStmt.x5c; const x5c: Buffer[] = attStmt.x5c;
if (x5c.length != 1) { if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation'); throw new Error('x5c length does not match expectation');
} }
@ -387,11 +387,11 @@ export const procedures = {
const negTwo: Buffer = publicKey.get(-2); const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
const negThree: Buffer = publicKey.get(-3); const negThree: Buffer = publicKey.get(-3);
if (!negThree || negThree.length != 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }

View File

@ -1,6 +1,7 @@
import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js';
import { publishMessagingStream } from '@/services/stream.js'; import { publishMessagingStream } from '@/services/stream.js';
import { publishMessagingIndexStream } from '@/services/stream.js'; import { publishMessagingIndexStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import { User, IRemoteUser } from '@/models/entities/user.js'; import { User, IRemoteUser } from '@/models/entities/user.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js';
@ -50,6 +51,21 @@ export async function readUserMessagingMessage(
if (!await Users.getHasUnreadMessagingMessage(userId)) { if (!await Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages'); publishMainStream(userId, 'readAllMessagingMessages');
pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const count = await MessagingMessages.count({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
take: 1
});
if (!count) {
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId });
}
} }
} }
@ -104,6 +120,19 @@ export async function readGroupMessagingMessage(
if (!await Users.getHasUnreadMessagingMessage(userId)) { if (!await Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages'); publishMainStream(userId, 'readAllMessagingMessages');
pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのグループにおいて未読がなければイベント発行
const unreadExist = await MessagingMessages.createQueryBuilder('message')
.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 }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null);
if (!unreadExist) {
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId });
}
} }
} }

View File

@ -1,4 +1,5 @@
import { publishMainStream } from '@/services/stream.js'; import { publishMainStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Notification } from '@/models/entities/notification.js'; import { Notification } from '@/models/entities/notification.js';
import { Notifications, Users } from '@/models/index.js'; import { Notifications, Users } from '@/models/index.js';
@ -16,28 +17,29 @@ export async function readNotification(
isRead: true, isRead: true,
}); });
post(userId); if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId);
else return postReadNotifications(userId, notificationIds);
} }
export async function readNotificationByQuery( export async function readNotificationByQuery(
userId: User['id'], userId: User['id'],
query: Record<string, any> query: Record<string, any>
) { ) {
// Update documents const notificationIds = await Notifications.find({
await Notifications.update({
...query, ...query,
notifieeId: userId, notifieeId: userId,
isRead: false, isRead: false,
}, { }).then(notifications => notifications.map(notification => notification.id));
isRead: true,
});
post(userId); return readNotification(userId, notificationIds);
} }
async function post(userId: User['id']) { function postReadAllNotifications(userId: User['id']) {
if (!await Users.getHasUnreadNotification(userId)) {
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllNotifications'); publishMainStream(userId, 'readAllNotifications');
} return pushNotification(userId, 'readAllNotifications', undefined);
}
function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
publishMainStream(userId, 'readNotifications', notificationIds);
return pushNotification(userId, 'readNotifications', { notificationIds });
} }

View File

@ -9,7 +9,7 @@ import { publishMainStream } from '@/services/stream.js';
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
if (redirect) { if (redirect) {
//#region Cookie //#region Cookie
ctx.cookies.set('igi', user.token, { ctx.cookies.set('igi', user.token!, {
path: '/', path: '/',
// SEE: https://github.com/koajs/koa/issues/974 // SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header

View File

@ -1,5 +1,6 @@
import { Schema } from '@/misc/schema.js'; import { Schema } from '@/misc/schema.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.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_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
@ -201,6 +202,11 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js'; import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js'; import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___messaging_history from './endpoints/messaging/history.js'; import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js'; import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
@ -304,6 +310,7 @@ import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_stats from './endpoints/users/stats.js';
const eps = [ const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports], ['admin/abuse-user-reports', ep___admin_abuseUserReports],
['admin/accounts/create', ep___admin_accounts_create], ['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete], ['admin/accounts/delete', ep___admin_accounts_delete],
@ -505,6 +512,11 @@ const eps = [
['i/update-email', ep___i_updateEmail], ['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update], ['i/update', ep___i_update],
['i/user-group-invites', ep___i_userGroupInvites], ['i/user-group-invites', ep___i_userGroupInvites],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
['messaging/history', ep___messaging_history], ['messaging/history', ep___messaging_history],
['messaging/messages', ep___messaging_messages], ['messaging/messages', ep___messaging_messages],
['messaging/messages/create', ep___messaging_messages_create], ['messaging/messages/create', ep___messaging_messages_create],

View File

@ -1,5 +1,6 @@
import define from '../../../define.js';
import { Announcements, AnnouncementReads } from '@/models/index.js'; import { Announcements, AnnouncementReads } from '@/models/index.js';
import { Announcement } from '@/models/entities/announcement.js';
import define from '../../../define.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js'; import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = { export const meta = {
@ -68,11 +69,21 @@ export default define(meta, paramDef, async (ps) => {
const announcements = await query.take(ps.limit).getMany(); const announcements = await query.take(ps.limit).getMany();
const reads = new Map<Announcement, number>();
for (const announcement of announcements) { for (const announcement of announcements) {
(announcement as any).reads = await AnnouncementReads.countBy({ reads.set(announcement, await AnnouncementReads.countBy({
announcementId: announcement.id, announcementId: announcement.id,
}); }));
} }
return announcements; return announcements.map(announcement => ({
id: announcement.id,
createdAt: announcement.createdAt.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
reads: reads.get(announcement)!,
}));
}); });

View File

@ -27,7 +27,12 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
hostname: { type: 'string', nullable: true, default: null }, hostname: {
type: 'string',
nullable: true,
default: null,
description: 'The local host is represented with `null`.',
},
}, },
required: [], required: [],
} as const; } as const;

View File

@ -40,6 +40,7 @@ export const meta = {
userHost: { userHost: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
description: 'The local host is represented with `null`.',
}, },
md5: { md5: {
type: 'string', type: 'string',
@ -151,11 +152,20 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
anyOf: [
{
properties: { properties: {
fileId: { type: 'string', format: 'misskey:id' }, fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
properties: {
url: { type: 'string' }, url: { type: 'string' },
}, },
required: [], required: ['url'],
},
],
} as const; } as const;
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export

View File

@ -40,6 +40,7 @@ export const meta = {
host: { host: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
description: 'The local host is represented with `null`.',
}, },
url: { url: {
type: 'string', type: 'string',
@ -54,7 +55,12 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
query: { type: 'string', nullable: true, default: null }, query: { type: 'string', nullable: true, default: null },
host: { type: 'string', nullable: true, default: null }, host: {
type: 'string',
nullable: true,
default: null,
description: 'Use `null` to represent the local host.',
},
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },

View File

@ -38,8 +38,9 @@ export const meta = {
optional: false, nullable: true, optional: false, nullable: true,
}, },
host: { host: {
type: 'string', type: 'null',
optional: false, nullable: true, optional: false,
description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
}, },
url: { url: {
type: 'string', type: 'string',

View File

@ -17,7 +17,11 @@ export const paramDef = {
ids: { type: 'array', items: { ids: { type: 'array', items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
category: { type: 'string', nullable: true }, category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
}, },
required: ['ids'], required: ['ids'],
} as const; } as const;

View File

@ -23,7 +23,11 @@ export const paramDef = {
properties: { properties: {
id: { type: 'string', format: 'misskey:id' }, id: { type: 'string', format: 'misskey:id' },
name: { type: 'string' }, name: { type: 'string' },
category: { type: 'string', nullable: true }, category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { type: 'array', items: { aliases: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },

View File

@ -1,5 +1,6 @@
import define from '../../../define.js'; import define from '../../../define.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],

View File

@ -1,5 +1,5 @@
import define from '../../define.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
import define from '../../define.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -24,10 +24,15 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 }, offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
state: { type: 'string', enum: ['all', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: "all" }, state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
username: { type: 'string', default: null }, username: { type: 'string', nullable: true, default: null },
hostname: { type: 'string', default: null }, hostname: {
type: 'string',
nullable: true,
default: null,
description: 'The local host is represented with `null`.',
},
}, },
required: [], required: [],
} as const; } as const;

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