Merge pull request 'develop' (#9013) from develop into main

Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9013
This commit is contained in:
Kainoa Kanter 2022-07-26 02:37:56 +02:00
commit bc7f3043bc
240 changed files with 3201 additions and 2700 deletions

View file

@ -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

View file

@ -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)

View file

@ -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?

View file

@ -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!

View file

@ -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."

View file

@ -36,6 +36,7 @@ const languages = [
'sk-SK',
'ug-CN',
'uk-UA',
'vi-VN',
'zh-CN',
'zh-TW',
];

View file

@ -900,6 +900,7 @@ navbar: "ナビゲーションバー"
shuffle: "シャッフル"
account: "アカウント"
move: "移動"
adminCustomCssWarn: "この設定は、それが何をするものであるかを知っている場合のみ使用してください。不適切な値を入力すると、クライアントが正常に動作しなくなる可能性があります。ユーザー設定でCSSをテストし、正しく動作することを確認してください。"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"

View file

@ -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"

View file

@ -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`);
}
}

View file

@ -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",

View file

@ -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

View file

@ -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();
};

View file

@ -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] : []));
},
});

View file

@ -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] : []);
}
});

View file

@ -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] : []);
},
});

View file

@ -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,

View file

@ -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');
}

View file

@ -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());
}

View file

@ -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, {

View file

@ -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.');

View file

@ -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 });

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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({

View file

@ -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,
},

View file

@ -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,

View file

@ -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;

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -13,7 +13,7 @@ export const meta = {
limit: {
duration: 60000,
max: 10,
max: 15,
},
kind: 'read:notifications',

View file

@ -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,
},

View file

@ -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: {

View file

@ -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 });
});

View file

@ -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({

View file

@ -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!) {

View file

@ -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);
}

View file

@ -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)) {

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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);
});

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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;
});
});

View file

@ -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({

View file

@ -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({

View file

@ -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({

View file

@ -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;
}

View file

@ -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({

View file

@ -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);

View file

@ -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);

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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({

View file

@ -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,
},

View file

@ -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 });
});

View file

@ -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');

View file

@ -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;

View file

@ -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;

View file

@ -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);
}

View file

@ -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がブロックされているユーザーが関わるものだったら無視する

View file

@ -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;

View file

@ -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がブロックされているユーザーが関わるものだったら無視する

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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がブロックされているユーザーが関わるものだったら無視する

View file

@ -243,7 +243,7 @@ export type StreamMessages = {
};
notes: {
name: 'notesStream';
payload: Packed<'Note'>;
payload: Note;
};
};

View file

@ -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);

View file

@ -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',
},
};
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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",

View file

@ -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;
}

View file

@ -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

View file

@ -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;

View file

@ -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);
};

View file

@ -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

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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;

View file

@ -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