Merge pull request 'develop' (#9013) from develop into main
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9013
This commit is contained in:
commit
bc7f3043bc
71
CALCKEY.md
71
CALCKEY.md
@ -1,6 +1,6 @@
|
||||
# All the changes to Calckey from stock Misskey
|
||||
|
||||
### Planned
|
||||
## Planned
|
||||
|
||||
- MFM button
|
||||
- Better Messaging UI
|
||||
@ -8,36 +8,65 @@
|
||||
- Like/star button
|
||||
- Option to publicize instance blocks
|
||||
- Better intro/onboarding
|
||||
- Fully revamp welcome.a (non-logged in screen)
|
||||
- Tabler icons instead of FontAwesome
|
||||
- Fully revamp non-logged-in screen
|
||||
- Personal notes for all accounts
|
||||
- Admin custom CSS
|
||||
- Improve notifications (content is too verbose)
|
||||
- Non-nyaify cat mode
|
||||
- Timeline filters
|
||||
- Mark as read from notifications widget
|
||||
- "Bubble" timeline
|
||||
- Filter notifications by user
|
||||
- Remove NSFW/AI stuff
|
||||
- [Rat mode?](https://stop.voring.me/notes/933fx97bmd)
|
||||
- Improve accesibility score
|
||||
<details><summary>Current Misskey score is 57/100</summary>
|
||||
|
||||
### Implemented
|
||||
![](https://pool.jortage.com/voringme/misskey/8ff18aae-4dc6-4b08-9e05-a4c9d051a9e3.png)
|
||||
|
||||
</details>
|
||||
|
||||
## Work in progress
|
||||
|
||||
- Less cluttered notification summary
|
||||
- Better timeline top bar
|
||||
- Admin custom CSS
|
||||
|
||||
## Implemented
|
||||
|
||||
- Yarn 3
|
||||
- Saner defaults
|
||||
- Star as default reaction
|
||||
- Rosé Pine by default
|
||||
- Rosé Pine by default (+ non-themable elements made Rosé Pine)
|
||||
- Better sidebar/navbar
|
||||
- [Profile background as banner](https://codeberg.org/Freeplay/Misskey-Tweaks/src/branch/main/snippets/profile-background.styl)
|
||||
- Mark as read from notifications widget
|
||||
- Better welcome screen (not logged in)
|
||||
- Ability to turn off "Connection lost" message
|
||||
- Annoying Orange search
|
||||
- Raw instance info only for moderators
|
||||
- Spinner instead of "Loading..."
|
||||
- SearchX instead of Google
|
||||
- Spacing on group items
|
||||
- MOTD
|
||||
- Reply limit bug fixed
|
||||
- Reply limit bug fixed (somewhat)
|
||||
- Custom assets
|
||||
- https://github.com/misskey-dev/misskey/pull/8983
|
||||
- https://github.com/misskey-dev/misskey/pull/8956
|
||||
- https://github.com/misskey-dev/misskey/pull/8954
|
||||
- https://github.com/misskey-dev/misskey/pull/8997
|
||||
- https://github.com/misskey-dev/misskey/pull/8996
|
||||
- https://github.com/misskey-dev/misskey/pull/8955
|
||||
- https://github.com/JakeMBauer/Misskey-Extras/blob/master/patches/star-is-like.patch
|
||||
- https://github.com/misskey-dev/misskey/pull/8671
|
||||
- https://github.com/misskey-dev/misskey/pull/8927
|
||||
- https://github.com/misskey-dev/misskey/pull/8927
|
||||
- https://github.com/misskey-dev/misskey/pull/8549
|
||||
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)
|
||||
- [Styled Repair Tools](https://github.com/misskey-dev/misskey/pull/8956)
|
||||
- [Option to make enter send message](https://github.com/misskey-dev/misskey/pull/8954)
|
||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||
- [Autocomplete in messaging](https://github.com/misskey-dev/misskey/pull/8955)
|
||||
- [Star is like](https://github.com/JakeMBauer/Misskey-Extras/blob/master/patches/star-is-like.patch)
|
||||
- [Add additional background for acrylic popups if backdrop-filter is unsupported](https://github.com/misskey-dev/misskey/pull/8671)
|
||||
- [Timeline page for non-login users](https://github.com/misskey-dev/misskey/pull/8927)
|
||||
- [Add parameters to MFM rotate](https://github.com/misskey-dev/misskey/pull/8549)
|
||||
- Many changes from [Foundkey](https://akkoma.dev/FoundKeyGang/Foundkey)
|
||||
- 0ece67b04c3f0365057624c1068808276ccab981: refactor pages/auth.form.vue to composition API
|
||||
- 0ece67b04c3f0365057624c1068808276ccab981: refactor pages/auth.form.vue to composition API
|
||||
- 4bc9610d8bf5af736b5e89e4782395705de45d7d: remove unnecessary joins
|
||||
- 9ee609d70082f7a6dc119a5d83c0e7c5e1208676: enhance privacy of notes
|
||||
- 0fec6e10477b1c1b95d9469fbaf4e249a3722f12: remove ms dependency
|
||||
- 46fff77accbe8bf0fd3cc88170d67b997bf2bdc3: client uses new API for child notes depth
|
||||
- c35372a20d22cddb75e93a0b407f2b652cd7faf0: pack children without detail
|
||||
- aca724e0bfff3e58b4d273f3ee744e3f3aa9c39b: enable to fetch replies recursively
|
||||
- 2fe64c11502fd8d89c126558cd715e095c83754e: Refactor components/page/page.textarea.vue to composition API
|
||||
- 6d3181f9835955e5b79bde5484c74bd70e7f9535: Refactor components/page/page.text.vue to composition API
|
||||
- b630cd7eacd695bb705e6748c87f38425ec4ed45: refactor: add NoteReactions.packMany
|
||||
- 3fe351df6d4e21f7748c46adfa6ca165abd030c0: fix: catch errors from packing with detail
|
||||
- 63591da33e233b2ed0ab331ae6bb3c9eff5020ae: refactor: colours in queue chart
|
||||
|
@ -12,9 +12,13 @@ You should also include the user name that made the change.
|
||||
## 12.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- Client: Add vi-VN language support
|
||||
|
||||
### Bugfixes
|
||||
- Server: リモートユーザーを正しくブロックできるように修正する @xianonn
|
||||
- Client: 一度作ったwebhookの設定画面を開こうとするとページがフリーズする @syuilo
|
||||
- Client: MiAuth認証ページが機能していない @syuilo
|
||||
- Client: 一部のアプリからファイルを投稿フォームへドロップできない場合がある問題を修正 @m-hayabusa
|
||||
|
||||
## 12.117.1 (2022/07/19)
|
||||
|
||||
|
@ -140,6 +140,34 @@ Misskey uses Vue(v3) as its front-end framework.
|
||||
- **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
|
||||
- Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
|
||||
|
||||
## nirax
|
||||
niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。
|
||||
**vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。**
|
||||
|
||||
### ルート定義
|
||||
ルート定義は、以下の形式のオブジェクトの配列です。
|
||||
|
||||
``` ts
|
||||
{
|
||||
name?: string;
|
||||
path: string;
|
||||
component: Component;
|
||||
query?: Record<string, string>;
|
||||
loginRequired?: boolean;
|
||||
hash?: string;
|
||||
globalCacheKey?: string;
|
||||
children?: RouteDef[];
|
||||
}
|
||||
```
|
||||
|
||||
> **Warning**
|
||||
> 現状、ルートは定義された順に評価されます。
|
||||
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
|
||||
|
||||
### 複数のルーター
|
||||
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
|
||||
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
|
||||
|
||||
## Notes
|
||||
### How to resolve conflictions occurred at yarn.lock?
|
||||
|
||||
|
@ -40,7 +40,7 @@ Read [this](./CALCKEY.md) for current and future differences.
|
||||
You need at least 🐢 NodeJS v16.10.0 (>v18.0.0 \<v18.6.0 reccomended!) and *exactly* 🧶 Yarn v3.2.1!
|
||||
|
||||
```sh
|
||||
# nvm install 18 && nvm alias default 18
|
||||
# nvm install 18.4.0 && nvm alias default 18.4.0 && nvm use 18.4.0
|
||||
corepack enable
|
||||
yarn set version berry
|
||||
```
|
||||
@ -48,9 +48,9 @@ yarn set version berry
|
||||
```sh
|
||||
git clone https://codeberg.org/thatonecalculator/calckey.git
|
||||
cd calckey/
|
||||
# `git checkout main` if you want only stable versions
|
||||
# git checkout main # if you want only stable versions
|
||||
cp ../misskey/.config/default.yml ./.config/default.yml # or wherever misskey folder is
|
||||
cp -r ../misskey/files . # if you don't use object storage
|
||||
# cp -r ../misskey/files . # if you don't use object storage
|
||||
YARN_CHECKSUM_BEHAVIOR=update yarn install
|
||||
NODE_ENV=production npm run build && npm run migrate
|
||||
# Edit service to point to calckey folder and restart!
|
||||
|
@ -803,7 +803,7 @@ translate: "Translate"
|
||||
translatedFrom: "Translated from {x}"
|
||||
accountDeletionInProgress: "Account deletion is currently in progress"
|
||||
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
||||
aiChanMode: "Ai Mode"
|
||||
aiChanMode: "Ai-chan in Classic UI"
|
||||
keepCw: "Keep content warnings"
|
||||
pubSub: "Pub/Sub Accounts"
|
||||
lastCommunication: "Last communication"
|
||||
@ -900,6 +900,7 @@ account: "Account"
|
||||
move: "Move"
|
||||
showAds: "Show ads"
|
||||
enterSendsMessage: "Press Return in Messaging to send message (off is Ctrl + Return)"
|
||||
adminCustomCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause EVERYONE'S clients to stop functioning normally. Please ensure your CSS works properly by testing it in your user settings."
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
|
@ -36,6 +36,7 @@ const languages = [
|
||||
'sk-SK',
|
||||
'ug-CN',
|
||||
'uk-UA',
|
||||
'vi-VN',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
];
|
||||
|
@ -900,6 +900,7 @@ navbar: "ナビゲーションバー"
|
||||
shuffle: "シャッフル"
|
||||
account: "アカウント"
|
||||
move: "移動"
|
||||
adminCustomCssWarn: "この設定は、それが何をするものであるかを知っている場合のみ使用してください。不適切な値を入力すると、クライアントが正常に動作しなくなる可能性があります。ユーザー設定でCSSをテストし、正しく動作することを確認してください。"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "12.117.1.2-calc",
|
||||
"version": "12.118.0-calc.b4",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -54,12 +54,9 @@
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "latest",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.7",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "latest",
|
||||
"cypress": "10.3.1",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.7.4",
|
||||
"vue-eslint-parser": "^9.0.2"
|
||||
|
@ -0,0 +1,52 @@
|
||||
export class noteRepliesFunction1658656633972 {
|
||||
name = 'noteRepliesFunction1658656633972'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`
|
||||
CREATE OR REPLACE FUNCTION note_replies(start_id varchar, max_depth integer, max_breadth integer) RETURNS TABLE (id VARCHAR) AS
|
||||
$$
|
||||
SELECT DISTINCT id FROM (
|
||||
WITH RECURSIVE tree (id, ancestors, depth) AS (
|
||||
SELECT start_id, '{}'::VARCHAR[], 0
|
||||
UNION
|
||||
SELECT
|
||||
note.id,
|
||||
CASE
|
||||
WHEN note."replyId" = tree.id THEN tree.ancestors || note."replyId"
|
||||
ELSE tree.ancestors || note."renoteId"
|
||||
END,
|
||||
depth + 1
|
||||
FROM note, tree
|
||||
WHERE (
|
||||
note."replyId" = tree.id
|
||||
OR
|
||||
(
|
||||
-- get renotes but not pure renotes
|
||||
note."renoteId" = tree.id
|
||||
AND
|
||||
(
|
||||
note.text IS NOT NULL
|
||||
OR
|
||||
CARDINALITY(note."fileIds") != 0
|
||||
OR
|
||||
note."hasPoll" = TRUE
|
||||
)
|
||||
)
|
||||
) AND depth < max_depth
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
-- apply the limit per node
|
||||
row_number() OVER (PARTITION BY ancestors[array_upper(ancestors, 1)]) AS nth_child
|
||||
FROM tree
|
||||
WHERE depth > 0
|
||||
) AS recursive WHERE nth_child < max_breadth
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP FUNCTION note_replies`);
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
"test": "yarn mocha"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-node": "3.18.0"
|
||||
"@tensorflow/tfjs-node": "3.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "4.0.0",
|
||||
@ -28,7 +28,6 @@
|
||||
"@peertube/http-signature": "1.6.0",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"abort-controller": "3.0.0",
|
||||
"ajv": "8.11.0",
|
||||
"archiver": "5.3.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
@ -45,13 +44,13 @@
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.28.0",
|
||||
"date-fns": "2.29.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "17.1.2",
|
||||
"file-type": "17.1.3",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"got": "12.1.0",
|
||||
"got": "12.2.0",
|
||||
"hpagent": "0.1.2",
|
||||
"ioredis": "4.28.5",
|
||||
"ip-cidr": "3.0.10",
|
||||
@ -61,7 +60,7 @@
|
||||
"json5": "2.2.1",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "6.0.0",
|
||||
"jsrsasign": "10.5.25",
|
||||
"jsrsasign": "10.5.26",
|
||||
"koa": "2.13.4",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-favicon": "2.1.0",
|
||||
@ -71,14 +70,13 @@
|
||||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"mfm-js": "0.23.0-canary.1",
|
||||
"mfm-js": "0.23.0",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "10.0.0",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"multer": "1.4.4",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.2.8",
|
||||
"node-fetch": "3.2.9",
|
||||
"nodemailer": "6.7.7",
|
||||
"nsfwjs": "2.4.1",
|
||||
"oauth": "^0.9.15",
|
||||
@ -91,32 +89,30 @@
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.1.1",
|
||||
"pureimage": "0.3.14",
|
||||
"qrcode": "1.5.0",
|
||||
"qrcode": "1.5.1",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.17.7",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"require-all": "3.0.0",
|
||||
"rndstr": "1.0.0",
|
||||
"rss-parser": "3.12.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.7.0",
|
||||
"sanitize-html": "2.7.1",
|
||||
"semver": "7.3.7",
|
||||
"sharp": "0.30.6",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "3.3.1",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.12.0",
|
||||
"systeminformation": "5.12.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.3.1",
|
||||
"ts-node": "10.8.2",
|
||||
"tsc-alias": "1.6.11",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "1.7.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.7",
|
||||
@ -125,21 +121,20 @@
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.5.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.8.0",
|
||||
"ws": "8.8.1",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.100",
|
||||
"@redocly/openapi-core": "1.0.0-beta.104",
|
||||
"@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/is-url": "1.2.30",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "16.2.14",
|
||||
"@types/jsonld": "1.5.6",
|
||||
"@types/jsrsasign": "10.5.1",
|
||||
"@types/jsrsasign": "10.5.2",
|
||||
"@types/koa": "2.13.5",
|
||||
"@types/koa-bodyparser": "4.3.7",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
@ -152,7 +147,7 @@
|
||||
"@types/koa__multer": "2.0.4",
|
||||
"@types/koa__router": "8.0.11",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/node": "18.0.3",
|
||||
"@types/node": "18.6.1",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.4",
|
||||
"@types/oauth": "0.9.1",
|
||||
@ -174,10 +169,10 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.7",
|
||||
"@typescript-eslint/parser": "5.30.7",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.19.0",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"execa": "6.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
@ -1,5 +1,12 @@
|
||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||
|
||||
export const SECOND = 1000;
|
||||
export const SEC = 1000;
|
||||
export const MINUTE = 60 * SEC;
|
||||
export const MIN = 60 * SEC;
|
||||
export const HOUR = 60 * MIN;
|
||||
export const DAY = 24 * HOUR;
|
||||
|
||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
||||
|
||||
|
@ -9,10 +9,6 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
||||
return `(❌⛔)`;
|
||||
}
|
||||
|
||||
if (note.isHidden) {
|
||||
return `(⛔)`;
|
||||
}
|
||||
|
||||
let summary = '';
|
||||
|
||||
// 本文
|
||||
@ -32,6 +28,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
||||
summary += ` (📊)`;
|
||||
}
|
||||
|
||||
/*
|
||||
// 返信のとき
|
||||
if (note.replyId) {
|
||||
if (note.reply) {
|
||||
@ -49,6 +46,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
||||
summary += '\n\nRN: ...';
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return summary.trim();
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({
|
||||
id: favorite.id,
|
||||
createdAt: favorite.createdAt.toISOString(),
|
||||
noteId: favorite.noteId,
|
||||
// may throw error
|
||||
note: await Notes.pack(favorite.note || favorite.noteId, me),
|
||||
};
|
||||
},
|
||||
@ -22,6 +23,7 @@ export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({
|
||||
favorites: any[],
|
||||
me: { id: User['id'] }
|
||||
) {
|
||||
return Promise.all(favorites.map(x => this.pack(x, me)));
|
||||
return Promise.allSettled(favorites.map(x => this.pack(x, me)))
|
||||
.then(promises => promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : []));
|
||||
},
|
||||
});
|
||||
|
@ -25,8 +25,22 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
|
||||
user: await Users.pack(reaction.user ?? reaction.userId, me),
|
||||
type: convertLegacyReaction(reaction.reaction),
|
||||
...(opts.withNote ? {
|
||||
// may throw error
|
||||
note: await Notes.pack(reaction.note ?? reaction.noteId, me),
|
||||
} : {}),
|
||||
};
|
||||
},
|
||||
|
||||
async packMany(
|
||||
src: NoteReaction[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
withNote: booleam;
|
||||
},
|
||||
): Promise<Packed<'NoteReaction'>[]> {
|
||||
const reactions = await Promise.allSettled(src.map(reaction => this.pack(reaction, me, options)));
|
||||
|
||||
// filter out rejected promises, only keep fulfilled values
|
||||
return reactions.flatMap(result => result.status === 'fulfilled' ? [result.value] : []);
|
||||
}
|
||||
});
|
||||
|
@ -10,66 +10,7 @@ import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
async function hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
|
||||
if (specified) {
|
||||
hide = false;
|
||||
} else {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await Followings.findOneBy({
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
});
|
||||
|
||||
if (following == null) {
|
||||
hide = true;
|
||||
} else {
|
||||
hide = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
packedNote.visibleUserIds = undefined;
|
||||
packedNote.fileIds = [];
|
||||
packedNote.files = [];
|
||||
packedNote.text = null;
|
||||
packedNote.poll = undefined;
|
||||
packedNote.cw = null;
|
||||
packedNote.isHidden = true;
|
||||
}
|
||||
}
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
async function populatePoll(note: Note, meId: User['id'] | null) {
|
||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
@ -193,7 +134,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
detail?: boolean;
|
||||
skipHide?: boolean;
|
||||
_hint_?: {
|
||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||
};
|
||||
@ -201,13 +141,16 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
): Promise<Packed<'Note'>> {
|
||||
const opts = Object.assign({
|
||||
detail: true,
|
||||
skipHide: false,
|
||||
}, options);
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const note = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
const host = note.userHost;
|
||||
|
||||
if (!await this.isVisibleForMe(note, meId)) {
|
||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||
}
|
||||
|
||||
let text = note.text;
|
||||
|
||||
if (note.name && (note.url ?? note.uri)) {
|
||||
@ -282,10 +225,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
packed.text = mfm.toString(tokens);
|
||||
}
|
||||
|
||||
if (!opts.skipHide) {
|
||||
await hideNote(packed, meId);
|
||||
}
|
||||
|
||||
return packed;
|
||||
},
|
||||
|
||||
@ -294,7 +233,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
detail?: boolean;
|
||||
skipHide?: boolean;
|
||||
}
|
||||
) {
|
||||
if (notes.length === 0) return [];
|
||||
@ -316,11 +254,14 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||
|
||||
await prefetchEmojis(aggregateNoteEmojis(notes));
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
const promises = await Promise.allSettled(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
myReactions: myReactionsMap,
|
||||
},
|
||||
})));
|
||||
|
||||
// filter out rejected promises, only keep fulfilled values
|
||||
return promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : []);
|
||||
},
|
||||
});
|
||||
|
@ -52,10 +52,6 @@ export const packedNoteSchema = {
|
||||
optional: true, nullable: true,
|
||||
ref: 'Note',
|
||||
},
|
||||
isHidden: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -7,7 +7,7 @@ import { Blocking } from '@/models/entities/blocking.js';
|
||||
* @param block The block to be rendered. The blockee relation must be loaded.
|
||||
*/
|
||||
export function renderBlock(block: Blocking) {
|
||||
if (block.blockee?.url == null) {
|
||||
if (block.blockee?.uri == null) {
|
||||
throw new Error('renderBlock: missing blockee uri');
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,8 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
|
||||
};
|
||||
|
||||
// Authentication
|
||||
authenticate(body['i']).then(([user, app]) => {
|
||||
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
||||
authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => {
|
||||
// API invoking
|
||||
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
||||
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
||||
@ -80,11 +81,15 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
|
||||
}
|
||||
}).catch(e => {
|
||||
if (e instanceof AuthenticationError) {
|
||||
reply(403, new ApiError({
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
ctx.response.status = 403;
|
||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||
ctx.response.body = {
|
||||
message: 'Authentication failed: ' + e.message,
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}));
|
||||
kind: 'client',
|
||||
};
|
||||
res();
|
||||
} else {
|
||||
reply(500, new ApiError());
|
||||
}
|
||||
|
@ -15,8 +15,25 @@ export class AuthenticationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export default async (token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
if (token == null) {
|
||||
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
let token: string | null = null;
|
||||
|
||||
// check if there is an authorization header set
|
||||
if (authorization != null) {
|
||||
if (bodyToken != null) {
|
||||
throw new AuthenticationError('using multiple authorization schemes');
|
||||
}
|
||||
|
||||
// check if OAuth 2.0 Bearer tokens are being used
|
||||
// Authorization schemes are case insensitive
|
||||
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
|
||||
token = authorization.substring(7);
|
||||
} else {
|
||||
throw new AuthenticationError('unsupported authentication scheme');
|
||||
}
|
||||
} else if (bodyToken != null) {
|
||||
token = bodyToken;
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
@ -25,7 +42,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
|
||||
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationError('user not found');
|
||||
throw new AuthenticationError('unknown token');
|
||||
}
|
||||
|
||||
return [user, null];
|
||||
@ -39,7 +56,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
|
||||
});
|
||||
|
||||
if (accessToken == null) {
|
||||
throw new AuthenticationError('invalid signature');
|
||||
throw new AuthenticationError('unknown token');
|
||||
}
|
||||
|
||||
AccessTokens.update(accessToken.id, {
|
||||
|
@ -2,12 +2,20 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Notes, Users } from '@/models/index.js';
|
||||
import { generateVisibilityQuery } from './generate-visibility-query.js';
|
||||
|
||||
/**
|
||||
* Get note for API processing
|
||||
* Get note for API processing, taking into account visibility.
|
||||
*/
|
||||
export async function getNote(noteId: Note['id']) {
|
||||
const note = await Notes.findOneBy({ id: noteId });
|
||||
export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null) {
|
||||
const query = Notes.createQueryBuilder('note')
|
||||
.where("note.id = :id", {
|
||||
id: noteId,
|
||||
});
|
||||
|
||||
generateVisibilityQuery(query, me);
|
||||
|
||||
const note = await query.getOne();
|
||||
|
||||
if (note == null) {
|
||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||
|
@ -35,9 +35,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const exist = await PromoNotes.findOneBy({ noteId: note.id });
|
||||
|
@ -1,7 +1,6 @@
|
||||
import define from '../../define.js';
|
||||
import Resolver from '@/remote/activitypub/resolver.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import ms from 'ms';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@ -9,7 +8,7 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 30,
|
||||
},
|
||||
|
||||
|
@ -11,8 +11,8 @@ import { Note } from '@/models/entities/note.js';
|
||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
|
||||
import ms from 'ms';
|
||||
import { SchemaType } from '@/misc/schema.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@ -20,7 +20,7 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 30,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import ms from 'ms';
|
||||
import create from '@/services/blocking/create.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../common/getters.js';
|
||||
import { Blockings, NoteWatchings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 100,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import ms from 'ms';
|
||||
import deleteBlocking from '@/services/blocking/delete.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../common/getters.js';
|
||||
import { Blockings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 100,
|
||||
},
|
||||
|
||||
|
@ -52,9 +52,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const exist = await ClipNotes.findOneBy({
|
||||
|
@ -1,4 +1,3 @@
|
||||
import ms from 'ms';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||
@ -7,6 +6,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import define from '../../../define.js';
|
||||
import { apiLogger } from '../../../logger.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
@ -14,7 +16,7 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 120,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
import ms from 'ms';
|
||||
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
|
||||
import define from '../../../define.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||
import define from '../../../define.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 60,
|
||||
},
|
||||
|
||||
@ -34,8 +33,8 @@ export const paramDef = {
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
|
||||
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
|
||||
DriveFiles.pack(file, { self: true }).then(packedFile => {
|
||||
publishMainStream(user.id, 'urlUploadFinished', {
|
||||
marker: ps.marker,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import ms from 'ms';
|
||||
import { createExportCustomEmojisJob } from '@/queue/index.js';
|
||||
import define from '../define.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,16 +1,16 @@
|
||||
import ms from 'ms';
|
||||
import create from '@/services/following/create.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../common/getters.js';
|
||||
import { Followings, Users } from '@/models/index.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 100,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import ms from 'ms';
|
||||
import deleteFollowing from '@/services/following/delete.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../common/getters.js';
|
||||
import { Followings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 100,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import ms from 'ms';
|
||||
import deleteFollowing from '@/services/following/delete.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getUser } from '../../common/getters.js';
|
||||
import { Followings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['following', 'users'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 100,
|
||||
},
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import ms from 'ms';
|
||||
import define from '../../../define.js';
|
||||
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
||||
import { genId } from '../../../../../misc/gen-id.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
@ -14,7 +14,7 @@ export const meta = {
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from 'ms';
|
||||
import define from '../../../define.js';
|
||||
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
@ -13,7 +13,7 @@ export const meta = {
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { createExportBlockingJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { createExportFollowingJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { createExportMuteJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { createExportNotesJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { DAY } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
duration: DAY,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { createExportUserListsJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { MINUTE } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1min'),
|
||||
duration: MINUTE,
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
@ -1,15 +1,15 @@
|
||||
import define from '../../define.js';
|
||||
import { createImportBlockingJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import define from '../../define.js';
|
||||
import { createImportFollowingJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duratition: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
import define from '../../define.js';
|
||||
import { createImportMutingJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import define from '../../define.js';
|
||||
import { createImportUserListsJob } from '@/queue/index.js';
|
||||
import ms from 'ms';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const meta = {
|
||||
|
||||
limit: {
|
||||
duration: 60000,
|
||||
max: 10,
|
||||
max: 15,
|
||||
},
|
||||
|
||||
kind: 'read:notifications',
|
||||
|
@ -2,12 +2,12 @@ import { publishMainStream } from '@/services/stream.js';
|
||||
import define from '../../define.js';
|
||||
import rndstr from 'rndstr';
|
||||
import config from '@/config/index.js';
|
||||
import ms from 'ms';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { sendEmail } from '@/services/send-email.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { validateEmailForAccount } from '@/services/validate-email-for-account.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
@ -15,7 +15,7 @@ export const meta = {
|
||||
secure: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 3,
|
||||
},
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import define from '../../../define.js';
|
||||
import ms from 'ms';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { MessagingMessages } from '@/models/index.js';
|
||||
import { deleteMessage } from '@/services/messages/delete.js';
|
||||
import { SECOND, HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['messaging'],
|
||||
@ -12,9 +12,9 @@ export const meta = {
|
||||
kind: 'write:messaging',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
minInterval: ms('1sec'),
|
||||
minInterval: SECOND,
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
@ -12,6 +12,8 @@ export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Get a list of children of a notes. Children includes replies as well as quote renotes that quote the respective post. A post will not be duplicated if it is a reply and a quote of a note in this thread. For depths larger than 1 the threading has to be computed by the client.',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
@ -27,7 +29,20 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
limit: {
|
||||
description: 'The maximum number of replies/quotes to show per parent note, i.e. the maximum number of children each note may have.',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10,
|
||||
},
|
||||
depth: {
|
||||
description: 'The number of layers of replies to fetch at once. Defaults to 1 for backward compatibility.',
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 1,
|
||||
},
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
@ -37,28 +52,10 @@ export const paramDef = {
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('note.replyId = :noteId', { noteId: ps.noteId })
|
||||
.orWhere(new Brackets(qb => { qb
|
||||
.where('note.renoteId = :noteId', { noteId: ps.noteId })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('note.text IS NOT NULL')
|
||||
.orWhere('note.fileIds != \'{}\'')
|
||||
.orWhere('note.hasPoll = TRUE');
|
||||
}));
|
||||
}));
|
||||
}))
|
||||
.andWhere('note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))', { noteId: ps.noteId, depth: ps.depth, limit: ps.limit })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
|
||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) {
|
||||
@ -66,7 +63,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
|
||||
const notes = await query.take(ps.limit).getMany();
|
||||
const notes = await query.getMany();
|
||||
|
||||
return await Notes.packMany(notes, user);
|
||||
return await Notes.packMany(notes, user, { detail: false });
|
||||
});
|
||||
|
@ -39,9 +39,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, me).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const clipNotes = await ClipNotes.findBy({
|
||||
|
@ -41,9 +41,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const conversation: Note[] = [];
|
||||
@ -51,7 +51,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
|
||||
async function get(id: any) {
|
||||
i++;
|
||||
const p = await Notes.findOneBy({ id });
|
||||
const p = await getNote(id, user).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') return null;
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (p == null) return;
|
||||
|
||||
if (i > ps.offset!) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import ms from 'ms';
|
||||
import { In } from 'typeorm';
|
||||
import create from '@/services/note/create.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
@ -10,6 +9,8 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { noteVisibilities } from '../../../../types.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import define from '../../define.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@ -17,7 +18,7 @@ export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
@ -83,7 +84,7 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
|
||||
visibility: { type: 'string', enum: noteVisibilities, default: 'public' },
|
||||
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
@ -185,11 +186,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
let renote: Note | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await Notes.findOneBy({ id: ps.renoteId });
|
||||
renote = await getNote(ps.renoteId, user).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (renote == null) {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
||||
if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
@ -208,11 +210,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
let reply: Note | null = null;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await Notes.findOneBy({ id: ps.replyId });
|
||||
reply = await getNote(ps.replyId, user).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
||||
if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from 'ms';
|
||||
import deleteNote from '@/services/note/delete.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { SECOND, HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@ -13,9 +13,9 @@ export const meta = {
|
||||
kind: 'write:notes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
minInterval: ms('1sec'),
|
||||
minInterval: SECOND,
|
||||
},
|
||||
|
||||
errors: {
|
||||
@ -43,9 +43,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) {
|
||||
|
@ -37,9 +37,9 @@ export const paramDef = {
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// Get favoritee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// if already favorited
|
||||
|
@ -36,9 +36,9 @@ export const paramDef = {
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// Get favoritee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// if already favorited
|
||||
|
@ -72,9 +72,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
const createdAt = new Date();
|
||||
|
||||
// Get votee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!note.hasPoll) {
|
||||
|
@ -3,6 +3,7 @@ import { NoteReactions } from '@/models/index.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'reactions'],
|
||||
@ -47,6 +48,12 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// check note visibility
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const query = {
|
||||
noteId: ps.noteId,
|
||||
} as FindOptionsWhere<NoteReaction>;
|
||||
@ -69,5 +76,5 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
relations: ['user', 'user.avatar', 'user.banner', 'note'],
|
||||
});
|
||||
|
||||
return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
|
||||
return await NoteReactions.packMany(reactions, user);
|
||||
});
|
||||
|
@ -42,9 +42,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
await createReaction(user, note, ps.reaction).catch(e => {
|
||||
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ms from 'ms';
|
||||
import deleteReaction from '@/services/note/reaction/delete.js';
|
||||
import define from '../../../define.js';
|
||||
import { getNote } from '../../../common/getters.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { SECOND, HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['reactions', 'notes'],
|
||||
@ -12,9 +12,9 @@ export const meta = {
|
||||
kind: 'write:reactions',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 60,
|
||||
minInterval: ms('3sec'),
|
||||
minInterval: 3 * SECOND,
|
||||
},
|
||||
|
||||
errors: {
|
||||
@ -42,9 +42,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
await deleteReaction(user, note).catch(e => {
|
||||
if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
|
||||
|
@ -45,9 +45,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
|
@ -34,12 +34,16 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
return await Notes.pack(note, user, {
|
||||
// FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774)
|
||||
detail: true,
|
||||
}).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
@ -36,7 +37,7 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await Notes.findOneByOrFail({ id: ps.noteId });
|
||||
const note = await getNote(ps.noteId, user);
|
||||
|
||||
const [favorite, watching, threadMuting] = await Promise.all([
|
||||
NoteFavorites.count({
|
||||
|
@ -31,9 +31,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const mutedNotes = await Notes.find({
|
||||
|
@ -29,9 +29,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
await NoteThreadMutings.delete({
|
||||
|
@ -39,15 +39,11 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
if (note.text == null) {
|
||||
return 204;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from 'ms';
|
||||
import deleteNote from '@/services/note/delete.js';
|
||||
import { Notes, Users } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { SECOND, HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@ -13,9 +13,9 @@ export const meta = {
|
||||
kind: 'write:notes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
minInterval: ms('1sec'),
|
||||
minInterval: SECOND,
|
||||
},
|
||||
|
||||
errors: {
|
||||
@ -37,9 +37,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const renotes = await Notes.findBy({
|
||||
|
@ -29,9 +29,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
await watch(user.id, note);
|
||||
|
@ -29,9 +29,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
await unwatch(user.id, note);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ms from 'ms';
|
||||
import { Pages, DriveFiles } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['pages'],
|
||||
@ -13,7 +13,7 @@ export const meta = {
|
||||
kind: 'write:pages',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ms from 'ms';
|
||||
import { Not } from 'typeorm';
|
||||
import { Pages, DriveFiles } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['pages'],
|
||||
@ -12,7 +12,7 @@ export const meta = {
|
||||
kind: 'write:pages',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
|
@ -28,9 +28,9 @@ export const paramDef = {
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const exist = await PromoReads.findOneBy({
|
||||
|
@ -1,5 +1,4 @@
|
||||
import rndstr from 'rndstr';
|
||||
import ms from 'ms';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import config from '@/config/index.js';
|
||||
@ -8,6 +7,7 @@ import { sendEmail } from '@/services/send-email.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { ApiError } from '../error.js';
|
||||
import define from '../define.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['reset password'],
|
||||
@ -17,7 +17,7 @@ export const meta = {
|
||||
description: 'Request a users password to be reset.',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
duration: HOUR,
|
||||
max: 3,
|
||||
},
|
||||
|
||||
|
@ -63,5 +63,5 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
|
||||
return await NoteReactions.packMany(reactions, me, { withNote: true });
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ms from 'ms';
|
||||
import { Users, Followings } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query.js';
|
||||
import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query.js';
|
||||
import { DAY } from '@/const.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||
.where('user.isLocked = FALSE')
|
||||
.andWhere('user.isExplorable = TRUE')
|
||||
.andWhere('user.host IS NULL')
|
||||
.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) })
|
||||
.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - (7 * DAY)) })
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
.orderBy('user.followersCount', 'DESC');
|
||||
|
||||
|
@ -33,6 +33,11 @@ export function genOpenapiSpec() {
|
||||
in: 'body',
|
||||
name: 'i',
|
||||
},
|
||||
// TODO: change this to oauth2 when the remaining oauth stuff is set up
|
||||
Bearer: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -71,6 +76,19 @@ export function genOpenapiSpec() {
|
||||
schema.required.push('file');
|
||||
}
|
||||
|
||||
const security = [
|
||||
{
|
||||
ApiKeyAuth: [],
|
||||
},
|
||||
{
|
||||
Bearer: [],
|
||||
},
|
||||
];
|
||||
if (!endpoint.meta.requireCredential) {
|
||||
// add this to make authentication optional
|
||||
security.push({});
|
||||
}
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
summary: endpoint.name,
|
||||
@ -79,14 +97,8 @@ export function genOpenapiSpec() {
|
||||
description: 'Source code',
|
||||
url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
|
||||
},
|
||||
...(endpoint.meta.tags ? {
|
||||
tags: [endpoint.meta.tags[0]],
|
||||
} : {}),
|
||||
...(endpoint.meta.requireCredential ? {
|
||||
security: [{
|
||||
ApiKeyAuth: [],
|
||||
}],
|
||||
} : {}),
|
||||
tags: endpoint.meta.tags || undefined,
|
||||
security,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
@ -181,9 +193,16 @@ export function genOpenapiSpec() {
|
||||
},
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
const path = {
|
||||
post: info,
|
||||
};
|
||||
if (endpoint.meta.allowGet) {
|
||||
path.get = { ...info };
|
||||
// API Key authentication is not permitted for GET requests
|
||||
path.get.security = path.get.security.filter(elem => !Object.prototype.hasOwnProperty.call(elem, 'ApiKeyAuth'));
|
||||
}
|
||||
|
||||
spec.paths['/' + endpoint.name] = path;
|
||||
}
|
||||
|
||||
return spec;
|
||||
|
@ -1,4 +1,8 @@
|
||||
import Connection from '.';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
/**
|
||||
* Stream channel
|
||||
@ -54,6 +58,32 @@ export default abstract class Channel {
|
||||
});
|
||||
}
|
||||
|
||||
protected withPackedNote(callback: (note: Packed<'Note'>) => void): (Note) => void {
|
||||
return async (note: Note) => {
|
||||
try {
|
||||
// because `note` was previously JSON.stringify'ed, the fields that
|
||||
// were objects before are now strings and have to be restored or
|
||||
// removed from the object
|
||||
note.createdAt = new Date(note.createdAt);
|
||||
delete note.reply;
|
||||
delete note.renote;
|
||||
delete note.user;
|
||||
delete note.channel;
|
||||
|
||||
const packed = await Notes.pack(note, this.user, { detail: true });
|
||||
|
||||
callback(packed);
|
||||
} catch (err) {
|
||||
if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
|
||||
// skip: note not visible to user
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public abstract init(params: any): void;
|
||||
public dispose?(): void;
|
||||
public onMessage?(type: string, body: any): void;
|
||||
|
@ -2,6 +2,7 @@ import Channel from '../channel.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'antenna';
|
||||
@ -23,16 +24,25 @@ export default class extends Channel {
|
||||
|
||||
private async onEvent(data: StreamMessages['antenna']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = await Notes.pack(data.body.id, this.user, { detail: true });
|
||||
try {
|
||||
const note = await Notes.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.blocking)) return;
|
||||
|
||||
this.connection.cacheNote(note);
|
||||
this.connection.cacheNote(note);
|
||||
|
||||
this.send('note', note);
|
||||
this.send('note', note);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError && e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
|
||||
// skip: note not visible to user
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.send(data.type, data.body);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Channel from '../channel.js';
|
||||
import { Notes, Users } from '@/models/index.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
@ -31,19 +31,6 @@ export default class extends Channel {
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
if (note.channelId !== this.channelId) return;
|
||||
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Channel from '../channel.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
@ -13,7 +12,7 @@ export default class extends Channel {
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -30,19 +29,6 @@ export default class extends Channel {
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Channel from '../channel.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
@ -12,7 +11,7 @@ export default class extends Channel {
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -29,13 +28,6 @@ export default class extends Channel {
|
||||
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
|
||||
if (!matched) return;
|
||||
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Channel from '../channel.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
@ -12,7 +11,7 @@ export default class extends Channel {
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -31,29 +30,6 @@ export default class extends Channel {
|
||||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
note = await Notes.pack(note.id, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
if (note.isHidden) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Channel from '../channel.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
@ -13,7 +12,7 @@ export default class extends Channel {
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -36,29 +35,6 @@ export default class extends Channel {
|
||||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||
)) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
note = await Notes.pack(note.id, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
if (note.isHidden) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user!, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Channel from '../channel.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
@ -12,7 +11,7 @@ export default class extends Channel {
|
||||
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -30,19 +29,6 @@ export default class extends Channel {
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
|
||||
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
const reply = note.reply;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Channel from '../channel.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
|
||||
|
||||
export default class extends Channel {
|
||||
@ -16,26 +15,12 @@ export default class extends Channel {
|
||||
if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
if (data.body.userId && this.muting.has(data.body.userId)) return;
|
||||
|
||||
if (data.body.note && data.body.note.isHidden) {
|
||||
const note = await Notes.pack(data.body.note.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
this.connection.cacheNote(note);
|
||||
data.body.note = note;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
if (this.muting.has(data.body.userId)) return;
|
||||
if (data.body.isHidden) {
|
||||
const note = await Notes.pack(data.body.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
this.connection.cacheNote(note);
|
||||
data.body = note;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Channel from '../channel.js';
|
||||
import { Notes, UserListJoinings, UserLists } from '@/models/index.js';
|
||||
import { UserListJoinings, UserLists } from '@/models/index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
@ -15,7 +15,7 @@ export default class extends Channel {
|
||||
constructor(id: string, connection: Channel['connection']) {
|
||||
super(id, connection);
|
||||
this.updateListUsers = this.updateListUsers.bind(this);
|
||||
this.onNote = this.onNote.bind(this);
|
||||
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||
}
|
||||
|
||||
public async init(params: any) {
|
||||
@ -51,29 +51,6 @@ export default class extends Channel {
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
if (!this.listUsers.includes(note.userId)) return;
|
||||
|
||||
if (['followers', 'specified'].includes(note.visibility)) {
|
||||
note = await Notes.pack(note.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
if (note.isHidden) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// リプライなら再pack
|
||||
if (note.replyId != null) {
|
||||
note.reply = await Notes.pack(note.replyId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
// Renoteなら再pack
|
||||
if (note.renoteId != null) {
|
||||
note.renote = await Notes.pack(note.renoteId, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@ -243,7 +243,7 @@ export type StreamMessages = {
|
||||
};
|
||||
notes: {
|
||||
name: 'notesStream';
|
||||
payload: Packed<'Note'>;
|
||||
payload: Note;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -17,10 +17,14 @@ export const initializeStreamingServer = (server: http.Server) => {
|
||||
ws.on('request', async (request) => {
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
|
||||
// TODO: トークンが間違ってるなどしてauthenticateに失敗したら
|
||||
// コネクション切断するなりエラーメッセージ返すなりする
|
||||
// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
|
||||
const [user, app] = await authenticate(q.i as string);
|
||||
const [user, app] = await authenticate(request.httpRequest.headers.authorization, q.i)
|
||||
.catch(err => {
|
||||
request.reject(403, err.message);
|
||||
return [];
|
||||
});
|
||||
if (typeof user === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user?.isSuspended) {
|
||||
request.reject(400);
|
||||
|
@ -78,7 +78,7 @@ const nodeinfo2 = async () => {
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
themeColor: meta.themeColor || '#86b300',
|
||||
themeColor: meta.themeColor || '#31748f',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
main > .tabs {
|
||||
padding: 16px;
|
||||
border-bottom: 4px solid #c3c3c3;
|
||||
border-bottom: 4px solid #908caa;
|
||||
}
|
||||
#lsEditor > .adder {
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
border: 2px solid #c3c3c3;
|
||||
border: 2px solid #908caa;
|
||||
}
|
||||
#lsEditor > .adder > textarea {
|
||||
display: block;
|
||||
@ -15,7 +15,7 @@ main > .tabs {
|
||||
}
|
||||
#lsEditor > .record {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #c3c3c3;
|
||||
border-bottom: 1px solid #908caa;
|
||||
}
|
||||
#lsEditor > .record > header {
|
||||
font-weight: 700;
|
||||
@ -28,15 +28,15 @@ main > .tabs {
|
||||
}
|
||||
|
||||
html {
|
||||
background: #222;
|
||||
background: #191724;
|
||||
}
|
||||
main {
|
||||
background: #333;
|
||||
background: #1f1d2e;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#tl > div {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #c3c3c3;
|
||||
border-bottom: 1px solid #908caa;
|
||||
}
|
||||
#tl > div > header {
|
||||
font-weight: 700;
|
||||
@ -50,8 +50,8 @@ main {
|
||||
}
|
||||
body,
|
||||
html {
|
||||
background-color: #222;
|
||||
color: #dfddcc;
|
||||
background-color: #191724;
|
||||
color: #e0def4;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
@ -63,9 +63,9 @@ button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
|
||||
background: linear-gradient(90deg, rgb(156, 207, 216), rgb(49, 116, 143));
|
||||
line-height: 50px;
|
||||
color: #222;
|
||||
color: #191724;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding: 12px;
|
||||
@ -80,29 +80,28 @@ button {
|
||||
button {
|
||||
background: #444;
|
||||
line-height: 40px;
|
||||
color: rgb(153, 204, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
font-size: 16px;
|
||||
padding: 0 20px;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
|
||||
}
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
#ls {
|
||||
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
|
||||
background: linear-gradient(90deg, rgb(156, 207, 216), rgb(49, 116, 143));
|
||||
line-height: 30px;
|
||||
color: #222;
|
||||
color: #191724;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding: 12px;
|
||||
}
|
||||
#ls:hover {
|
||||
background: rgb(153, 204, 0);
|
||||
background: rgb(156, 207, 216);
|
||||
}
|
||||
a {
|
||||
color: rgb(134, 179, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
text-decoration: none;
|
||||
}
|
||||
p,
|
||||
@ -120,7 +119,7 @@ textarea {
|
||||
background-color: #444;
|
||||
border: solid #aaa;
|
||||
border-radius: 10px;
|
||||
color: #dfddcc;
|
||||
color: #e0def4;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 20rem;
|
||||
@ -135,7 +134,7 @@ input {
|
||||
background-color: #666;
|
||||
border: solid #aaa;
|
||||
border-radius: 10px;
|
||||
color: #dfddcc;
|
||||
color: #e0def4;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
width: 10rem;
|
||||
|
@ -137,6 +137,7 @@
|
||||
<span class="button-label-big">Refresh</span>
|
||||
</button>
|
||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||
<p>Please make sure your browser is up-to-date and any AdBlockers are off.</p>
|
||||
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
@ -180,8 +181,8 @@
|
||||
|
||||
body,
|
||||
html {
|
||||
background-color: #222;
|
||||
color: #dfddcc;
|
||||
background-color: #191724;
|
||||
color: #e0def4;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
@ -197,12 +198,12 @@
|
||||
}
|
||||
|
||||
.button-big {
|
||||
background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
|
||||
background: linear-gradient(90deg, rgb(196, 167, 231), rgb(235, 188, 186));
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.button-big:hover {
|
||||
background: rgb(153, 204, 0);
|
||||
background: rgb(49, 116, 143);
|
||||
}
|
||||
|
||||
.button-small {
|
||||
@ -215,20 +216,20 @@
|
||||
}
|
||||
|
||||
.button-label-big {
|
||||
color: #222;
|
||||
color: #191724;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.button-label-small {
|
||||
color: rgb(153, 204, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(134, 179, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -243,7 +244,7 @@
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
color: #dec340;
|
||||
color: #f6c177;
|
||||
height: 4rem;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
@ -257,7 +258,7 @@
|
||||
}
|
||||
|
||||
details {
|
||||
background: #333;
|
||||
background: #1f1d2e;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
width: 40rem;
|
||||
|
@ -1,13 +1,13 @@
|
||||
html {
|
||||
background: #222;
|
||||
background: #191724;
|
||||
}
|
||||
main {
|
||||
background: #333;
|
||||
background: #1f1d2e;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#tl > div {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #c3c3c3;
|
||||
border-bottom: 1px solid #908caa;
|
||||
}
|
||||
#tl > div > header {
|
||||
font-weight: 700;
|
||||
@ -21,8 +21,8 @@ main {
|
||||
}
|
||||
body,
|
||||
html {
|
||||
background-color: #222;
|
||||
color: #dfddcc;
|
||||
background-color: #191724;
|
||||
color: #e0def4;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
@ -35,17 +35,17 @@ button {
|
||||
border:none;
|
||||
cursor:pointer;
|
||||
margin-bottom:12px;
|
||||
background:linear-gradient(90deg,#86b300,#4ab300);
|
||||
background:linear-gradient(90deg,#9ccfd8,#31748f);
|
||||
line-height:50px;
|
||||
color:#222;
|
||||
color:#191724;
|
||||
font-weight:700;
|
||||
font-size:20px;
|
||||
}
|
||||
button:hover {
|
||||
background: rgb(153, 204, 0);
|
||||
background: rgb(156, 207, 216);
|
||||
}
|
||||
a {
|
||||
color: rgb(134, 179, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
text-decoration: none;
|
||||
}
|
||||
p,
|
||||
@ -63,7 +63,7 @@ code {
|
||||
background-color: #444;
|
||||
border: solid #aaa;
|
||||
border-radius: 10px;
|
||||
color: #dfddcc;
|
||||
color: #e0def4;
|
||||
margin-top: 3rem;
|
||||
width: 20rem;
|
||||
height: 5rem;
|
||||
|
@ -4,8 +4,7 @@
|
||||
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
|
||||
import ms from 'ms';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import send from 'koa-send';
|
||||
@ -27,6 +26,7 @@ import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
|
||||
import { urlPreviewHandler } from './url-preview.js';
|
||||
import { manifestHandler } from './manifest.js';
|
||||
import packFeed from './feed.js';
|
||||
import { MINUTE, DAY } from '@/const.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@ -100,21 +100,21 @@ const router = new Router();
|
||||
router.get('/static-assets/(.*)', async ctx => {
|
||||
await send(ctx as any, ctx.path.replace('/static-assets/', ''), {
|
||||
root: staticAssets,
|
||||
maxage: ms('7 days'),
|
||||
maxage: 7 * DAY,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/client-assets/(.*)', async ctx => {
|
||||
await send(ctx as any, ctx.path.replace('/client-assets/', ''), {
|
||||
root: clientAssets,
|
||||
maxage: ms('7 days'),
|
||||
maxage: 7 * DAY,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/assets/(.*)', async ctx => {
|
||||
await send(ctx as any, ctx.path.replace('/assets/', ''), {
|
||||
root: assets,
|
||||
maxage: ms('7 days'),
|
||||
maxage: 7 * DAY,
|
||||
});
|
||||
});
|
||||
|
||||
@ -137,7 +137,7 @@ router.get('/twemoji/(.*)', async ctx => {
|
||||
|
||||
await send(ctx as any, path, {
|
||||
root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`,
|
||||
maxage: ms('30 days'),
|
||||
maxage: 30 * DAY,
|
||||
});
|
||||
});
|
||||
|
||||
@ -188,7 +188,7 @@ router.get('/twemoji-badge/(.*)', async ctx => {
|
||||
router.get(`/sw.js`, async ctx => {
|
||||
await send(ctx as any, `/sw.js`, {
|
||||
root: swAssets,
|
||||
maxage: ms('10 minutes'),
|
||||
maxage: 10 * MINUTE,
|
||||
});
|
||||
});
|
||||
|
||||
@ -344,24 +344,32 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||
});
|
||||
|
||||
if (note) {
|
||||
const _note = await Notes.pack(note);
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
|
||||
const meta = await fetchMeta();
|
||||
await ctx.render('note', {
|
||||
note: _note,
|
||||
profile,
|
||||
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })),
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
instanceName: meta.name || 'Calckey',
|
||||
icon: meta.iconUrl,
|
||||
privateMode: meta.privateMode,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
try {
|
||||
// FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774)
|
||||
const _note = await Notes.pack(note);
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
|
||||
const meta = await fetchMeta();
|
||||
await ctx.render('note', {
|
||||
note: _note,
|
||||
profile,
|
||||
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })),
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
instanceName: meta.name || 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
|
||||
// note not visible to user
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
|
@ -3,8 +3,8 @@
|
||||
"name": "Calckey",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#313a42",
|
||||
"theme_color": "#86b300",
|
||||
"background_color": "#6e6a86",
|
||||
"theme_color": "#31748f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static-assets/icons/192.png",
|
||||
|
@ -12,15 +12,15 @@ html
|
||||
}
|
||||
body,
|
||||
html {
|
||||
background-color: #222;
|
||||
color: #dfddcc;
|
||||
background-color: #191724;
|
||||
color: #e0def4;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
a {
|
||||
color: rgb(134, 179, 0);
|
||||
color: rgb(156, 207, 216);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -345,19 +345,15 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
}
|
||||
}
|
||||
|
||||
// Pack the note
|
||||
const noteObj = await Notes.pack(note);
|
||||
publishNotesStream(note);
|
||||
|
||||
publishNotesStream(noteObj);
|
||||
const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note')));
|
||||
|
||||
getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'note', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
});
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'note', {
|
||||
note: await Notes.pack(note, user),
|
||||
});
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
@ -378,12 +374,14 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
|
||||
const packedReply = await Notes.pack(note, { id: data.reply.userId });
|
||||
publishMainStream(data.reply.userId, 'reply', packedReply);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'reply', {
|
||||
note: noteObj,
|
||||
note: packedReply,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -404,12 +402,13 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
|
||||
// Publish event
|
||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||
publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||
const packedRenote = await Notes.pack(note, { id: data.renote.userId });
|
||||
publishMainStream(data.renote.userId, 'renote', packedRenote);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'renote', {
|
||||
note: noteObj,
|
||||
note: packedRenote,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -642,17 +641,23 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note,
|
||||
continue;
|
||||
}
|
||||
|
||||
const detailPackedNote = await Notes.pack(note, u, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'mention', {
|
||||
note: detailPackedNote,
|
||||
// note with "specified" visibility might not be visible to mentioned users
|
||||
try {
|
||||
const detailPackedNote = await Notes.pack(note, u, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'mention', {
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') continue;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Create notification
|
||||
|
@ -51,7 +51,7 @@ export async function sendEmail(to: string, subject: string, html: string, text:
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #86b300;
|
||||
color: #31748f;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@ -60,12 +60,12 @@ export async function sendEmail(to: string, subject: string, html: string, text:
|
||||
main {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #555;
|
||||
background: #e0def4;
|
||||
color: #6e6a86;
|
||||
}
|
||||
main > header {
|
||||
padding: 32px;
|
||||
background: #86b300;
|
||||
background: #31748f;
|
||||
}
|
||||
main > header > img {
|
||||
max-width: 128px;
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
UserListStreamTypes,
|
||||
UserStreamTypes,
|
||||
} from '@/server/api/stream/types.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
class Publisher {
|
||||
private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
|
||||
@ -87,7 +86,7 @@ class Publisher {
|
||||
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
};
|
||||
|
||||
public publishNotesStream = (note: Packed<'Note'>): void => {
|
||||
public publishNotesStream = (note: Note): void => {
|
||||
this.publish('notesStream', null, note);
|
||||
};
|
||||
|
||||
|
@ -154,18 +154,18 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] followers-postを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(fol.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] followers-postを未認証が見れない', async(async () => {
|
||||
const res = await show(fol.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
it('[show] specified-postを自分が見れる', async(async () => {
|
||||
const res = await show(spe.id, alice);
|
||||
assert.strictEqual(res.body.text, 'x');
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-postを指定ユーザーが見れる', async(async () => {
|
||||
@ -175,17 +175,17 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] specified-postをフォロワーが見れない', async(async () => {
|
||||
const res = await show(spe.id, follower);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-postを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(spe.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-postを未認証が見れない', async(async () => {
|
||||
const res = await show(spe.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
@ -260,12 +260,12 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] followers-replyを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(folR.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] followers-replyを未認証が見れない', async(async () => {
|
||||
const res = await show(folR.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
@ -286,17 +286,17 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] specified-replyをフォロワーが見れない', async(async () => {
|
||||
const res = await show(speR.id, follower);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-replyを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(speR.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-replyを未認証が見れない', async(async () => {
|
||||
const res = await show(speR.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
@ -371,12 +371,12 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(folM.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] followers-mentionを未認証が見れない', async(async () => {
|
||||
const res = await show(folM.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
@ -392,22 +392,22 @@ describe('API visibility', () => {
|
||||
|
||||
it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => {
|
||||
const res = await show(speM.id, target2);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-mentionをフォロワーが見れない', async(async () => {
|
||||
const res = await show(speM.id, follower);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
|
||||
const res = await show(speM.id, other);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it('[show] specified-mentionを未認証が見れない', async(async () => {
|
||||
const res = await show(speM.id, null);
|
||||
assert.strictEqual(res.body.isHidden, true);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
|
@ -15,47 +15,34 @@
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "3.0.1",
|
||||
"@vue/compiler-sfc": "3.2.37",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.13.0",
|
||||
"browser-image-resizer": "misskey-dev/browser-image-resizer#tag=v2.2.1-misskey.2",
|
||||
"chart.js": "3.8.0",
|
||||
"broadcast-channel": "4.14.0",
|
||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
|
||||
"chart.js": "3.8.2",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-gradient": "0.5.0",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "4.1.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"cropperjs": "2.0.0-beta",
|
||||
"date-fns": "2.28.0",
|
||||
"date-fns": "2.29.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.1",
|
||||
"katex": "0.15.6",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.0-canary.1",
|
||||
"mfm-js": "0.23.0",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "10.0.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"photoswipe": "5.2.8",
|
||||
"photoswipe": "5.3.0",
|
||||
"prismjs": "1.28.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.5.0",
|
||||
"querystring": "0.2.1",
|
||||
"random-seed": "0.3.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.53.0",
|
||||
"sass": "1.54.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
@ -64,47 +51,37 @@
|
||||
"three": "0.142.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.6.11",
|
||||
"tsc-alias": "1.7.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "4.7.4",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vite": "^3.0.2",
|
||||
"vite": "3.0.3",
|
||||
"vue": "3.2.37",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.8.0"
|
||||
"vuedraggable": "4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "7.2.0",
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/is-url": "1.2.30",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.17.7",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.2",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/seedrandom": "3.0.2",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.7",
|
||||
"@typescript-eslint/parser": "5.30.7",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"eslint": "8.19.0",
|
||||
"cypress": "10.3.1",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.2.0",
|
||||
"rollup": "2.76.0",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"rollup": "2.77.0",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
@ -15,27 +15,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="depth < 5">
|
||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
|
||||
<template v-if="conversation">
|
||||
<template v-if="depth < 5">
|
||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1"/>
|
||||
</template>
|
||||
<div v-else-if="replies.length > 0" class="more">
|
||||
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="more">
|
||||
<MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { notePage } from '@/filters/note';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import MkNoteSubNoteContent from './sub-note-content.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import { notePage } from '@/filters/note';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
detail?: boolean;
|
||||
conversation?: misskey.entities.Note[];
|
||||
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
depth?: number;
|
||||
@ -44,16 +47,7 @@ const props = withDefaults(defineProps<{
|
||||
});
|
||||
|
||||
let showContent = $ref(false);
|
||||
let replies: misskey.entities.Note[] = $ref([]);
|
||||
|
||||
if (props.detail) {
|
||||
os.api('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: 5
|
||||
}).then(res => {
|
||||
replies = res;
|
||||
});
|
||||
}
|
||||
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id) ?? [];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</MkA>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.registeredDate }}</template>
|
||||
<template #key>{{ i18n.ts.registeredDate }}</template>
|
||||
<template #value>{{ new Date(report.targetUser.createdAt).toLocaleString() }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
@ -18,18 +18,18 @@
|
||||
<Mfm :text="report.comment"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
||||
<div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
|
||||
<div v-if="report.assignee">
|
||||
{{ $ts.moderator }}:
|
||||
{{ i18n.ts.moderator }}:
|
||||
<MkAcct :user="report.assignee"/>
|
||||
</div>
|
||||
<div><MkTime :time="report.createdAt"/></div>
|
||||
<div class="action">
|
||||
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
||||
{{ $ts.forwardReport }}
|
||||
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
|
||||
{{ i18n.ts.forwardReport }}
|
||||
<template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
|
||||
<MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,6 +41,7 @@ import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import { acct, userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
report: any;
|
||||
|
@ -397,17 +397,17 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
|
||||
series: [{
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(raw.inboxReceived),
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
color: '#c4a7e7',
|
||||
data: format(raw.deliverSucceeded),
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
color: '#f6c177',
|
||||
data: format(raw.deliverFailed),
|
||||
}],
|
||||
};
|
||||
@ -636,17 +636,17 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
||||
series: [{
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(raw.requests.received),
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
color: '#c4a7e7',
|
||||
data: format(raw.requests.succeeded),
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
color: '#f6c177',
|
||||
data: format(raw.requests.failed),
|
||||
}],
|
||||
};
|
||||
@ -658,7 +658,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
|
||||
series: [{
|
||||
name: 'Users',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(total
|
||||
? raw.users.total
|
||||
: sum(raw.users.inc, negate(raw.users.dec)),
|
||||
@ -673,7 +673,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(total
|
||||
? raw.notes.total
|
||||
: sum(raw.notes.inc, negate(raw.notes.dec)),
|
||||
@ -688,7 +688,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
|
||||
series: [{
|
||||
name: 'Following',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(total
|
||||
? raw.following.total
|
||||
: sum(raw.following.inc, negate(raw.following.dec)),
|
||||
@ -696,7 +696,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
|
||||
}, {
|
||||
name: 'Followers',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
color: '#c4a7e7',
|
||||
data: format(total
|
||||
? raw.followers.total
|
||||
: sum(raw.followers.inc, negate(raw.followers.dec)),
|
||||
@ -712,7 +712,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
||||
series: [{
|
||||
name: 'Drive usage',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(total
|
||||
? raw.drive.totalUsage
|
||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
|
||||
@ -727,7 +727,7 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
color: '#31748f',
|
||||
data: format(total
|
||||
? raw.drive.totalFiles
|
||||
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user