forked from FoundKeyGang/FoundKey
Compare commits
40 commits
9c5755cf65
...
77358c8f4b
Author | SHA1 | Date | |
---|---|---|---|
77358c8f4b | |||
62cd1e7ed6 | |||
796adc8599 | |||
ff8b9b6651 | |||
c1268c04f8 | |||
42b555e5e4 | |||
5c3e7c132a | |||
76c8e6b11b | |||
ca24080596 | |||
a12debb7b6 | |||
f760426142 | |||
2f30af1812 | |||
2d46cf7c1e | |||
2ea6daaf7a | |||
597de07465 | |||
9289b0e8ed | |||
b600efae0d | |||
ecca5a164e | |||
1125a623a7 | |||
51a319e8ca | |||
f7904a240a | |||
693dd3ad97 | |||
777b981bf1 | |||
bbd35054e6 | |||
|
e9862d480c | ||
cc0915775b | |||
f181a8805d | |||
ac482e6eec | |||
38786b6999 | |||
680d1f1459 | |||
a0f0bac1ca | |||
af003fc0fe | |||
94cd10365d | |||
0addcddd6c | |||
64c4973eca | |||
7272bde464 | |||
7c9e118ff1 | |||
bed8286175 | |||
f00b3cc378 | |||
21ab8e75ee |
100 changed files with 1587 additions and 1188 deletions
|
@ -153,3 +153,9 @@ redis:
|
|||
# info: /twemoji/1f440.svg
|
||||
# notFound: /twemoji/2049.svg
|
||||
# error: /twemoji/1f480.svg
|
||||
|
||||
# Whether it should be allowed to fetch content in ActivityPub form without HTTP signatures.
|
||||
# It is recommended to leave this as default to improve the effectiveness of instance blocks.s
|
||||
# However, note that while this prevents fetching in ActivityPub form, it could still be scraped
|
||||
# from the API or other representations if the other side is determined to do so.
|
||||
#allowUnsignedFetches: false
|
||||
|
|
2
.mailmap
2
.mailmap
|
@ -6,7 +6,7 @@ Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast+git@toast.cafe>
|
|||
Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast@toast.cafe>
|
||||
Dr. Gutfuck LLC <40531868+gutfuckllc@users.noreply.github.com>
|
||||
Ehsan Javadynia <31900907+ehsanjavadynia@users.noreply.github.com> <ehsan.javadynia@gmail.com>
|
||||
Francis Dinh <normandy@biribiri.dev>
|
||||
Norm <normandy@biribiri.dev>
|
||||
Hakaba Hitoyo <tsukadayoshio@gmail.com> Hakaba Hitoyo <example@example.com>
|
||||
Johann150 <johann.galle@protonmail.com> <johann@qwertqwefsday.eu>
|
||||
Michcio <public+git@meekchopp.es> <michcio@noreply.akkoma>
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -11,6 +11,33 @@ Unreleased changes should not be listed in this file.
|
|||
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
||||
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||
|
||||
## 13.0.0-preview6 - 2023-07-02
|
||||
|
||||
## Added
|
||||
- **BREAKING** activitypub: validate fetch signatures
|
||||
Fetching the ActivityPub representation of something now requires a valid HTTP signature.
|
||||
- client: add MFM functions `position`, `scale`, `fg`, `bg`
|
||||
- server: add webhook stat to nodeinfo
|
||||
- activitypub: handle incoming Update Note activities
|
||||
|
||||
## Changed
|
||||
- client: change followers only icon to closed lock
|
||||
- client: disable sound for received note by default
|
||||
- client: always forbid MFM overflow
|
||||
- make mutes case insensitive
|
||||
- activitypub: improve JSON-LD context
|
||||
The context now properly notes the `@type`s of defined attributes.
|
||||
- docker: only publish port on localhost
|
||||
|
||||
## Fixed
|
||||
- server: fix internal download in emoji import
|
||||
- server: replace unzipper with decompress
|
||||
|
||||
## Removed
|
||||
- migrate note favorites to clips
|
||||
If you previously had favorites they will now be in a clip called "⭐".
|
||||
If you want to add a note as a "favorite" you can use the menu item "Clip".
|
||||
|
||||
## 13.0.0-preview5 - 2023-05-23
|
||||
This release contains 6 breaking changes and 1 security update.
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ services:
|
|||
- redis
|
||||
# - es
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "127.0.0.1:3000:3000"
|
||||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
|
|
@ -1309,6 +1309,7 @@ _notification:
|
|||
groupInvited: "Erhaltene Gruppeneinladungen"
|
||||
app: "Benachrichtigungen von Apps"
|
||||
move: Account-Umzüge
|
||||
updated: Beobachtete Notiz wurde bearbeitet
|
||||
_actions:
|
||||
followBack: "folgt dir nun auch"
|
||||
reply: "Antworten"
|
||||
|
|
|
@ -1304,6 +1304,7 @@ _notification:
|
|||
reaction: "Reactions"
|
||||
pollVote: "Votes on polls"
|
||||
pollEnded: "Polls ending"
|
||||
updated: "Watched Note was updated"
|
||||
receiveFollowRequest: "Received follow requests"
|
||||
followRequestAccepted: "Accepted follow requests"
|
||||
groupInvited: "Group invitations"
|
||||
|
|
|
@ -748,6 +748,7 @@ _ffVisibility:
|
|||
followers: "フォロワーだけに公開"
|
||||
private: "非公開"
|
||||
|
||||
nobody: 誰にも見せない (あなたにさえも)
|
||||
_signup:
|
||||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "foundkey",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"extension": ["ts","js","cjs","mjs"],
|
||||
"node-option": [
|
||||
"experimental-specifier-resolution=node",
|
||||
"loader=./test/loader.js"
|
||||
"experimental-specifier-resolution=node"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 30000,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
export class noteEditing1685997617959 {
|
||||
name = 'noteEditing1685997617959';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."updatedAt" IS 'The updated date of the Note.'`);
|
||||
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"main": "./index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -8,10 +8,10 @@
|
|||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
"mocha": "NODE_ENV=test mocha",
|
||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"test": "npm run mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -41,6 +41,7 @@
|
|||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.28.0",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
|
@ -109,7 +110,6 @@
|
|||
"tsconfig-paths": "4.1.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.7",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.5.0",
|
||||
"ws": "8.8.0",
|
||||
|
@ -164,7 +164,6 @@
|
|||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
|
|
|
@ -61,6 +61,7 @@ export function loadConfig(): Config {
|
|||
proxyRemoteFiles: false,
|
||||
maxFileSize: 262144000, // 250 MiB
|
||||
maxNoteTextLength: 3000,
|
||||
allowUnsignedFetches: false,
|
||||
}, config);
|
||||
|
||||
mixin.version = meta.version;
|
||||
|
|
|
@ -68,6 +68,8 @@ export type Source = {
|
|||
notFound?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
allowUnsignedFetches?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,10 +20,11 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
|
|||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
|
||||
if (text === '') return false;
|
||||
const textLower = text.toLowerCase();
|
||||
|
||||
const matched = mutedWords.some(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.every(keyword => text.includes(keyword));
|
||||
return filter.every(keyword => textLower.includes(keyword.toLowerCase()));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
|
|
@ -11,7 +11,7 @@ export function isSelfHost(host: string | null): boolean {
|
|||
return toPuny(config.host) === toPuny(host);
|
||||
}
|
||||
|
||||
export function extractDbHost(uri: string): string {
|
||||
export function extractPunyHost(uri: string): string {
|
||||
const url = new URL(uri);
|
||||
return toPuny(url.hostname);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,12 @@ export class Note {
|
|||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The updated date of the Note.',
|
||||
})
|
||||
public updatedAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -169,6 +169,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
updatedAt: note.updatedAt?.toISOString() ?? null,
|
||||
userId: note.userId,
|
||||
user: Users.pack(note.user ?? note.userId, me, {
|
||||
detail: false,
|
||||
|
|
|
@ -12,6 +12,11 @@ export const packedNoteSchema = {
|
|||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -32,11 +32,6 @@ export const packedUserLiteSchema = {
|
|||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
avatarColor: {
|
||||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
default: null,
|
||||
},
|
||||
isAdmin: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
|
@ -122,11 +117,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
bannerColor: {
|
||||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
default: null,
|
||||
},
|
||||
isLocked: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as fs from 'node:fs';
|
||||
import Bull from 'bull';
|
||||
import unzipper from 'unzipper';
|
||||
import decompress from 'decompress';
|
||||
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { DriveFiles, Emojis } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { copyFileTo } from '@/services/drive/read-file.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-custom-emojis');
|
||||
|
||||
|
@ -33,7 +33,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
|
||||
try {
|
||||
fs.writeFileSync(destPath, '', 'binary');
|
||||
await downloadUrl(file.url, destPath);
|
||||
await copyFileTo(file, destPath);
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
if (e instanceof Error || typeof e === 'string') {
|
||||
logger.error(e);
|
||||
|
@ -42,44 +42,41 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
}
|
||||
|
||||
const outputPath = path + '/emojis';
|
||||
const unzipStream = fs.createReadStream(destPath);
|
||||
const extractor = unzipper.Extract({ path: outputPath });
|
||||
extractor.on('close', async () => {
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
});
|
||||
unzipStream.pipe(extractor);
|
||||
logger.succ(`Unzipping to ${outputPath}`);
|
||||
await decompress(destPath, outputPath);
|
||||
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { perform } from '@/remote/activitypub/perform.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||
import { toPuny, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
|
||||
import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { AuthUser, getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { InboxJobData } from '@/queue/types.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
||||
// Processing when an activity arrives in the user's inbox
|
||||
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
const resolver = new Resolver();
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity) as any;
|
||||
|
@ -29,46 +29,12 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
logger.debug(JSON.stringify(info, null, 2));
|
||||
//#endregion
|
||||
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||
}
|
||||
|
||||
const host = toPuny(new URL(keyIdLower).hostname);
|
||||
|
||||
// Stop if the host is blocked.
|
||||
if (await shouldBlockInstance(host)) {
|
||||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let authUser;
|
||||
try {
|
||||
authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver);
|
||||
} catch (e) {
|
||||
if (e instanceof StatusError) {
|
||||
if (e.isClientError) {
|
||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||
} else {
|
||||
throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
// Key not found? Unacceptable!
|
||||
return 'skip: failed to resolve user';
|
||||
} else {
|
||||
// Found key!
|
||||
}
|
||||
|
||||
// verify the HTTP Signature
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
const validated = await verifyHttpSignature(signature, resolver, getApId(activity.actor));
|
||||
let authUser = validated.authUser;
|
||||
|
||||
// The signature must be valid.
|
||||
// The signature must also match the actor otherwise anyone could sign any activity.
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) {
|
||||
// Last resort: LD-Signature
|
||||
if (activity.signature) {
|
||||
if (activity.signature.type !== 'RsaSignature2017') {
|
||||
|
@ -98,7 +64,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
}
|
||||
|
||||
// Stop if the host is blocked.
|
||||
const ldHost = extractDbHost(authUser.user.uri);
|
||||
const ldHost = extractPunyHost(authUser.user.uri);
|
||||
if (await shouldBlockInstance(ldHost)) {
|
||||
return `Blocked request: ${ldHost}`;
|
||||
}
|
||||
|
@ -107,15 +73,20 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
}
|
||||
}
|
||||
|
||||
// authUser cannot be null at this point:
|
||||
// either it was already not null because the HTTP signature was valid
|
||||
// or, if the LD signature was not verified, this function will already have returned.
|
||||
authUser = authUser as AuthUser;
|
||||
|
||||
// Verify that the actor's host is not blocked
|
||||
const signerHost = extractDbHost(authUser.user.uri!);
|
||||
const signerHost = extractPunyHost(authUser.user.uri!);
|
||||
if (await shouldBlockInstance(signerHost)) {
|
||||
return `Blocked request: ${signerHost}`;
|
||||
}
|
||||
|
||||
if (typeof activity.id === 'string') {
|
||||
// Verify that activity and actor are from the same host.
|
||||
const activityIdHost = extractDbHost(activity.id);
|
||||
const activityIdHost = extractPunyHost(activity.id);
|
||||
if (signerHost !== activityIdHost) {
|
||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
||||
}
|
||||
|
|
|
@ -10,52 +10,49 @@ const logger = queueLogger.createSubLogger('check-expired');
|
|||
export async function checkExpired(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
|
||||
logger.info('Checking expired data...');
|
||||
|
||||
const expiredMutings = await Mutings.createQueryBuilder('muting')
|
||||
.where('muting.expiresAt IS NOT NULL')
|
||||
.andWhere('muting.expiresAt < :now', { now: new Date() })
|
||||
.innerJoinAndSelect('muting.mutee', 'mutee')
|
||||
.getMany();
|
||||
|
||||
if (expiredMutings.length > 0) {
|
||||
await Mutings.delete({
|
||||
id: In(expiredMutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
for (const m of expiredMutings) {
|
||||
publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||
}
|
||||
}
|
||||
|
||||
const OlderThan = (millis: number) => {
|
||||
return LessThan(new Date(new Date().getTime() - millis));
|
||||
};
|
||||
|
||||
await Signins.delete({
|
||||
createdAt: OlderThan(2 * MONTH),
|
||||
});
|
||||
await Promise.all([
|
||||
Mutings.createQueryBuilder('muting')
|
||||
.where('muting.expiresAt IS NOT NULL')
|
||||
.andWhere('muting.expiresAt < :now', { now: new Date() })
|
||||
.innerJoinAndSelect('muting.mutee', 'mutee')
|
||||
.getMany()
|
||||
.then(async (expiredMutings) => {
|
||||
if (expiredMutings.length > 0) {
|
||||
await Mutings.delete({
|
||||
id: In(expiredMutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
await AttestationChallenges.delete({
|
||||
createdAt: OlderThan(5 * MINUTE),
|
||||
});
|
||||
|
||||
await PasswordResetRequests.delete({
|
||||
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
||||
createdAt: OlderThan(30 * MINUTE),
|
||||
});
|
||||
|
||||
await AuthSessions.delete({
|
||||
createdAt: OlderThan(15 * MINUTE),
|
||||
});
|
||||
|
||||
await Notifications.delete({
|
||||
isRead: true,
|
||||
createdAt: OlderThan(3 * MONTH),
|
||||
});
|
||||
|
||||
await Users.delete({
|
||||
// delete users where the deletion status reference count has come down to zero
|
||||
isDeleted: 0,
|
||||
});
|
||||
for (const m of expiredMutings) {
|
||||
publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||
}
|
||||
}
|
||||
}),
|
||||
Signins.delete({
|
||||
createdAt: OlderThan(2 * MONTH),
|
||||
}),
|
||||
AttestationChallenges.delete({
|
||||
createdAt: OlderThan(5 * MINUTE),
|
||||
}),
|
||||
PasswordResetRequests.delete({
|
||||
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
||||
createdAt: OlderThan(30 * MINUTE),
|
||||
}),
|
||||
AuthSessions.delete({
|
||||
createdAt: OlderThan(15 * MINUTE),
|
||||
}),
|
||||
Notifications.delete({
|
||||
isRead: true,
|
||||
createdAt: OlderThan(3 * MONTH),
|
||||
}),
|
||||
Users.delete({
|
||||
// delete users where the deletion status reference count has come down to zero
|
||||
isDeleted: 0,
|
||||
}),
|
||||
]);
|
||||
|
||||
logger.succ('Deleted expired data.');
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
|
@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
|||
const uri = getApId(activity);
|
||||
|
||||
// Cancel if the announced from host is blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) return;
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) return;
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { createNote, fetchNote } from '@/remote/activitypub/models/note.js';
|
||||
|
@ -18,7 +18,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
|
|||
}
|
||||
|
||||
if (typeof note.id === 'string') {
|
||||
if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
|
||||
if (extractPunyHost(actor.uri) !== extractPunyHost(note.id)) {
|
||||
return 'skip: host in actor.uri !== note.id';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||
|
@ -42,7 +42,7 @@ async function performOneActivity(actor: IRemoteUser, activity: IObject, resolve
|
|||
if (actor.isSuspended) return;
|
||||
|
||||
if (typeof activity.id !== 'undefined') {
|
||||
const host = extractDbHost(getApId(activity));
|
||||
const host = extractPunyHost(getApId(activity));
|
||||
if (await shouldBlockInstance(host)) return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { isSelfHost, extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { MessagingMessages } from '@/models/index.js';
|
||||
import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js';
|
||||
import { IRead, getApId } from '../type.js';
|
||||
|
@ -7,7 +7,7 @@ import { IRead, getApId } from '../type.js';
|
|||
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
|
||||
const id = await getApId(activity.object);
|
||||
|
||||
if (!isSelfHost(extractDbHost(id))) {
|
||||
if (!isSelfHost(extractPunyHost(id))) {
|
||||
return `skip: Read to foreign host (${id})`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js';
|
||||
import { getApId, getOneApId, getApType, IUpdate, isActor, isPost } from '@/remote/activitypub/type.js';
|
||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||
import { updateQuestion } from '@/remote/activitypub/models/question.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||
import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js';
|
||||
|
||||
/**
|
||||
* Updateアクティビティを捌きます
|
||||
|
@ -30,6 +31,8 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver)
|
|||
} else if (getApType(object) === 'Question') {
|
||||
await updateQuestion(object, resolver).catch(e => console.log(e));
|
||||
return 'ok: Question updated';
|
||||
} else if (isPost(object)) {
|
||||
return await updateNote(actor, object, resolver);
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import createNote from '@/remote/activitypub/kernel/create/note.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { updateNote } from '@/remote/activitypub/models/note.js';
|
||||
|
||||
export async function update(actor: IRemoteUser, note: IObject, resolver: Resolver): Promise<string> {
|
||||
// check whether note exists
|
||||
const uri = getApId(note);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
|
||||
if (exists == null) {
|
||||
// does not yet exist, handle as if this was a create activity
|
||||
// and since this is not a direct creation, handle it silently
|
||||
createNote(resolver, actor, note, true);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
// if creating was successful...
|
||||
const existsNow = await Notes.findOneByOrFail({ uri });
|
||||
// set the updatedAt timestamp since the note was changed
|
||||
await Notes.update(existsNow.id, { updatedAt: new Date() });
|
||||
return 'ok: unknown note created and marked as updated';
|
||||
} catch (e) {
|
||||
return `skip: updated note unknown and creating rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
} else {
|
||||
// check that actor is authorized to update this note
|
||||
if (actor.id !== exists.userId) {
|
||||
return 'skip: actor not authorized to update Note';
|
||||
}
|
||||
// this does not redo the checks from the Create Note kernel
|
||||
// since if the note made it into the database, we assume
|
||||
// those checks must have been passed before.
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
await updateNote(note, actor, resolver);
|
||||
return 'ok: note updated';
|
||||
} catch (e) {
|
||||
return `skip: update note rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { IRemoteUser } from '@/models/entities/user.js';
|
|||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
|
||||
export type AuthUser = {
|
||||
user: IRemoteUser;
|
||||
|
@ -29,8 +30,8 @@ function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await publicKeyCache.fetch(keyId)
|
||||
export async function authUserFromKeyId(keyId: string): Promise<AuthUser | null> {
|
||||
return await publicKeyCache.fetch(keyId)
|
||||
.then(async key => {
|
||||
if (!key) return null;
|
||||
else return {
|
||||
|
@ -38,6 +39,10 @@ export async function getAuthUser(keyId: string, actorUri: string, resolver: Res
|
|||
key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await authUserFromKeyId(keyId);
|
||||
if (authUser != null) return authUser;
|
||||
|
||||
authUser = await authUserFromApId(actorUri);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import config from '@/config/index.js';
|
||||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
||||
import { vote } from '@/services/note/polls/vote.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages } from '@/models/index.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages, Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
|
@ -27,6 +27,8 @@ import { resolveImage } from './image.js';
|
|||
import { extractApHashtags, extractQuoteUrl, extractEmojis } from './tag.js';
|
||||
import { extractPollFromQuestion } from './question.js';
|
||||
import { extractApMentions } from './mention.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { sideEffects } from '@/services/note/side-effects.js';
|
||||
|
||||
export function validateNote(object: IObject): Error | null {
|
||||
if (object == null) {
|
||||
|
@ -45,9 +47,9 @@ export function validateNote(object: IObject): Error | null {
|
|||
}
|
||||
|
||||
// Check that the server is authorized to act on behalf of this author.
|
||||
const expectHost = extractDbHost(id);
|
||||
const expectHost = extractPunyHost(id);
|
||||
const attributedToHost = object.attributedTo
|
||||
? extractDbHost(getOneApId(object.attributedTo))
|
||||
? extractPunyHost(getOneApId(object.attributedTo))
|
||||
: null;
|
||||
if (attributedToHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
|
||||
|
@ -62,7 +64,7 @@ export function validateNote(object: IObject): Error | null {
|
|||
/**
|
||||
* Function to process the content of a note, reusable in createNote and updateNote.
|
||||
*/
|
||||
async function processContent(note: IPost, quoteUri: string | null):
|
||||
async function processContent(actor: IRemoteUser, note: IPost, quoteUri: string | null, resolver: Resolver):
|
||||
Promise<{
|
||||
cw: string | null,
|
||||
files: DriveFile[],
|
||||
|
@ -78,10 +80,10 @@ async function processContent(note: IPost, quoteUri: string | null):
|
|||
const limit = promiseLimit(2);
|
||||
|
||||
const attachments = toArray(note.attachment);
|
||||
const files = note.attachment
|
||||
const files = attachments
|
||||
// If the note is marked as sensitive, the images should be marked sensitive too.
|
||||
.map(attach => attach.sensitive |= note.sensitive)
|
||||
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise<DriveFile>)))
|
||||
? (await Promise.all(attachments.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise<DriveFile>)))
|
||||
.filter(image => image != null)
|
||||
: [];
|
||||
|
||||
|
@ -93,7 +95,7 @@ async function processContent(note: IPost, quoteUri: string | null):
|
|||
text = fromHtml(note.content, quoteUri);
|
||||
}
|
||||
|
||||
const emojis = await extractEmojis(note.tag || [], extractDbHost(getApId(note))).catch(e => {
|
||||
const emojis = await extractEmojis(note.tag || [], extractPunyHost(getApId(note))).catch(e => {
|
||||
apLogger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
|
@ -258,7 +260,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
|
||||
const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
|
||||
const processedContent = await processContent(note, quote?.uri);
|
||||
const processedContent = await processContent(actor, note, quote?.uri, resolver);
|
||||
|
||||
if (isTalk) {
|
||||
for (const recipient of visibleUsers) {
|
||||
|
@ -299,7 +301,7 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
if (uri == null) throw new Error('missing uri');
|
||||
|
||||
// Interrupt if blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) throw new StatusError('host blocked', 451, `host ${extractDbHost(uri)} is blocked`);
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) throw new StatusError('host blocked', 451, `host ${extractPunyHost(uri)} is blocked`);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
@ -324,3 +326,45 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a note.
|
||||
*
|
||||
* If the target Note is not registered, it will be ignored.
|
||||
*/
|
||||
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
|
||||
const err = validateNote(value);
|
||||
if (err) {
|
||||
apLogger.error(`${err.message}`);
|
||||
throw new Error('invalid updated note');
|
||||
}
|
||||
|
||||
const uri = getApId(value);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
if (exists == null) return null;
|
||||
|
||||
let quoteUri = null;
|
||||
if (exists.renoteId && !foundkey.entities.isPureRenote(exists)) {
|
||||
const quote = await Notes.findOneBy({ id: exists.renoteId });
|
||||
quoteUri = quote.uri;
|
||||
}
|
||||
|
||||
// process content and update attached files (e.g. also image descriptions)
|
||||
const processedContent = await processContent(actor, value, quoteUri, resolver);
|
||||
|
||||
// update note content itself
|
||||
await Notes.update(exists.id, {
|
||||
updatedAt: new Date(),
|
||||
|
||||
cw: processedContent.cw,
|
||||
fileIds: processedContent.files.map(file => file.id),
|
||||
attachedFileTypes: processedContent.files.map(file => file.type),
|
||||
text: processedContent.text,
|
||||
emojis: processedContent.apEmoji,
|
||||
tags: processedContent.apHashtags.map(tag => normalizeForSearch(tag)),
|
||||
url: processedContent.url,
|
||||
name: processedContent.name,
|
||||
});
|
||||
|
||||
await sideEffects(actor, await Notes.findOneByOrFail({ id: exists.id }), false, false);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { genId } from '@/misc/gen-id.js';
|
|||
import { instanceChart, usersChart } from '@/services/chart/index.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
|
@ -57,7 +57,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This check is security critical.
|
||||
// Without this check, an entry could be inserted into UserPublickey for a local user.
|
||||
if (extractDbHost(uri) === extractDbHost(config.url)) {
|
||||
if (extractPunyHost(uri) === extractPunyHost(config.url)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This is a security critical check to not insert or change an entry of
|
||||
// UserPublickey to point to a local key id.
|
||||
if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) {
|
||||
if (extractPunyHost(uri) !== extractPunyHost(x.publicKey.id)) {
|
||||
throw new Error('invalid Actor: publicKey.id has different host');
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
|||
|
||||
apLogger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
const host = extractDbHost(object.id);
|
||||
const host = extractPunyHost(object.id);
|
||||
|
||||
const { fields } = analyzeAttachments(person.attachment || []);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { toPuny } from '@/misc/convert-host.js';
|
|||
import { Emojis } from '@/models/index.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
|
||||
export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] {
|
||||
if (tags == null) return [];
|
||||
|
|
|
@ -17,27 +17,56 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
manuallyApprovesFollowers: {
|
||||
'@id': 'as:manuallyApprovesFollowers',
|
||||
'@type': 'xsd:boolean',
|
||||
},
|
||||
sensitive: {
|
||||
'@id': 'as:sensitive',
|
||||
'@type': 'xsd:boolean',
|
||||
},
|
||||
Hashtag: 'as:Hashtag',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
featured: {
|
||||
'@id': 'toot:featured',
|
||||
'@type': '@id',
|
||||
},
|
||||
discoverable: {
|
||||
'@id': 'toot:discoverable',
|
||||
'@type': 'xsd:boolean',
|
||||
},
|
||||
// Fedibird
|
||||
fedibird: 'http://fedibird.com/ns#',
|
||||
quoteUri: 'fedibird:quoteUri',
|
||||
quoteUri: {
|
||||
'@id': 'http://fedibird.com/ns#quoteUri',
|
||||
'@type': '@id',
|
||||
},
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
schema: 'http://schema.org/',
|
||||
PropertyValue: {
|
||||
'@id': 'schema:PropertyValue',
|
||||
'@context': {
|
||||
'value': 'schema:value',
|
||||
'name': 'schema:name',
|
||||
},
|
||||
},
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_talk': 'misskey:_misskey_talk',
|
||||
'isCat': 'misskey:isCat',
|
||||
'_misskey_quote': {
|
||||
'@id': 'misskey:_misskey_quote',
|
||||
'@type': '@id',
|
||||
},
|
||||
'_misskey_talk': {
|
||||
'@id': 'misskey:_misskey_talk',
|
||||
'@type': 'xsd:boolean',
|
||||
},
|
||||
'isCat': {
|
||||
'@id': 'misskey:isCat',
|
||||
'@type': 'xsd:boolean',
|
||||
},
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost, isSelfHost } from '@/misc/convert-host.js';
|
||||
import { Notes, NoteReactions, Polls, Users } from '@/models/index.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
|
@ -50,7 +50,7 @@ export class Resolver {
|
|||
|
||||
if (typeof value !== 'string') {
|
||||
if (typeof value.id !== 'undefined') {
|
||||
const host = extractDbHost(getApId(value));
|
||||
const host = extractPunyHost(getApId(value));
|
||||
if (await shouldBlockInstance(host)) {
|
||||
throw new Error('instance is blocked');
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export class Resolver {
|
|||
}
|
||||
this.history.add(value);
|
||||
|
||||
const host = extractDbHost(value);
|
||||
const host = extractPunyHost(value);
|
||||
if (isSelfHost(host)) {
|
||||
return await this.resolveLocal(value);
|
||||
}
|
||||
|
|
95
packages/backend/src/remote/http-signature.ts
Normal file
95
packages/backend/src/remote/http-signature.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { URL } from 'node:url';
|
||||
import { extractPunyHost } from "@/misc/convert-host.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import httpSignature from "@peertube/http-signature";
|
||||
import { Resolver } from "./activitypub/resolver.js";
|
||||
import { StatusError } from "@/misc/fetch.js";
|
||||
import { AuthUser, authUserFromKeyId, getAuthUser } from "./activitypub/misc/auth-user.js";
|
||||
import { ApObject, getApId, isActor } from "./activitypub/type.js";
|
||||
import { createPerson } from "./activitypub/models/person.js";
|
||||
|
||||
async function resolveKeyId(keyId: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
// Do we already know that keyId?
|
||||
const authUser = await authUserFromKeyId(keyId);
|
||||
if (authUser != null) return authUser;
|
||||
|
||||
// If not, discover it.
|
||||
const keyUrl = new URL(keyId);
|
||||
keyUrl.hash = ''; // Fragment should not be part of the request.
|
||||
|
||||
const keyObject = await resolver.resolve(keyUrl.toString());
|
||||
|
||||
// Does the keyId end up resolving to an Actor?
|
||||
if (isActor(keyObject)) {
|
||||
await createPerson(keyObject, resolver);
|
||||
return await getAuthUser(keyId, getApId(keyObject), resolver);
|
||||
}
|
||||
|
||||
// Does the keyId end up resolving to a Key-like?
|
||||
const keyData = keyObject as any;
|
||||
if (keyData.owner != null && keyData.publicKeyPem != null) {
|
||||
await createPerson(keyData.owner, resolver);
|
||||
return await getAuthUser(keyId, getApId(keyData.owner), resolver);
|
||||
}
|
||||
|
||||
// Cannot be resolved.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type SignatureValidationResult = {
|
||||
status: 'missing' | 'invalid' | 'rejected';
|
||||
authUser: AuthUser | null;
|
||||
} | {
|
||||
status: 'valid';
|
||||
authUser: AuthUser;
|
||||
};
|
||||
|
||||
export async function verifyHttpSignature(signature: httpSignature.IParsedSignature, resolver: Resolver, actor?: ApObject): Promise<SignatureValidationResult> {
|
||||
// This old `keyId` format is no longer supported.
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) return { status: 'invalid', authUser: null };
|
||||
|
||||
const host = extractPunyHost(keyIdLower);
|
||||
|
||||
// Reject if the host is blocked.
|
||||
if (await shouldBlockInstance(host)) return { status: 'rejected', authUser: null };
|
||||
|
||||
let authUser = null;
|
||||
try {
|
||||
if (actor != null) {
|
||||
authUser = await getAuthUser(signature.keyId, getApId(actor), resolver);
|
||||
} else {
|
||||
authUser = await resolveKeyId(signature.keyId, resolver);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof StatusError) {
|
||||
if (e.isClientError) {
|
||||
// Actor is deleted.
|
||||
return { status: 'rejected', authUser };
|
||||
} else {
|
||||
throw new Error(`Error in signature ${signature} - ${e.statusCode || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
// Key not found? Unacceptable!
|
||||
return { status: 'invalid', authUser };
|
||||
} else {
|
||||
// Found key!
|
||||
}
|
||||
|
||||
// Make sure the resolved user matches the keyId host.
|
||||
if (authUser.user.host !== host) return { status: 'rejected', authUser };
|
||||
|
||||
// Verify the HTTP Signature
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
if (httpSignatureValidated === true)
|
||||
return {
|
||||
status: 'valid',
|
||||
authUser,
|
||||
};
|
||||
|
||||
// Otherwise, fail.
|
||||
return { status: 'invalid', authUser };
|
||||
}
|
|
@ -20,6 +20,11 @@ import Outbox from './activitypub/outbox.js';
|
|||
import Followers from './activitypub/followers.js';
|
||||
import Following from './activitypub/following.js';
|
||||
import Featured from './activitypub/featured.js';
|
||||
import { isInstanceActor } from '@/services/instance-actor.js';
|
||||
import { getUser } from './api/common/getters.js';
|
||||
import config from '@/config/index.js';
|
||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
@ -59,6 +64,36 @@ export function setResponseType(ctx: Router.RouterContext): void {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSignature(ctx: Router.RouterContext): Promise<boolean> {
|
||||
if (config.allowUnsignedFetches) {
|
||||
// Fetch signature verification is disabled.
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
return true;
|
||||
} else {
|
||||
let verified;
|
||||
try {
|
||||
let signature = httpSignature.parseRequest(ctx.req);
|
||||
verified = await verifyHttpSignature(signature, new Resolver());
|
||||
} catch (e) {
|
||||
verified = { status: 'missing' };
|
||||
}
|
||||
|
||||
switch (verified.status) {
|
||||
// Fetch signature verification succeeded.
|
||||
case 'valid':
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
return true;
|
||||
case 'missing':
|
||||
case 'invalid':
|
||||
case 'rejected':
|
||||
default:
|
||||
ctx.status = 403;
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inbox
|
||||
router.post('/inbox', json(), inbox);
|
||||
router.post('/users/:user/inbox', json(), inbox);
|
||||
|
@ -66,6 +101,7 @@ router.post('/users/:user/inbox', json(), inbox);
|
|||
// note
|
||||
router.get('/notes/:note', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
|
@ -89,7 +125,6 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNote(note, false));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
@ -103,6 +138,7 @@ router.get('/notes/:note/activity', async ctx => {
|
|||
ctx.redirect(`/notes/${ctx.params.note}`);
|
||||
return;
|
||||
}
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
|
@ -117,23 +153,32 @@ router.get('/notes/:note/activity', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNoteOrRenoteActivity(note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
async function requireHttpSignature(ctx: Router.Context, next: () => Promise<void>) {
|
||||
if (!(await handleSignature(ctx))) {
|
||||
return;
|
||||
} else {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
// outbox
|
||||
router.get('/users/:user/outbox', Outbox);
|
||||
router.get('/users/:user/outbox', requireHttpSignature, Outbox);
|
||||
|
||||
// followers
|
||||
router.get('/users/:user/followers', Followers);
|
||||
router.get('/users/:user/followers', requireHttpSignature, Followers);
|
||||
|
||||
// following
|
||||
router.get('/users/:user/following', Following);
|
||||
router.get('/users/:user/following', requireHttpSignature, Following);
|
||||
|
||||
// featured
|
||||
router.get('/users/:user/collections/featured', Featured);
|
||||
router.get('/users/:user/collections/featured', requireHttpSignature, Featured);
|
||||
|
||||
// publickey
|
||||
// This does not require HTTP signatures in order for other instances
|
||||
// to be able to verify our own signatures.
|
||||
router.get('/users/:user/publickey', async ctx => {
|
||||
const userId = ctx.params.user;
|
||||
|
||||
|
@ -166,7 +211,6 @@ async function userInfo(ctx: Router.RouterContext, user: User | null): Promise<v
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
|
||||
|
@ -181,11 +225,26 @@ router.get('/users/:user', async (ctx, next) => {
|
|||
isSuspended: false,
|
||||
});
|
||||
|
||||
// Allow fetching the instance actor without any HTTP signature.
|
||||
// Only on this route, as it is the canonical route.
|
||||
// If the user could not be resolved, or is not the instance actor,
|
||||
// validate and enforce signatures.
|
||||
if (user == null || !isInstanceActor(user))
|
||||
{
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
}
|
||||
else if (isInstanceActor(user))
|
||||
{
|
||||
// Set cache at all times for instance actors.
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
|
||||
await userInfo(ctx, user);
|
||||
});
|
||||
|
||||
router.get('/@:user', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: ctx.params.user.toLowerCase(),
|
||||
|
@ -198,6 +257,9 @@ router.get('/@:user', async (ctx, next) => {
|
|||
|
||||
// emoji
|
||||
router.get('/emojis/:emoji', async ctx => {
|
||||
// Enforcing HTTP signatures on Emoji objects could cause problems for
|
||||
// other software that might use those objects for copying custom emoji.
|
||||
|
||||
const emoji = await Emojis.findOneBy({
|
||||
host: IsNull(),
|
||||
name: ctx.params.emoji,
|
||||
|
@ -215,6 +277,7 @@ router.get('/emojis/:emoji', async ctx => {
|
|||
|
||||
// like
|
||||
router.get('/likes/:like', async ctx => {
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
const reaction = await NoteReactions.findOneBy({ id: ctx.params.like });
|
||||
|
||||
if (reaction == null) {
|
||||
|
@ -233,12 +296,12 @@ router.get('/likes/:like', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderLike(reaction, note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// follow
|
||||
router.get('/follows/:follower/:followee', async ctx => {
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists.
|
||||
|
||||
|
@ -259,7 +322,6 @@ router.get('/follows/:follower/:followee', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
|
|
@ -36,6 +36,5 @@ export default async (ctx: Router.RouterContext) => {
|
|||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
};
|
||||
|
|
|
@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -90,7 +90,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { createPerson } from '@/remote/activitypub/models/person.js';
|
|||
import { createNote } from '@/remote/activitypub/models/note.js';
|
||||
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { Users, Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||
|
@ -87,7 +87,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
*/
|
||||
async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
// Stop if the host is blocked.
|
||||
const host = extractDbHost(uri);
|
||||
const host = extractPunyHost(uri);
|
||||
if (await shouldBlockInstance(host)) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { WebSocket } from 'ws';
|
|||
import { readNote } from '@/services/note/read.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Channel as ChannelModel } from '@/models/entities/channel.js';
|
||||
import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
||||
import { Followings, Mutings, Notes, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js';
|
||||
|
@ -14,6 +14,7 @@ import { channels } from './channels/index.js';
|
|||
import Channel from './channel.js';
|
||||
import { StreamEventEmitter, StreamMessages } from './types.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
const logger = new Logger('streaming');
|
||||
|
||||
|
@ -254,11 +255,43 @@ export class Connection {
|
|||
}
|
||||
|
||||
private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
|
||||
this.sendMessageToWs('noteUpdated', {
|
||||
id: data.body.id,
|
||||
type: data.type,
|
||||
body: data.body.body,
|
||||
});
|
||||
if (data.type === 'updated') {
|
||||
const note = data.body.body.note;
|
||||
// FIXME analogous to Channel.withPackedNote, but for some reason, the note
|
||||
// stream is not handled as a channel but instead handled at the top level
|
||||
// so this code is duplicated here I guess.
|
||||
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);
|
||||
note.reply = null;
|
||||
note.renote = null;
|
||||
note.user = null;
|
||||
note.channel = null;
|
||||
|
||||
const packed = await Notes.pack(note, this.user, { detail: true });
|
||||
|
||||
this.sendMessageToWs('noteUpdated', {
|
||||
id: data.body.id,
|
||||
type: 'updated',
|
||||
body: { note: packed },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
|
||||
// skip: note not visible to user
|
||||
return;
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sendMessageToWs('noteUpdated', {
|
||||
id: data.body.id,
|
||||
type: data.type,
|
||||
body: data.body.body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -128,6 +128,9 @@ export interface NoteStreamTypes {
|
|||
reaction: string;
|
||||
userId: User['id'];
|
||||
};
|
||||
updated: {
|
||||
note: Note;
|
||||
};
|
||||
}
|
||||
type NoteStreamEventTypes = {
|
||||
[key in keyof NoteStreamTypes]: {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Router from '@koa/router';
|
|||
import { IsNull, MoreThan } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, Notes } from '@/models/index.js';
|
||||
import { Users, Notes, Webhooks } from '@/models/index.js';
|
||||
import { MONTH, DAY } from '@/const.js';
|
||||
|
||||
const router = new Router();
|
||||
|
@ -52,12 +52,14 @@ const nodeinfo2 = async (): Promise<NodeInfo2Base> => {
|
|||
activeHalfyear,
|
||||
activeMonth,
|
||||
localPosts,
|
||||
activeWebhooks,
|
||||
] = await Promise.all([
|
||||
fetchMeta(true),
|
||||
Users.count({ where: { host: IsNull() } }),
|
||||
Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 180 * DAY)) } }),
|
||||
Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - MONTH)) } }),
|
||||
Notes.count({ where: { userHost: IsNull() } }),
|
||||
Webhooks.countBy({ active: true }),
|
||||
]);
|
||||
|
||||
const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null;
|
||||
|
@ -100,6 +102,7 @@ const nodeinfo2 = async (): Promise<NodeInfo2Base> => {
|
|||
enableEmail: meta.enableEmail,
|
||||
proxyAccountName: proxyAccount?.username ?? null,
|
||||
themeColor: meta.themeColor || '#86b300',
|
||||
activeWebhooks,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
14
packages/backend/src/services/drive/read-file.ts
Normal file
14
packages/backend/src/services/drive/read-file.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { InternalStorage } from './internal-storage.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
|
||||
export async function copyFileTo(file: DriveFile, toPath: string): Promise<void> {
|
||||
if (file.storedInternal) {
|
||||
const fromPath = InternalStorage.resolvePath(file.accessKey);
|
||||
fs.copyFileSync(fromPath, toPath);
|
||||
} else {
|
||||
await downloadUrl(file.url, toPath);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { getSystemUser } from './system-user.js';
|
||||
|
||||
|
@ -17,3 +17,7 @@ export async function getInstanceActor(): Promise<ILocalUser> {
|
|||
|
||||
return instanceActor;
|
||||
}
|
||||
|
||||
export function isInstanceActor(user: User): boolean {
|
||||
return user.username === ACTOR_USERNAME && user.host === null;
|
||||
}
|
||||
|
|
|
@ -31,13 +31,13 @@ export default class Logger {
|
|||
* @param color Log message color
|
||||
* @param store Whether to store messages
|
||||
*/
|
||||
constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info) {
|
||||
constructor(domain: string, color?: KEYWORD, store = true, minLevel?: Level) {
|
||||
this.domain = {
|
||||
name: domain,
|
||||
color,
|
||||
};
|
||||
this.store = store;
|
||||
this.minLevel = minLevel;
|
||||
this.minLevel = minLevel ?? envOption.logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,7 +47,7 @@ export default class Logger {
|
|||
* @param store Whether to store messages
|
||||
* @returns A Logger instance whose parent logger is this instance.
|
||||
*/
|
||||
public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info): Logger {
|
||||
public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel?: Level): Logger {
|
||||
const logger = new Logger(domain, color, store, minLevel);
|
||||
logger.parentLogger = this;
|
||||
return logger;
|
||||
|
|
|
@ -1,109 +1,21 @@
|
|||
import { ArrayOverlap, Not, In } from 'typeorm';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { publishMainStream, publishNotesStream } from '@/services/stream.js';
|
||||
import { DeliverManager } from '@/remote/activitypub/deliver-manager.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import renderCreate from '@/remote/activitypub/renderer/create.js';
|
||||
import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import { resolveUser } from '@/remote/resolve-user.js';
|
||||
import { concat } from '@/prelude/array.js';
|
||||
import { insertNoteUnread } from '@/services/note/unread.js';
|
||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Mutings, Users, NoteWatchings, Notes, Instances, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js';
|
||||
import { Users, Channels } from '@/models/index.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { App } from '@/models/entities/app.js';
|
||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index.js';
|
||||
import { Poll, IPoll } from '@/models/entities/poll.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { checkHitAntenna } from '@/misc/check-hit-antenna.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { endedPollNotificationQueue } from '@/queue/queues.js';
|
||||
import { webhookDeliver } from '@/queue/index.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js';
|
||||
import { updateHashtags } from '../update-hashtag.js';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||
import { createNotification } from '../create-notification.js';
|
||||
import { addNoteToAntenna } from '../add-note-to-antenna.js';
|
||||
import { deliverToRelays } from '../relay.js';
|
||||
import { mutedWordsCache, index } from './index.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
class NotificationManager {
|
||||
private notifier: { id: User['id']; };
|
||||
private note: Note;
|
||||
private queue: {
|
||||
target: ILocalUser['id'];
|
||||
reason: NotificationType;
|
||||
}[];
|
||||
|
||||
constructor(notifier: { id: User['id']; }, note: Note) {
|
||||
this.notifier = notifier;
|
||||
this.note = note;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
public push(notifiee: ILocalUser['id'], reason: NotificationType): void {
|
||||
// No notification to yourself.
|
||||
if (this.notifier.id === notifiee) return;
|
||||
|
||||
const exist = this.queue.find(x => x.target === notifiee);
|
||||
|
||||
if (exist) {
|
||||
// If you have been "mentioned and replied to," make the notification as a reply, not as a mention.
|
||||
if (reason !== 'mention') {
|
||||
exist.reason = reason;
|
||||
}
|
||||
} else {
|
||||
this.queue.push({
|
||||
reason,
|
||||
target: notifiee,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async deliver(): Promise<void> {
|
||||
for (const x of this.queue) {
|
||||
// check if the sender or thread are muted
|
||||
const userMuted = await Mutings.countBy({
|
||||
muterId: x.target,
|
||||
muteeId: this.notifier.id,
|
||||
});
|
||||
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: x.target,
|
||||
threadId: In([
|
||||
// replies
|
||||
this.note.threadId ?? this.note.id,
|
||||
// renotes
|
||||
this.note.renoteId ?? undefined,
|
||||
]),
|
||||
mutingNotificationTypes: ArrayOverlap([x.reason]),
|
||||
});
|
||||
|
||||
if (!userMuted && !threadMuted) {
|
||||
createNotification(x.target, x.reason, {
|
||||
notifierId: this.notifier.id,
|
||||
noteId: this.note.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { sideEffects } from './side-effects.js';
|
||||
|
||||
type MinimumUser = {
|
||||
id: User['id'];
|
||||
|
@ -238,253 +150,9 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
|||
|
||||
res(note);
|
||||
|
||||
// Update Statistics
|
||||
notesChart.update(note, true);
|
||||
perUserNotesChart.update(user, note, true);
|
||||
|
||||
// Register host
|
||||
if (Users.isRemoteUser(user)) {
|
||||
registerOrFetchInstanceDoc(user.host).then(i => {
|
||||
Instances.increment({ id: i.id }, 'notesCount', 1);
|
||||
instanceChart.updateNote(i.host, note, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Hashtag Update
|
||||
if (data.visibility === 'public' || data.visibility === 'home') {
|
||||
updateHashtags(user, tags);
|
||||
}
|
||||
|
||||
// Increment notes count (user)
|
||||
incNotesCountOfUser(user);
|
||||
|
||||
// Word mute
|
||||
mutedWordsCache.fetch('').then(us => {
|
||||
for (const u of us) {
|
||||
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
|
||||
if (shouldMute) {
|
||||
MutedNotes.insert({
|
||||
id: genId(),
|
||||
userId: u.userId,
|
||||
noteId: note.id,
|
||||
reason: 'word',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Antenna
|
||||
for (const antenna of (await getAntennas())) {
|
||||
checkHitAntenna(antenna, note, user).then(hit => {
|
||||
if (hit) {
|
||||
addNoteToAntenna(antenna, note, user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Channel
|
||||
if (note.channelId) {
|
||||
ChannelFollowings.findBy({ followeeId: note.channelId }).then(followings => {
|
||||
for (const following of followings) {
|
||||
insertNoteUnread(following.followerId, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data.reply) {
|
||||
saveReply(data.reply, note);
|
||||
}
|
||||
|
||||
// When there is no re-note of the specified note by the specified user except for this post
|
||||
if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
|
||||
incRenoteCount(data.renote);
|
||||
}
|
||||
|
||||
if (data.poll && data.poll.expiresAt) {
|
||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||
endedPollNotificationQueue.add({
|
||||
noteId: note.id,
|
||||
}, {
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
if (Users.isLocalUser(user)) activeUsersChart.write(user);
|
||||
|
||||
// Create unread notifications
|
||||
if (data.visibility === 'specified') {
|
||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
// Local users only
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: true,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const u of mentionedUsers) {
|
||||
// Local users only
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
publishNotesStream(note);
|
||||
|
||||
const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note')));
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'note', {
|
||||
note: await Notes.pack(note, user),
|
||||
});
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
await createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
// If has in reply to note
|
||||
if (data.reply) {
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
|
||||
|
||||
// 通知
|
||||
if (data.reply.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId || data.reply.id,
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
|
||||
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: packedReply,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it is renote
|
||||
if (data.renote) {
|
||||
const type = data.text ? 'quote' : 'renote';
|
||||
|
||||
// Notify
|
||||
if (data.renote.userHost === null) {
|
||||
nm.push(data.renote.userId, type);
|
||||
}
|
||||
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
|
||||
|
||||
// Publish event
|
||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||
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: packedRenote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(nmRelatedPromises).then(() => {
|
||||
nm.deliver();
|
||||
});
|
||||
|
||||
//#region AP deliver
|
||||
if (Users.isLocalUser(user) && !data.localOnly) {
|
||||
(async () => {
|
||||
const noteActivity = renderActivity(await renderNoteOrRenoteActivity(note));
|
||||
const dm = new DeliverManager(user, noteActivity);
|
||||
|
||||
// Delivered to remote users who have been mentioned
|
||||
for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
}
|
||||
|
||||
// If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver
|
||||
if (data.reply && data.reply.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: data.reply.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver
|
||||
if (data.renote && data.renote.userHost !== null) {
|
||||
const u = await Users.findOneBy({ id: data.renote.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// Deliver to followers
|
||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
if (['public'].includes(note.visibility)) {
|
||||
deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
if (data.channel) {
|
||||
Channels.increment({ id: data.channel.id }, 'notesCount', 1);
|
||||
Channels.update(data.channel.id, {
|
||||
lastNotedAt: new Date(),
|
||||
});
|
||||
|
||||
const count = await Notes.countBy({
|
||||
userId: user.id,
|
||||
channelId: data.channel.id,
|
||||
});
|
||||
|
||||
// This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission.
|
||||
// TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that.
|
||||
if (count === 1) {
|
||||
Channels.increment({ id: data.channel.id }, 'usersCount', 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Register to search database
|
||||
index(note);
|
||||
sideEffects(user, note, silent, true);
|
||||
});
|
||||
|
||||
function incRenoteCount(renote: Note): void {
|
||||
Notes.createQueryBuilder().update()
|
||||
.set({
|
||||
renoteCount: () => '"renoteCount" + 1',
|
||||
score: () => '"score" + 1',
|
||||
})
|
||||
.where('id = :id', { id: renote.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]): Promise<Note> {
|
||||
const createdAt = data.createdAt ?? new Date();
|
||||
|
||||
|
@ -563,77 +231,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
|
|||
}
|
||||
}
|
||||
|
||||
async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise<void> {
|
||||
const watchers = await NoteWatchings.findBy({
|
||||
noteId: renote.id,
|
||||
userId: Not(user.id),
|
||||
});
|
||||
|
||||
for (const watcher of watchers) {
|
||||
nm.push(watcher.userId, type);
|
||||
}
|
||||
}
|
||||
|
||||
async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager): Promise<void> {
|
||||
const watchers = await NoteWatchings.findBy({
|
||||
noteId: reply.id,
|
||||
userId: Not(user.id),
|
||||
});
|
||||
|
||||
for (const watcher of watchers) {
|
||||
nm.push(watcher.userId, 'reply');
|
||||
}
|
||||
}
|
||||
|
||||
async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager): Promise<void> {
|
||||
for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: u.id,
|
||||
threadId: note.threadId || note.id,
|
||||
});
|
||||
|
||||
if (threadMuted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
nm.push(u.id, 'mention');
|
||||
}
|
||||
}
|
||||
|
||||
function saveReply(reply: Note): void {
|
||||
Notes.increment({ id: reply.id }, 'repliesCount', 1);
|
||||
}
|
||||
|
||||
function incNotesCountOfUser(user: { id: User['id']; }): void {
|
||||
Users.createQueryBuilder().update()
|
||||
.set({
|
||||
updatedAt: new Date(),
|
||||
notesCount: () => '"notesCount" + 1',
|
||||
})
|
||||
.where('id = :id', { id: user.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> {
|
||||
if (tokens.length === 0) return [];
|
||||
|
||||
|
|
474
packages/backend/src/services/note/side-effects.ts
Normal file
474
packages/backend/src/services/note/side-effects.ts
Normal file
|
@ -0,0 +1,474 @@
|
|||
import { ArrayOverlap, Not, In } from 'typeorm';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { publishMainStream, publishNoteStream, publishNotesStream } from '@/services/stream.js';
|
||||
import { DeliverManager } from '@/remote/activitypub/deliver-manager.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import { resolveUser } from '@/remote/resolve-user.js';
|
||||
import { insertNoteUnread } from '@/services/note/unread.js';
|
||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { AntennaNotes, Mutings, Users, NoteWatchings, Notes, Instances, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js';
|
||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index.js';
|
||||
import { checkHitAntenna } from '@/misc/check-hit-antenna.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { endedPollNotificationQueue } from '@/queue/queues.js';
|
||||
import { webhookDeliver } from '@/queue/index.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js';
|
||||
import { updateHashtags } from '../update-hashtag.js';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||
import { createNotification } from '../create-notification.js';
|
||||
import { addNoteToAntenna } from '../add-note-to-antenna.js';
|
||||
import { deliverToRelays } from '../relay.js';
|
||||
import { mutedWordsCache, index } from './index.js';
|
||||
import { Polls } from '@/models/index.js';
|
||||
import { Poll } from '@/models/entities/poll.js';
|
||||
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'update';
|
||||
|
||||
class NotificationManager {
|
||||
private notifier: { id: User['id']; };
|
||||
private note: Note;
|
||||
private queue: {
|
||||
target: ILocalUser['id'];
|
||||
reason: NotificationType;
|
||||
}[];
|
||||
|
||||
constructor(notifier: { id: User['id']; }, note: Note) {
|
||||
this.notifier = notifier;
|
||||
this.note = note;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
public push(notifiee: ILocalUser['id'], reason: NotificationType): void {
|
||||
// No notification to yourself.
|
||||
if (this.notifier.id === notifiee) return;
|
||||
|
||||
const exist = this.queue.find(x => x.target === notifiee);
|
||||
|
||||
if (exist) {
|
||||
// If you have been "mentioned and replied to," make the notification as a reply, not as a mention.
|
||||
if (reason !== 'mention') {
|
||||
exist.reason = reason;
|
||||
}
|
||||
} else {
|
||||
this.queue.push({
|
||||
reason,
|
||||
target: notifiee,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async deliver(): Promise<void> {
|
||||
for (const x of this.queue) {
|
||||
// check if the sender or thread are muted
|
||||
const userMuted = await Mutings.countBy({
|
||||
muterId: x.target,
|
||||
muteeId: this.notifier.id,
|
||||
});
|
||||
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: x.target,
|
||||
threadId: In([
|
||||
// replies
|
||||
this.note.threadId ?? this.note.id,
|
||||
// renotes
|
||||
this.note.renoteId ?? undefined,
|
||||
]),
|
||||
mutingNotificationTypes: ArrayOverlap([x.reason]),
|
||||
});
|
||||
|
||||
if (!userMuted && !threadMuted) {
|
||||
createNotification(x.target, x.reason, {
|
||||
notifierId: this.notifier.id,
|
||||
noteId: this.note.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform side effects for a Note such as incrementing statistics, updating hashtag usage etc.
|
||||
*
|
||||
* @param user The author of the note.
|
||||
* @param note The note for which the side effects should be performed.
|
||||
* @param silent Whether notifications and similar side effects should be suppressed.
|
||||
* @param created Whether statistics should be incremented (i.e. the note was inserted and not updated in the database)
|
||||
*/
|
||||
export async function sideEffects(user: User, note: Note, silent = false, created = true): Promise<void> {
|
||||
if (created) {
|
||||
// Update Statistics
|
||||
notesChart.update(note, true);
|
||||
perUserNotesChart.update(user, note, true);
|
||||
Users.createQueryBuilder().update()
|
||||
.set({
|
||||
updatedAt: new Date(),
|
||||
notesCount: () => '"notesCount" + 1',
|
||||
})
|
||||
.where('id = :id', { id: user.id })
|
||||
.execute();
|
||||
|
||||
if (Users.isLocalUser(user)) {
|
||||
activeUsersChart.write(user);
|
||||
} else {
|
||||
// Remote user, register host
|
||||
registerOrFetchInstanceDoc(user.host).then(i => {
|
||||
Instances.increment({ id: i.id }, 'notesCount', 1);
|
||||
instanceChart.updateNote(i.host, note, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Channel
|
||||
if (note.channelId) {
|
||||
ChannelFollowings.findBy({ followeeId: note.channelId }).then(followings => {
|
||||
for (const following of followings) {
|
||||
insertNoteUnread(following.followerId, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Channels.increment({ id: note.channelId }, 'notesCount', 1);
|
||||
Channels.update(note.channelId, {
|
||||
lastNotedAt: new Date(),
|
||||
});
|
||||
|
||||
const count = await Notes.countBy({
|
||||
userId: user.id,
|
||||
channelId: note.channelId,
|
||||
});
|
||||
|
||||
// This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission.
|
||||
// TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that.
|
||||
if (count === 1) {
|
||||
Channels.increment({ id: note.channelId }, 'usersCount', 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (note.replyId) {
|
||||
Notes.increment({ id: note.replyId }, 'repliesCount', 1);
|
||||
}
|
||||
|
||||
// When there is no re-note of the specified note by the specified user except for this post
|
||||
if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id) === 0)) {
|
||||
Notes.createQueryBuilder().update()
|
||||
.set({
|
||||
renoteCount: () => '"renoteCount" + 1',
|
||||
score: () => '"score" + 1',
|
||||
})
|
||||
.where('id = :id', { id: note.renoteId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
// create job for ended poll notifications
|
||||
if (note.hasPoll) {
|
||||
Polls.findOneByOrFail({ noteId: note.id })
|
||||
.then((poll: Poll) => {
|
||||
if (poll.expiresAt) {
|
||||
const delay = poll.expiresAt.getTime() - Date.now();
|
||||
endedPollNotificationQueue.add({
|
||||
noteId: note.id,
|
||||
}, {
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { mentionedUsers, tags } = await extractFromMfm(user, note);
|
||||
|
||||
// Hashtag Update
|
||||
if (note.visibility === 'public' || note.visibility === 'home') {
|
||||
updateHashtags(user, tags);
|
||||
}
|
||||
|
||||
// Word mute
|
||||
mutedWordsCache.fetch('').then(us => {
|
||||
for (const u of us) {
|
||||
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
|
||||
if (shouldMute) {
|
||||
MutedNotes.insert({
|
||||
id: genId(),
|
||||
userId: u.userId,
|
||||
noteId: note.id,
|
||||
reason: 'word',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Antenna
|
||||
if (!created) {
|
||||
// Provisionally remove from antenna, it may be added again in the next step.
|
||||
// But if it is not removed, it can cause duplicate key errors when trying to
|
||||
// add it to the same antenna again.
|
||||
await AntennaNotes.delete({
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
getAntennas()
|
||||
.then(async antennas => {
|
||||
await Promise.all(antennas.map(antenna => {
|
||||
return checkHitAntenna(antenna, note, user)
|
||||
.then(hit => { if (hit) return addNoteToAntenna(antenna, note, user); });
|
||||
}));
|
||||
});
|
||||
|
||||
// Notifications
|
||||
if (!silent && created) {
|
||||
// Create unread notifications
|
||||
if (note.visibility === 'specified') {
|
||||
if (note.visibleUserIds == null) {
|
||||
throw new Error('specified note but does not have any visible user ids');
|
||||
}
|
||||
|
||||
Users.findBy({
|
||||
id: In(note.visibleUserIds),
|
||||
}).then((visibleUsers: User[]) => {
|
||||
visibleUsers
|
||||
.filter(u => Users.isLocalUser(u))
|
||||
.forEach(u => {
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: true,
|
||||
isMentioned: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
} else {
|
||||
mentionedUsers
|
||||
.filter(u => Users.isLocalUser(u))
|
||||
.forEach(u => {
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: true,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
publishNotesStream(note);
|
||||
|
||||
const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note')));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'note', {
|
||||
note: await Notes.pack(note, user),
|
||||
});
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
await createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
// If it is in reply to another note
|
||||
if (note.replyId) {
|
||||
const reply = await Notes.findOneByOrFail({ id: note.replyId });
|
||||
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(notifyWatchers(note.replyId, user, nm, 'reply'));
|
||||
|
||||
// Notifications
|
||||
if (reply.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: reply.userId,
|
||||
threadId: reply.threadId ?? reply.id,
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(reply.userId, 'reply');
|
||||
|
||||
const packedReply = await Notes.pack(note, { id: reply.userId });
|
||||
publishMainStream(reply.userId, 'reply', packedReply);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === reply.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'reply', {
|
||||
note: packedReply,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it is a renote
|
||||
if (note.renoteId) {
|
||||
const type = note.text ? 'quote' : 'renote';
|
||||
const renote = await Notes.findOneByOrFail({ id : note.renoteId });
|
||||
|
||||
// Notify
|
||||
if (renote.userHost === null) {
|
||||
nm.push(renote.userId, type);
|
||||
}
|
||||
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(notifyWatchers(note.renoteId, user, nm, type));
|
||||
|
||||
// Publish event
|
||||
if ((user.id !== renote.userId) && renote.userHost === null) {
|
||||
const packedRenote = await Notes.pack(note, { id: renote.userId });
|
||||
publishMainStream(renote.userId, 'renote', packedRenote);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === renote.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, 'renote', {
|
||||
note: packedRenote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(nmRelatedPromises).then(() => {
|
||||
nm.deliver();
|
||||
});
|
||||
|
||||
//#region AP deliver
|
||||
if (Users.isLocalUser(user) && !note.localOnly && created) {
|
||||
(async () => {
|
||||
const noteActivity = renderActivity(await renderNoteOrRenoteActivity(note));
|
||||
const dm = new DeliverManager(user, noteActivity);
|
||||
|
||||
// Delivered to remote users who have been mentioned
|
||||
for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
}
|
||||
|
||||
// If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver
|
||||
if (note.replyId) {
|
||||
const subquery = Notes.createaQueryBuilder()
|
||||
.select('userId')
|
||||
.where('"id" = :replyId', { replyId: note.replyId });
|
||||
const u = await Users.createQueryBuilder()
|
||||
.where('"id" IN (' + subquery.getQuery() + ')')
|
||||
.andWhere('"userHost" IS NOT NULL')
|
||||
.getOne();
|
||||
if (u != null) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver
|
||||
if (note.renoteId) {
|
||||
const subquery = Notes.createaQueryBuilder()
|
||||
.select('userId')
|
||||
.where('"id" = :renoteId', { renoteId: note.renoteId });
|
||||
const u = await Users.createQueryBuilder()
|
||||
.where('"id" IN (' + subquery.getQuery() + ')')
|
||||
.andWhere('"userHost" IS NOT NULL')
|
||||
.getOne();
|
||||
if (u != null) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// Deliver to followers
|
||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
if (['public'].includes(note.visibility)) {
|
||||
deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
} else if (!created) {
|
||||
// updating a note does not change its un-/read status
|
||||
// updating does not trigger notifications for replied to or renoted notes
|
||||
// updating does not trigger notifications for mentioned users (since mentions cannot be changed)
|
||||
|
||||
// TODO publish to streaming API
|
||||
publishNoteStream(note.id, 'updated', { note });
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
notifyWatchers(note.id, user, nm, 'update');
|
||||
await nm.deliver();
|
||||
|
||||
// TODO AP deliver
|
||||
}
|
||||
|
||||
// Register to search database
|
||||
index(note);
|
||||
}
|
||||
|
||||
async function notifyWatchers(noteId: Note['id'], user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise<void> {
|
||||
const watchers = await NoteWatchings.findBy({
|
||||
noteId,
|
||||
userId: Not(user.id),
|
||||
});
|
||||
|
||||
for (const watcher of watchers) {
|
||||
nm.push(watcher.userId, type);
|
||||
}
|
||||
}
|
||||
|
||||
async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager): Promise<void> {
|
||||
for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: u.id,
|
||||
threadId: note.threadId || note.id,
|
||||
});
|
||||
|
||||
if (threadMuted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
nm.push(u.id, 'mention');
|
||||
}
|
||||
}
|
||||
|
||||
async function extractFromMfm(user: { host: User['host']; }, note: Note): Promise<{ mentionedUsers: User[], tags: string[] }> {
|
||||
const tokens = mfm.parse(note.text ?? '').concat(mfm.parse(note.cw ?? ''));
|
||||
|
||||
const tags = extractHashtags(tokens);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return {
|
||||
mentionedUsers: [],
|
||||
tags,
|
||||
};
|
||||
}
|
||||
|
||||
const mentions = extractMentions(tokens);
|
||||
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
resolveUser(m.username, m.host || user.host).catch(() => null),
|
||||
))).filter(x => x != null) as User[];
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
i === self.findIndex(u2 => u.id === u2.id),
|
||||
);
|
||||
|
||||
return {
|
||||
mentionedUsers,
|
||||
tags,
|
||||
};
|
||||
}
|
|
@ -12,7 +12,7 @@ import { db } from '@/db/postgre.js';
|
|||
import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js';
|
||||
|
||||
export async function getSystemUser(username: string): Promise<User> {
|
||||
const exist = await Users.findBy({
|
||||
const exist = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { initDb } from '../src/db/postgre.js';
|
||||
import { initTestDb } from './utils.js';
|
||||
import { initDb } from '../built/db/postgre.js';
|
||||
import { initTestDb } from './utils.mjs';
|
||||
|
||||
|
||||
function rndstr(length): string {
|
||||
function rndstr(length) {
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const chars_len = 62;
|
||||
|
||||
|
@ -52,8 +52,8 @@ describe('ActivityPub', () => {
|
|||
};
|
||||
|
||||
it('Minimum Actor', async () => {
|
||||
const { MockResolver } = await import('./misc/mock-resolver.js');
|
||||
const { createPerson } = await import('../src/remote/activitypub/models/person.js');
|
||||
const { MockResolver } = await import('./misc/mock-resolver.mjs');
|
||||
const { createPerson } = await import('../built/remote/activitypub/models/person.js');
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
||||
|
@ -66,8 +66,8 @@ describe('ActivityPub', () => {
|
|||
});
|
||||
|
||||
it('Minimum Note', async () => {
|
||||
const { MockResolver } = await import('./misc/mock-resolver.js');
|
||||
const { createNote } = await import('../src/remote/activitypub/models/note.js');
|
||||
const { MockResolver } = await import('./misc/mock-resolver.mjs');
|
||||
const { createNote } = await import('../built/remote/activitypub/models/note.js');
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
||||
|
@ -99,8 +99,8 @@ describe('ActivityPub', () => {
|
|||
};
|
||||
|
||||
it('Actor', async () => {
|
||||
const { MockResolver } = await import('./misc/mock-resolver.js');
|
||||
const { createPerson } = await import('../src/remote/activitypub/models/person.js');
|
||||
const { MockResolver } = await import('./misc/mock-resolver.mjs');
|
||||
const { createPerson } = await import('../built/remote/activitypub/models/person.js');
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
|
@ -1,9 +1,9 @@
|
|||
import * as assert from 'assert';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { genRsaKeyPair } from '../src/misc/gen-key-pair.js';
|
||||
import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js';
|
||||
import { genRsaKeyPair } from '../built/misc/gen-key-pair.js';
|
||||
import { createSignedPost, createSignedGet } from '../built/remote/activitypub/ap-request.js';
|
||||
|
||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
|
||||
export const buildParsedSignature = (signingString, signature, algorithm) => {
|
||||
return {
|
||||
scheme: 'Signature',
|
||||
params: {
|
|
@ -2,13 +2,14 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('API visibility', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('API visibility', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let p;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
p = await startServer();
|
||||
});
|
||||
|
||||
|
@ -19,48 +20,48 @@ describe('API visibility', () => {
|
|||
describe('Note visibility', async () => {
|
||||
//#region vars
|
||||
/** protagonist */
|
||||
let alice: any;
|
||||
let alice;
|
||||
/** follower */
|
||||
let follower: any;
|
||||
let follower;
|
||||
/** non-follower */
|
||||
let other: any;
|
||||
let other;
|
||||
/** non-follower who has been replied to or mentioned */
|
||||
let target: any;
|
||||
let target;
|
||||
/** actor for which a specified visibility was set */
|
||||
let target2: any;
|
||||
let target2;
|
||||
|
||||
/** public-post */
|
||||
let pub: any;
|
||||
let pub;
|
||||
/** home-post */
|
||||
let home: any;
|
||||
let home;
|
||||
/** followers-post */
|
||||
let fol: any;
|
||||
let fol;
|
||||
/** specified-post */
|
||||
let spe: any;
|
||||
let spe;
|
||||
|
||||
/** public-reply to target's post */
|
||||
let pubR: any;
|
||||
let pubR;
|
||||
/** home-reply to target's post */
|
||||
let homeR: any;
|
||||
let homeR;
|
||||
/** followers-reply to target's post */
|
||||
let folR: any;
|
||||
let folR;
|
||||
/** specified-reply to target's post */
|
||||
let speR: any;
|
||||
let speR;
|
||||
|
||||
/** public-mention to target */
|
||||
let pubM: any;
|
||||
let pubM;
|
||||
/** home-mention to target */
|
||||
let homeM: any;
|
||||
let homeM;
|
||||
/** followers-mention to target */
|
||||
let folM: any;
|
||||
let folM;
|
||||
/** specified-mention to target */
|
||||
let speM: any;
|
||||
let speM;
|
||||
|
||||
/** reply target post */
|
||||
let tgt: any;
|
||||
let tgt;
|
||||
//#endregion
|
||||
|
||||
const show = async (noteId: any, by: any) => {
|
||||
const show = async (noteId, by) => {
|
||||
return await request('/notes/show', {
|
||||
noteId,
|
||||
}, by);
|
||||
|
@ -411,21 +412,21 @@ describe('API visibility', () => {
|
|||
it('[TL] public post on author home TL', async(async () => {
|
||||
const res = await request('/notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == pub.id);
|
||||
const notes = res.body.filter((n) => n.id == pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}));
|
||||
|
||||
it('[TL] public post absent from non-follower home TL', async(async () => {
|
||||
const res = await request('/notes/timeline', { limit: 100 }, other);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == pub.id);
|
||||
const notes = res.body.filter((n) => n.id == pub.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
}));
|
||||
|
||||
it('[TL] followers post on follower home TL', async(async () => {
|
||||
const res = await request('/notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == fol.id);
|
||||
const notes = res.body.filter((n) => n.id == fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}));
|
||||
//#endregion
|
||||
|
@ -434,21 +435,21 @@ describe('API visibility', () => {
|
|||
it('[TL] followers reply on follower reply TL', async(async () => {
|
||||
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
const notes = res.body.filter((n) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}));
|
||||
|
||||
it('[TL] followers reply absent from not replied to non-follower reply TL', async(async () => {
|
||||
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
const notes = res.body.filter((n) => n.id == folR.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
}));
|
||||
|
||||
it('[TL] followers reply on replied to actor reply TL', async(async () => {
|
||||
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
const notes = res.body.filter((n) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}));
|
||||
//#endregion
|
||||
|
@ -457,14 +458,14 @@ describe('API visibility', () => {
|
|||
it('[TL] followers reply on replied to non-follower mention TL', async(async () => {
|
||||
const res = await request('/notes/mentions', { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
const notes = res.body.filter((n) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}));
|
||||
|
||||
it('[TL] followers mention on mentioned non-follower mention TL', async(async () => {
|
||||
const res = await request('/notes/mentions', { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folM.id);
|
||||
const notes = res.body.filter((n) => n.id == folM.id);
|
||||
assert.strictEqual(notes[0].text, '@target x');
|
||||
}));
|
||||
//#endregion
|
|
@ -2,16 +2,15 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('API', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
describe('API', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
before(async function() {
|
||||
this.timeout(0);
|
||||
let p;
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
|
@ -2,19 +2,17 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('Block', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('Block', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let p;
|
||||
|
||||
// alice blocks bob
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
|
@ -80,8 +78,8 @@ describe('Block', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), true);
|
||||
}));
|
||||
});
|
|
@ -2,18 +2,14 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import TestChart from '../src/services/chart/charts/test.js';
|
||||
import TestGroupedChart from '../src/services/chart/charts/test-grouped.js';
|
||||
import TestUniqueChart from '../src/services/chart/charts/test-unique.js';
|
||||
import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js';
|
||||
import { initDb } from '../src/db/postgre.js';
|
||||
import TestChart from '../built/services/chart/charts/test.js';
|
||||
import TestGroupedChart from '../built/services/chart/charts/test-grouped.js';
|
||||
import TestUniqueChart from '../built/services/chart/charts/test-unique.js';
|
||||
import TestIntersectionChart from '../built/services/chart/charts/test-intersection.js';
|
||||
import { initDb } from '../built/db/postgre.js';
|
||||
|
||||
describe('Chart', () => {
|
||||
let testChart: TestChart;
|
||||
let testGroupedChart: TestGroupedChart;
|
||||
let testUniqueChart: TestUniqueChart;
|
||||
let testIntersectionChart: TestIntersectionChart;
|
||||
let clock: lolex.InstalledClock;
|
||||
let testChart, testGroupedChart, testUniqueChart, testIntersectionChart, clock;
|
||||
|
||||
beforeEach(async () => {
|
||||
await initDb(true);
|
|
@ -1,15 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
redistest:
|
||||
image: redis:6
|
||||
ports:
|
||||
- "127.0.0.1:56312:6379"
|
||||
|
||||
dbtest:
|
||||
image: postgres:13
|
||||
ports:
|
||||
- "127.0.0.1:54312:5432"
|
||||
environment:
|
||||
POSTGRES_DB: "test-misskey"
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
@ -3,13 +3,12 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('API: Endpoints', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
let p;
|
||||
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
|
@ -1,11 +1,11 @@
|
|||
import * as assert from 'assert';
|
||||
|
||||
import { parse } from 'mfm-js';
|
||||
import { extractMentions } from '../src/misc/extract-mentions.js';
|
||||
import { extractMentions } from '../built/misc/extract-mentions.js';
|
||||
|
||||
describe('Extract mentions', () => {
|
||||
it('simple', () => {
|
||||
const ast = parse('@foo @bar @baz')!;
|
||||
const ast = parse('@foo @bar @baz');
|
||||
const mentions = extractMentions(ast);
|
||||
assert.deepStrictEqual(mentions, [{
|
||||
username: 'foo',
|
||||
|
@ -23,7 +23,7 @@ describe('Extract mentions', () => {
|
|||
});
|
||||
|
||||
it('nested', () => {
|
||||
const ast = parse('@foo **@bar** @baz')!;
|
||||
const ast = parse('@foo **@bar** @baz');
|
||||
const mentions = extractMentions(ast);
|
||||
assert.deepStrictEqual(mentions, [{
|
||||
username: 'foo',
|
|
@ -3,7 +3,7 @@ process.env.NODE_ENV = 'test';
|
|||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as openapi from '@redocly/openapi-core';
|
||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js';
|
||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.mjs';
|
||||
|
||||
// Request Accept
|
||||
const ONLY_AP = 'application/activity+json';
|
||||
|
@ -16,14 +16,14 @@ const AP = 'application/activity+json; charset=utf-8';
|
|||
const JSON = 'application/json; charset=utf-8';
|
||||
const HTML = 'text/html; charset=utf-8';
|
||||
|
||||
describe('Fetch resource', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('Fetch resource', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let alice: any;
|
||||
let alicesPost: any;
|
||||
let p;
|
||||
|
||||
let alice, alicesPost;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
alicesPost = await post(alice, {
|
|
@ -2,18 +2,16 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.js';
|
||||
import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.mjs';
|
||||
|
||||
describe('FF visibility', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('FF visibility', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let follower: any;
|
||||
let p;
|
||||
|
||||
let alice, bob, follower;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
|
@ -1,8 +1,8 @@
|
|||
import * as assert from 'assert';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { getFileInfo } from '../src/misc/get-file-info.js';
|
||||
import { async } from './utils.js';
|
||||
import { getFileInfo } from '../built/misc/get-file-info.js';
|
||||
import { async } from './utils.mjs';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
@ -10,7 +10,7 @@ const _dirname = dirname(_filename);
|
|||
describe('Get file info', () => {
|
||||
it('Empty file', async (async () => {
|
||||
const path = `${_dirname}/resources/emptyfile`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -28,7 +28,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic JPEG', async (async () => {
|
||||
const path = `${_dirname}/resources/Lenna.jpg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -46,7 +46,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic APNG', async (async () => {
|
||||
const path = `${_dirname}/resources/anime.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -64,7 +64,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic AGIF', async (async () => {
|
||||
const path = `${_dirname}/resources/anime.gif`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -82,7 +82,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('PNG with alpha', async (async () => {
|
||||
const path = `${_dirname}/resources/with-alpha.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -100,7 +100,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Generic SVG', async (async () => {
|
||||
const path = `${_dirname}/resources/image.svg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -119,7 +119,7 @@ describe('Get file info', () => {
|
|||
it('SVG with XML definition', async (async () => {
|
||||
// https://github.com/misskey-dev/misskey/issues/4413
|
||||
const path = `${_dirname}/resources/with-xml-def.svg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -137,7 +137,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Dimension limit', async (async () => {
|
||||
const path = `${_dirname}/resources/25000x25000.png`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
||||
|
@ -155,7 +155,7 @@ describe('Get file info', () => {
|
|||
|
||||
it('Rotate JPEG', async (async () => {
|
||||
const path = `${_dirname}/resources/rotate.jpg`;
|
||||
const info = await getFileInfo(path) as any;
|
||||
const info = await getFileInfo(path);
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
assert.deepStrictEqual(info, {
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* ts-node/esmローダーに投げる前にpath mappingを解決する
|
||||
* 参考
|
||||
* - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
|
||||
* - https://nodejs.org/api/esm.html#loaders
|
||||
* ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる
|
||||
*/
|
||||
|
||||
import { resolve as resolveTs, load } from 'ts-node/esm';
|
||||
import { loadConfig, createMatchPath } from 'tsconfig-paths';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
const tsconfig = loadConfig();
|
||||
const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths);
|
||||
|
||||
export function resolve(specifier, ctx, defaultResolve) {
|
||||
let resolvedSpecifier;
|
||||
if (specifier.endsWith('.js')) {
|
||||
// maybe transpiled
|
||||
const specifierWithoutExtension = specifier.substring(0, specifier.length - '.js'.length);
|
||||
const matchedSpecifier = matchPath(specifierWithoutExtension);
|
||||
if (matchedSpecifier) {
|
||||
resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href;
|
||||
}
|
||||
} else {
|
||||
const matchedSpecifier = matchPath(specifier);
|
||||
if (matchedSpecifier) {
|
||||
resolvedSpecifier = pathToFileURL(matchedSpecifier).href;
|
||||
}
|
||||
}
|
||||
return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve);
|
||||
}
|
||||
|
||||
export { load };
|
|
@ -1,7 +1,7 @@
|
|||
import * as assert from 'assert';
|
||||
|
||||
import { toHtml } from '../src/mfm/to-html.js';
|
||||
import { fromHtml } from '../src/mfm/from-html.js';
|
||||
import { toHtml } from '../built/mfm/to-html.js';
|
||||
import { fromHtml } from '../built/mfm/from-html.js';
|
||||
|
||||
describe('toHtml', () => {
|
||||
it('br', async () => {
|
|
@ -1,21 +1,16 @@
|
|||
import { Resolver } from '../../src/remote/activitypub/resolver.js';
|
||||
import { IObject } from '../../src/remote/activitypub/type.js';
|
||||
|
||||
type MockResponse = {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
import { Resolver } from '../../built/remote/activitypub/resolver.js';
|
||||
|
||||
export class MockResolver extends Resolver {
|
||||
private _rs = new Map<string, MockResponse>();
|
||||
public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
|
||||
_rs = new Map();
|
||||
|
||||
async _register(uri, content, type = 'application/activity+json') {
|
||||
this._rs.set(uri, {
|
||||
type,
|
||||
content: typeof content === 'string' ? content : JSON.stringify(content),
|
||||
});
|
||||
}
|
||||
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
async resolve(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
const r = this._rs.get(value);
|
|
@ -2,19 +2,17 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.js';
|
||||
import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.mjs';
|
||||
|
||||
describe('Mute', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('Mute', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let p;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
|
@ -41,8 +39,8 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
|
||||
}));
|
||||
|
||||
it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => {
|
||||
|
@ -86,9 +84,9 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
|
||||
}));
|
||||
|
||||
it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => {
|
||||
|
@ -102,9 +100,9 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false);
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -118,8 +116,8 @@ describe('Mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||
assert.strictEqual(res.body.some((notification) => notification.userId === bob.id), true);
|
||||
assert.strictEqual(res.body.some((notification) => notification.userId === carol.id), false);
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -2,19 +2,18 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { Note } from '../src/models/entities/note.js';
|
||||
import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.js';
|
||||
import { Note } from '../built/models/entities/note.js';
|
||||
import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.mjs';
|
||||
|
||||
describe('Note', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let Notes: any;
|
||||
describe('Note', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let p;
|
||||
let Notes;
|
||||
|
||||
let alice, bob;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Notes = connection.getRepository(Note);
|
|
@ -1,5 +1,5 @@
|
|||
import * as assert from 'assert';
|
||||
import { query } from '../../src/prelude/url.js';
|
||||
import { query } from '../../built/prelude/url.js';
|
||||
|
||||
describe('url', () => {
|
||||
it('query', () => {
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
import * as assert from 'assert';
|
||||
|
||||
import { toDbReaction } from '../src/misc/reaction-lib.js';
|
||||
|
||||
describe('toDbReaction', async () => {
|
||||
it('既存の文字列リアクションはそのまま', async () => {
|
||||
assert.strictEqual(await toDbReaction('like'), 'like');
|
||||
});
|
||||
|
||||
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
|
||||
assert.strictEqual(await toDbReaction('🍮'), '🍮');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する like', async () => {
|
||||
assert.strictEqual(await toDbReaction('👍'), 'like');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する love', async () => {
|
||||
assert.strictEqual(await toDbReaction('❤️'), 'love');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
|
||||
assert.strictEqual(await toDbReaction('❤'), 'love');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
|
||||
assert.strictEqual(await toDbReaction('😆'), 'laugh');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
|
||||
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
|
||||
assert.strictEqual(await toDbReaction('😮'), 'surprise');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
|
||||
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
|
||||
assert.strictEqual(await toDbReaction('💢'), 'angry');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
|
||||
assert.strictEqual(await toDbReaction('😥'), 'confused');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
|
||||
assert.strictEqual(await toDbReaction('😇'), 'rip');
|
||||
});
|
||||
|
||||
it('それ以外はUnicodeのまま', async () => {
|
||||
assert.strictEqual(await toDbReaction('🍅'), '🍅');
|
||||
});
|
||||
|
||||
it('異体字セレクタ除去', async () => {
|
||||
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
|
||||
});
|
||||
|
||||
it('異体字セレクタ除去 必要なし', async () => {
|
||||
assert.strictEqual(await toDbReaction('㊗'), '㊗');
|
||||
});
|
||||
|
||||
it('fallback - undefined', async () => {
|
||||
assert.strictEqual(await toDbReaction(undefined), 'like');
|
||||
});
|
||||
|
||||
it('fallback - null', async () => {
|
||||
assert.strictEqual(await toDbReaction(null), 'like');
|
||||
});
|
||||
|
||||
it('fallback - empty', async () => {
|
||||
assert.strictEqual(await toDbReaction(''), 'like');
|
||||
});
|
||||
|
||||
it('fallback - unknown', async () => {
|
||||
assert.strictEqual(await toDbReaction('unknown'), 'like');
|
||||
});
|
||||
});
|
||||
*/
|
|
@ -3,19 +3,17 @@ process.env.NODE_ENV = 'test';
|
|||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as sinon from 'sinon';
|
||||
import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.js';
|
||||
import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.mjs';
|
||||
|
||||
describe('Creating a block activity', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('Creating a block activity', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let p;
|
||||
|
||||
// alice blocks bob
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
await initTestDb();
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
|
@ -34,10 +32,10 @@ describe('Creating a block activity', () => {
|
|||
});
|
||||
|
||||
it('Should federate blocks normally', async(async () => {
|
||||
const createBlock = (await import('../../src/services/blocking/create')).default;
|
||||
const deleteBlock = (await import('../../src/services/blocking/delete')).default;
|
||||
const createBlock = (await import('../../built/services/blocking/create')).default;
|
||||
const deleteBlock = (await import('../../built/services/blocking/delete')).default;
|
||||
|
||||
const queues = await import('../../src/queue/index');
|
||||
const queues = await import('../../built/queue/index');
|
||||
const spy = sinon.spy(queues, 'deliver');
|
||||
await createBlock(alice, bob);
|
||||
assert(spy.calledOnce);
|
||||
|
@ -46,12 +44,12 @@ describe('Creating a block activity', () => {
|
|||
}));
|
||||
|
||||
it('Should not federate blocks if federateBlocks is false', async () => {
|
||||
const createBlock = (await import('../../src/services/blocking/create')).default;
|
||||
const deleteBlock = (await import('../../src/services/blocking/delete')).default;
|
||||
const createBlock = (await import('../../built/services/blocking/create')).default;
|
||||
const deleteBlock = (await import('../../built/services/blocking/delete')).default;
|
||||
|
||||
alice.federateBlocks = true;
|
||||
|
||||
const queues = await import('../../src/queue/index');
|
||||
const queues = await import('../../built/queue/index');
|
||||
const spy = sinon.spy(queues, 'deliver');
|
||||
await createBlock(alice, carol);
|
||||
await deleteBlock(alice, carol);
|
|
@ -2,14 +2,14 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { Following } from '../src/models/entities/following.js';
|
||||
import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.js';
|
||||
import { Following } from '../built/models/entities/following.js';
|
||||
import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.mjs';
|
||||
|
||||
describe('Streaming', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let Followings: any;
|
||||
let p;
|
||||
let Followings;
|
||||
|
||||
const follow = async (follower: any, followee: any) => {
|
||||
const follow = async (follower, followee) => {
|
||||
await Followings.save({
|
||||
id: 'a',
|
||||
createdAt: new Date(),
|
||||
|
@ -24,22 +24,18 @@ describe('Streaming', () => {
|
|||
});
|
||||
};
|
||||
|
||||
describe('Streaming', () => {
|
||||
describe('Streaming', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
// Local users
|
||||
let ayano: any;
|
||||
let kyoko: any;
|
||||
let chitose: any;
|
||||
let ayano, kyoko, chitose;
|
||||
|
||||
// Remote users
|
||||
let akari: any;
|
||||
let chinatsu: any;
|
||||
let akari, chinatsu;
|
||||
|
||||
let kyokoNote: any;
|
||||
let list: any;
|
||||
let kyokoNote, list;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Followings = connection.getRepository(Following);
|
||||
|
@ -388,7 +384,7 @@ describe('Streaming', () => {
|
|||
});
|
||||
|
||||
describe('Hashtag Timeline', () => {
|
||||
it('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
|
||||
it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type == 'note') {
|
||||
assert.deepStrictEqual(body.text, '#foo');
|
||||
|
@ -406,11 +402,11 @@ describe('Streaming', () => {
|
|||
});
|
||||
}));
|
||||
|
||||
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
|
||||
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
|
||||
|
||||
const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
|
||||
if (type == 'note') {
|
||||
if (body.text === '#foo') fooCount++;
|
||||
|
@ -422,19 +418,19 @@ describe('Streaming', () => {
|
|||
['foo', 'bar'],
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo',
|
||||
});
|
||||
|
||||
|
||||
post(chitose, {
|
||||
text: '#bar',
|
||||
});
|
||||
|
||||
|
||||
post(chitose, {
|
||||
text: '#foo #bar',
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 0);
|
||||
assert.strictEqual(barCount, 0);
|
||||
|
@ -444,7 +440,7 @@ describe('Streaming', () => {
|
|||
}, 3000);
|
||||
}));
|
||||
|
||||
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
|
||||
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
|
@ -490,7 +486,7 @@ describe('Streaming', () => {
|
|||
}, 3000);
|
||||
}));
|
||||
|
||||
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
|
||||
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
|
@ -2,18 +2,16 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('Note thread mute', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('Note thread mute', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
let p;
|
||||
|
||||
let alice, bob, carol;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
|
@ -37,9 +35,9 @@ describe('Note thread mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((note) => note.id === carolReplyWithoutMention.id), false);
|
||||
}));
|
||||
|
||||
it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => {
|
||||
|
@ -97,8 +95,8 @@ describe('Note thread mute', () => {
|
|||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
|
||||
assert.strictEqual(res.body.some((notification) => notification.note.id === carolReply.id), false);
|
||||
assert.strictEqual(res.body.some((notification) => notification.note.id === carolReplyWithoutMention.id), false);
|
||||
|
||||
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
||||
}));
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"../node_modules/@types",
|
||||
"../src/@types"
|
||||
],
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
|
@ -2,19 +2,16 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.js';
|
||||
import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.mjs';
|
||||
|
||||
describe('users/notes', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
describe('users/notes', function() {
|
||||
this.timeout(20*60*1000);
|
||||
|
||||
let alice: any;
|
||||
let jpgNote: any;
|
||||
let pngNote: any;
|
||||
let jpgPngNote: any;
|
||||
let p;
|
||||
|
||||
let alice, jpgNote, pngNote, jpgPngNote;
|
||||
|
||||
before(async () => {
|
||||
this.timeout(0);
|
||||
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
||||
|
@ -43,8 +40,8 @@ describe('users/notes', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 2);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true);
|
||||
}));
|
||||
|
||||
it('ファイルタイプ指定 (jpg or png)', async(async () => {
|
||||
|
@ -56,8 +53,8 @@ describe('users/notes', () => {
|
|||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true);
|
||||
}));
|
||||
});
|
|
@ -10,8 +10,8 @@ import * as foundkey from 'foundkey-js';
|
|||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
import { DataSource } from 'typeorm';
|
||||
import loadConfig from '../src/config/load.js';
|
||||
import { entities } from '../src/db/postgre.js';
|
||||
import { loadConfig } from '../built/config/load.js';
|
||||
import { entities } from '../built/db/postgre.js';
|
||||
import got from 'got';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
|
@ -20,20 +20,20 @@ const _dirname = dirname(_filename);
|
|||
const config = loadConfig();
|
||||
export const port = config.port;
|
||||
|
||||
export const async = (fn: Function) => (done: Function) => {
|
||||
export const async = (fn) => (done) => {
|
||||
fn().then(() => {
|
||||
done();
|
||||
}, (err: Error) => {
|
||||
}, (err) => {
|
||||
done(err);
|
||||
});
|
||||
};
|
||||
|
||||
export const api = async (endpoint: string, params: any, me?: any) => {
|
||||
export const api = async (endpoint, params, me) => {
|
||||
endpoint = endpoint.replace(/^\//, '');
|
||||
|
||||
const auth = me ? { authorization: `Bearer ${me.token}` } : {};
|
||||
|
||||
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
|
||||
const res = await got(`http://localhost:${port}/api/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -63,7 +63,7 @@ export const api = async (endpoint: string, params: any, me?: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
||||
export const request = async (endpoint, params, me) => {
|
||||
const auth = me ? { authorization: `Bearer ${me.token}` } : {};
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
|
||||
|
@ -83,7 +83,7 @@ export const request = async (endpoint: string, params: any, me?: any): Promise<
|
|||
};
|
||||
};
|
||||
|
||||
export const signup = async (params?: any): Promise<any> => {
|
||||
export const signup = async (params) => {
|
||||
const q = Object.assign({
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
|
@ -94,7 +94,7 @@ export const signup = async (params?: any): Promise<any> => {
|
|||
return res.body;
|
||||
};
|
||||
|
||||
export const post = async (user: any, params?: foundkey.Endpoints['notes/create']['req']): Promise<foundkey.entities.Note> => {
|
||||
export const post = async (user, params) => {
|
||||
const q = Object.assign({
|
||||
text: 'test',
|
||||
}, params);
|
||||
|
@ -104,7 +104,7 @@ export const post = async (user: any, params?: foundkey.Endpoints['notes/create'
|
|||
return res.body ? res.body.createdNote : null;
|
||||
};
|
||||
|
||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
||||
export const react = async (user, note, reaction) => {
|
||||
await api('notes/reactions/create', {
|
||||
noteId: note.id,
|
||||
reaction: reaction,
|
||||
|
@ -116,10 +116,10 @@ export const react = async (user: any, note: any, reaction: string): Promise<any
|
|||
* @param user User
|
||||
* @param _path Optional, absolute path or relative from ./resources/
|
||||
*/
|
||||
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
|
||||
export const uploadFile = async (user, _path) => {
|
||||
const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
|
||||
|
||||
const formData = new FormData() as any;
|
||||
const formData = new FormData();
|
||||
formData.append('i', user.token);
|
||||
formData.append('file', fs.createReadStream(absPath));
|
||||
formData.append('force', 'true');
|
||||
|
@ -137,8 +137,8 @@ export const uploadFile = async (user: any, _path?: string): Promise<any> => {
|
|||
return body;
|
||||
};
|
||||
|
||||
export const uploadUrl = async (user: any, url: string) => {
|
||||
let file: any;
|
||||
export const uploadUrl = async (user, url) => {
|
||||
let file;
|
||||
|
||||
const ws = await connectStream(user, 'main', (msg) => {
|
||||
if (msg.type === 'driveFileCreated') {
|
||||
|
@ -157,7 +157,7 @@ export const uploadUrl = async (user: any, url: string) => {
|
|||
return file;
|
||||
};
|
||||
|
||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
||||
export function connectStream(user, channel, listener, params) {
|
||||
return new Promise((res, rej) => {
|
||||
const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`);
|
||||
|
||||
|
@ -184,11 +184,11 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
|||
});
|
||||
}
|
||||
|
||||
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
|
||||
return new Promise<boolean>(async (res, rej) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
export const waitFire = async (user, channel, trgr, cond, params) => {
|
||||
return new Promise(async (res, rej) => {
|
||||
let timer;
|
||||
|
||||
let ws: WebSocket;
|
||||
let ws;
|
||||
try {
|
||||
ws = await connectStream(user, channel, msg => {
|
||||
if (cond(msg)) {
|
||||
|
@ -201,7 +201,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
|
|||
rej(e);
|
||||
}
|
||||
|
||||
if (!ws!) return;
|
||||
if (!ws) return;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
ws.close();
|
||||
|
@ -218,7 +218,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
|
|||
})
|
||||
};
|
||||
|
||||
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
|
||||
export const simpleGet = async (path, accept = '*/*') => {
|
||||
// node-fetchだと3xxを取れない
|
||||
return await new Promise((resolve, reject) => {
|
||||
const req = http.request(`http://localhost:${port}${path}`, {
|
||||
|
@ -226,7 +226,7 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
|
|||
Accept: accept,
|
||||
},
|
||||
}, res => {
|
||||
if (res.statusCode! >= 400) {
|
||||
if (res.statusCode >= 400) {
|
||||
reject(res);
|
||||
} else {
|
||||
resolve({
|
||||
|
@ -241,8 +241,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
|
|||
});
|
||||
};
|
||||
|
||||
export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) {
|
||||
return (done: (err?: Error) => any) => {
|
||||
export function launchServer(callbackSpawnedProcess, moreProcess = async () => {}) {
|
||||
return (done) => {
|
||||
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
||||
|
@ -254,7 +254,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
|
|||
};
|
||||
}
|
||||
|
||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
export async function initTestDb(justBorrow = false, initEntities) {
|
||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
||||
|
||||
const db = new DataSource({
|
||||
|
@ -274,7 +274,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
|||
return db;
|
||||
}
|
||||
|
||||
export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProcess> {
|
||||
export function startServer(timeout = 60 * 1000) {
|
||||
return new Promise((res, rej) => {
|
||||
const t = setTimeout(() => {
|
||||
p.kill(SIGKILL);
|
||||
|
@ -297,7 +297,7 @@ export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProc
|
|||
});
|
||||
}
|
||||
|
||||
export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000) {
|
||||
export function shutdownServer(p, timeout = 20 * 1000) {
|
||||
return new Promise((res, rej) => {
|
||||
const t = setTimeout(() => {
|
||||
p.kill(SIGKILL);
|
||||
|
@ -313,8 +313,8 @@ export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000
|
|||
});
|
||||
}
|
||||
|
||||
export function sleep(msec: number) {
|
||||
return new Promise<void>(res => {
|
||||
export function sleep(msec) {
|
||||
return new Promise(res => {
|
||||
setTimeout(() => {
|
||||
res();
|
||||
}, msec);
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"watch": "vite build --watch --mode development",
|
||||
|
|
|
@ -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" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
|
|
|
@ -175,6 +175,9 @@ withDefaults(defineProps<{
|
|||
<style lang="scss" scoped>
|
||||
.havbbuyv {
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
|
||||
&.nowrap {
|
||||
white-space: pre;
|
||||
|
|
|
@ -49,19 +49,19 @@
|
|||
<div class="main">
|
||||
<div class="body">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
||||
<div class="text">
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,6 +79,12 @@
|
|||
<MkA class="created-at" :to="notePage(appearNote)">
|
||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||
</MkA>
|
||||
<!-- TODO link to edit history? -->
|
||||
<div v-if="appearNote.updatedAt != null">
|
||||
<i class="fas fa-pencil"></i>
|
||||
{{ i18n.ts.updatedAt }}
|
||||
<MkTime :time="appearNote.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</div>
|
||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="button _button" @click="reply()">
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</MkA>
|
||||
<span class="updated-at" v-if="note.updatedAt != null" ref="updatedEl">
|
||||
<i class="fas fa-pencil"></i>
|
||||
</span>
|
||||
<MkVisibility :note="note"/>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -69,6 +72,10 @@ defineProps<{
|
|||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .updated-at {
|
||||
margin-left: var(--marginHalf);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<div class="body">
|
||||
<div class="content">
|
||||
<Mfm :text="text.trim()" :author="$i" :i="$i"/>
|
||||
<Mfm :text="text.trim()" :author="$i"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,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" :custom-emojis="note.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
|
|
|
@ -37,19 +37,19 @@
|
|||
<MkInstanceTicker v-if="showTicker" class="ticker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
<div class="body">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
||||
<div class="text">
|
||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="body">
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="description">
|
||||
<div v-if="user.description" class="mfm">
|
||||
<Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
|
||||
<Mfm :text="user.description" :author="user" :custom-emojis="user.emojis"/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
|
||||
<Mfm v-if="user.description" :text="user.description" :author="user" :custom-emojis="user.emojis"/>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div class="fade"></div>
|
||||
</div>
|
||||
<div v-if="channel.description" class="description">
|
||||
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
|
||||
<Mfm :text="channel.description" :is-note="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div v-if="clip">
|
||||
<div class="okzinsic _panel">
|
||||
<div v-if="clip.description" class="description">
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
<Mfm :text="clip.description" :is-note="false"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<img src="/client-assets/remove.png" alt="Delete"/>
|
||||
</button>
|
||||
<div v-if="!message.isDeleted" class="content">
|
||||
<Mfm v-if="message.text" ref="text" class="text" :text="message.text" :i="$i"/>
|
||||
<Mfm v-if="message.text" ref="text" class="text" :text="message.text"/>
|
||||
<div v-if="message.file" class="file">
|
||||
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
|
||||
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
|
||||
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :custom-emojis="user.emojis"/>
|
||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||
</div>
|
||||
<div class="fields system">
|
||||
|
@ -74,7 +74,7 @@
|
|||
<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
|
||||
</dt>
|
||||
<dd class="value">
|
||||
<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
|
||||
<Mfm :text="field.value" :author="user" :custom-emojis="user.emojis" :colored="false"/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="content _panel">
|
||||
<div class="body">
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<div v-if="note.files.length > 0" class="richcontent">
|
||||
|
|
|
@ -6,6 +6,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
|||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
|
||||
if (text === '') return false;
|
||||
const textLower = text.toLowerCase();
|
||||
|
||||
const matched = mutedWords.some(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
|
@ -13,7 +14,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
|||
const filteredFilter = filter.filter(keyword => keyword !== '');
|
||||
if (filteredFilter.length === 0) return false;
|
||||
|
||||
return filteredFilter.every(keyword => text.includes(keyword));
|
||||
return filteredFilter.every(keyword => textLower.includes(keyword.toLowerCase()));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
|
|
@ -243,7 +243,7 @@ export class ColdDeviceStorage {
|
|||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
sound_masterVolume: 0.3,
|
||||
sound_note: { type: 'syuilo/down', volume: 1 },
|
||||
sound_note: { type: 'syuilo/down', volume: 0 },
|
||||
sound_noteMy: { type: 'syuilo/up', volume: 1 },
|
||||
sound_notification: { type: 'syuilo/pope2', volume: 1 },
|
||||
sound_chat: { type: 'syuilo/pope1', volume: 1 },
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "foundkey-js",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"description": "Fork of misskey-js for Foundkey",
|
||||
"type": "module",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app'] as const;
|
||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app'] as const;
|
||||
|
||||
export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded'] as const;
|
||||
export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update'] as const;
|
||||
|
||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ export type UserLite = {
|
|||
|
||||
export type UserDetailed = UserLite & {
|
||||
bannerBlurhash: string | null;
|
||||
bannerColor: string | null;
|
||||
bannerUrl: string | null;
|
||||
birthday: string | null;
|
||||
createdAt: DateString;
|
||||
|
@ -130,6 +129,7 @@ export type DriveFolder = TODO;
|
|||
export type Note = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
updatedAt: DateString | null;
|
||||
text: string | null;
|
||||
cw: string | null;
|
||||
user: User;
|
||||
|
@ -207,6 +207,11 @@ export type Notification = {
|
|||
user: User;
|
||||
userId: User['id'];
|
||||
note: Note;
|
||||
} | {
|
||||
type: 'update';
|
||||
user: User;
|
||||
userId: User['id'];
|
||||
note: Note;
|
||||
} | {
|
||||
type: 'follow';
|
||||
user: User;
|
||||
|
|
|
@ -142,6 +142,12 @@ export type NoteUpdatedEvent = {
|
|||
choice: number;
|
||||
userId: User['id'];
|
||||
};
|
||||
} | {
|
||||
id: Note['id'];
|
||||
type: 'updated';
|
||||
body: {
|
||||
note: Note;
|
||||
};
|
||||
};
|
||||
|
||||
export type BroadcastEvents = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sw",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"watch": "node build.js watch",
|
||||
|
|
365
yarn.lock
365
yarn.lock
|
@ -3717,8 +3717,8 @@ __metadata:
|
|||
cli-highlight: 2.1.11
|
||||
color-convert: 2.0.1
|
||||
content-disposition: 0.5.4
|
||||
cross-env: 7.0.3
|
||||
date-fns: 2.28.0
|
||||
decompress: 4.2.1
|
||||
deep-email-validator: 0.1.21
|
||||
escape-regexp: 0.0.1
|
||||
eslint: ^8.29.0
|
||||
|
@ -3794,7 +3794,6 @@ __metadata:
|
|||
twemoji-parser: 14.0.0
|
||||
typeorm: 0.3.7
|
||||
typescript: ^4.9.4
|
||||
unzipper: 0.10.11
|
||||
uuid: 8.3.2
|
||||
web-push: 3.5.0
|
||||
ws: 8.8.0
|
||||
|
@ -3861,13 +3860,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big-integer@npm:^1.6.17":
|
||||
version: 1.6.51
|
||||
resolution: "big-integer@npm:1.6.51"
|
||||
checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"big.js@npm:^5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "big.js@npm:5.2.2"
|
||||
|
@ -3882,16 +3874,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"binary@npm:~0.3.0":
|
||||
version: 0.3.0
|
||||
resolution: "binary@npm:0.3.0"
|
||||
dependencies:
|
||||
buffers: ~0.1.1
|
||||
chainsaw: ~0.1.0
|
||||
checksum: b4699fda9e2c2981e74a46b0115cf0d472eda9b68c0e9d229ef494e92f29ce81acf0a834415094cffcc340dfee7c4ef8ce5d048c65c18067a7ed850323f777af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"binaryextensions@npm:^2.2.0":
|
||||
version: 2.3.0
|
||||
resolution: "binaryextensions@npm:2.3.0"
|
||||
|
@ -3899,6 +3881,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bl@npm:^1.0.0":
|
||||
version: 1.2.3
|
||||
resolution: "bl@npm:1.2.3"
|
||||
dependencies:
|
||||
readable-stream: ^2.3.5
|
||||
safe-buffer: ^5.1.1
|
||||
checksum: 123f097989ce2fa9087ce761cd41176aaaec864e28f7dfe5c7dab8ae16d66d9844f849c3ad688eb357e3c5e4f49b573e3c0780bb8bc937206735a3b6f8569a5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bl@npm:^4.0.3":
|
||||
version: 4.1.0
|
||||
resolution: "bl@npm:4.1.0"
|
||||
|
@ -3924,13 +3916,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bluebird@npm:~3.4.1":
|
||||
version: 3.4.7
|
||||
resolution: "bluebird@npm:3.4.7"
|
||||
checksum: bffa9dee7d3a41ab15c4f3f24687b49959b4e64e55c058a062176feb8ccefc2163414fb4e1a0f3053bf187600936509660c3ebd168fd9f0e48c7eba23b019466
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blurhash@npm:1.1.5":
|
||||
version: 1.1.5
|
||||
resolution: "blurhash@npm:1.1.5"
|
||||
|
@ -4078,6 +4063,23 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-alloc-unsafe@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "buffer-alloc-unsafe@npm:1.1.0"
|
||||
checksum: c5e18bf51f67754ec843c9af3d4c005051aac5008a3992938dda1344e5cfec77c4b02b4ca303644d1e9a6e281765155ce6356d85c6f5ccc5cd21afc868def396
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-alloc@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "buffer-alloc@npm:1.2.0"
|
||||
dependencies:
|
||||
buffer-alloc-unsafe: ^1.1.0
|
||||
buffer-fill: ^1.0.0
|
||||
checksum: 560cd27f3cbe73c614867da373407d4506309c62fe18de45a1ce191f3785ec6ca2488d802ff82065798542422980ca25f903db078c57822218182c37c3576df5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3":
|
||||
version: 0.2.13
|
||||
resolution: "buffer-crc32@npm:0.2.13"
|
||||
|
@ -4099,6 +4101,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-fill@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "buffer-fill@npm:1.0.0"
|
||||
checksum: c29b4723ddeab01e74b5d3b982a0c6828f2ded49cef049ddca3dac661c874ecdbcecb5dd8380cf0f4adbeb8cff90a7de724126750a1f1e5ebd4eb6c59a1315b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-from@npm:^1.0.0":
|
||||
version: 1.1.1
|
||||
resolution: "buffer-from@npm:1.1.1"
|
||||
|
@ -4106,13 +4115,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-indexof-polyfill@npm:~1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "buffer-indexof-polyfill@npm:1.0.2"
|
||||
checksum: fbfb2d69c6bb2df235683126f9dc140150c08ac3630da149913a9971947b667df816a913b6993bc48f4d611999cb99a1589914d34c02dccd2234afda5cb75bbc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer-writer@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "buffer-writer@npm:2.0.0"
|
||||
|
@ -4131,7 +4133,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:^5.5.0, buffer@npm:^5.6.0":
|
||||
"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0":
|
||||
version: 5.7.1
|
||||
resolution: "buffer@npm:5.7.1"
|
||||
dependencies:
|
||||
|
@ -4151,13 +4153,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffers@npm:~0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "buffers@npm:0.1.1"
|
||||
checksum: ad6f8e483efab39cefd92bdc04edbff6805e4211b002f4d1cfb70c6c472a61cc89fb18c37bcdfdd4ee416ca096e9ff606286698a7d41a18b539bac12fd76d4d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bull@npm:4.8.4":
|
||||
version: 4.8.4
|
||||
resolution: "bull@npm:4.8.4"
|
||||
|
@ -4385,15 +4380,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chainsaw@npm:~0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "chainsaw@npm:0.1.0"
|
||||
dependencies:
|
||||
traverse: ">=0.3.0 <0.4"
|
||||
checksum: 22a96b9fb0cd9fb20813607c0869e61817d1acc81b5d455cc6456b5e460ea1dd52630e0f76b291cf8294bfb6c1fc42e299afb52104af9096242699d6d3aa6d3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk-template@npm:0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "chalk-template@npm:0.4.0"
|
||||
|
@ -5042,7 +5028,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3":
|
||||
"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3, commander@npm:^2.8.1":
|
||||
version: 2.20.3
|
||||
resolution: "commander@npm:2.20.3"
|
||||
checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e
|
||||
|
@ -5684,6 +5670,69 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-tar@npm:^4.0.0, decompress-tar@npm:^4.1.0, decompress-tar@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "decompress-tar@npm:4.1.1"
|
||||
dependencies:
|
||||
file-type: ^5.2.0
|
||||
is-stream: ^1.1.0
|
||||
tar-stream: ^1.5.2
|
||||
checksum: 42d5360b558a28dd884e1bf809e3fea92b9910fda5151add004d4a64cc76ac124e8b3e9117e805f2349af9e49c331d873e6fc5ad86a00e575703fee632b0a225
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-tarbz2@npm:^4.0.0":
|
||||
version: 4.1.1
|
||||
resolution: "decompress-tarbz2@npm:4.1.1"
|
||||
dependencies:
|
||||
decompress-tar: ^4.1.0
|
||||
file-type: ^6.1.0
|
||||
is-stream: ^1.1.0
|
||||
seek-bzip: ^1.0.5
|
||||
unbzip2-stream: ^1.0.9
|
||||
checksum: 519c81337730159a1f2d7072a6ee8523ffd76df48d34f14c27cb0a27f89b4e2acf75dad2f761838e5bc63230cea1ac154b092ecb7504be4e93f7d0e32ddd6aff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-targz@npm:^4.0.0":
|
||||
version: 4.1.1
|
||||
resolution: "decompress-targz@npm:4.1.1"
|
||||
dependencies:
|
||||
decompress-tar: ^4.1.1
|
||||
file-type: ^5.2.0
|
||||
is-stream: ^1.1.0
|
||||
checksum: 22738f58eb034568dc50d370c03b346c428bfe8292fe56165847376b5af17d3c028fefca82db642d79cb094df4c0a599d40a8f294b02aad1d3ddec82f3fd45d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-unzip@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "decompress-unzip@npm:4.0.1"
|
||||
dependencies:
|
||||
file-type: ^3.8.0
|
||||
get-stream: ^2.2.0
|
||||
pify: ^2.3.0
|
||||
yauzl: ^2.4.2
|
||||
checksum: ba9f3204ab2415bedb18d796244928a18148ef40dbb15174d0d01e5991b39536b03d02800a8a389515a1523f8fb13efc7cd44697df758cd06c674879caefd62b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress@npm:4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "decompress@npm:4.2.1"
|
||||
dependencies:
|
||||
decompress-tar: ^4.0.0
|
||||
decompress-tarbz2: ^4.0.0
|
||||
decompress-targz: ^4.0.0
|
||||
decompress-unzip: ^4.0.1
|
||||
graceful-fs: ^4.1.10
|
||||
make-dir: ^1.0.0
|
||||
pify: ^2.3.0
|
||||
strip-dirs: ^2.0.0
|
||||
checksum: 8247a31c6db7178413715fdfb35a482f019c81dfcd6e8e623d9f0382c9889ce797ce0144de016b256ed03298907a620ce81387cca0e69067a933470081436cb8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dedent@npm:^0.7.0":
|
||||
version: 0.7.0
|
||||
resolution: "dedent@npm:0.7.0"
|
||||
|
@ -6080,15 +6129,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"duplexer2@npm:~0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "duplexer2@npm:0.1.4"
|
||||
dependencies:
|
||||
readable-stream: ^2.0.2
|
||||
checksum: 744961f03c7f54313f90555ac20284a3fb7bf22fdff6538f041a86c22499560eb6eac9d30ab5768054137cb40e6b18b40f621094e0261d7d8c35a37b7a5ad241
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"duplexer@npm:~0.1.1":
|
||||
version: 0.1.2
|
||||
resolution: "duplexer@npm:0.1.2"
|
||||
|
@ -7531,6 +7571,27 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-type@npm:^3.8.0":
|
||||
version: 3.9.0
|
||||
resolution: "file-type@npm:3.9.0"
|
||||
checksum: 1db70b2485ac77c4edb4b8753c1874ee6194123533f43c2651820f96b518f505fa570b093fedd6672eb105ba9fb89c62f84b6492e46788e39c3447aed37afa2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-type@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "file-type@npm:5.2.0"
|
||||
checksum: b2b21c7fc3cfb3c6a3a18b0d5d7233b74d8c17d82757655766573951daf42962a5c809e5fc3637675b237c558ebc67e4958fb2cc5a4ad407bc545aaa40001c74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-type@npm:^6.1.0":
|
||||
version: 6.2.0
|
||||
resolution: "file-type@npm:6.2.0"
|
||||
checksum: 749540cefcd4959121eb83e373ed84e49b2e5a510aa5d598b725bd772dd306ae41fd00d3162ae3f6563b4db5cfafbbd0df321de3f20c17e20a8c56431ae55e58
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"filelist@npm:^1.0.1":
|
||||
version: 1.0.4
|
||||
resolution: "filelist@npm:1.0.4"
|
||||
|
@ -7954,18 +8015,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fstream@npm:^1.0.12":
|
||||
version: 1.0.12
|
||||
resolution: "fstream@npm:1.0.12"
|
||||
dependencies:
|
||||
graceful-fs: ^4.1.2
|
||||
inherits: ~2.0.0
|
||||
mkdirp: ">=0.5 0"
|
||||
rimraf: 2
|
||||
checksum: e6998651aeb85fd0f0a8a68cec4d05a3ada685ecc4e3f56e0d063d0564a4fc39ad11a856f9020f926daf869fc67f7a90e891def5d48e4cadab875dc313094536
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"function-bind@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "function-bind@npm:1.1.1"
|
||||
|
@ -8087,6 +8136,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-stream@npm:^2.2.0":
|
||||
version: 2.3.1
|
||||
resolution: "get-stream@npm:2.3.1"
|
||||
dependencies:
|
||||
object-assign: ^4.0.1
|
||||
pinkie-promise: ^2.0.0
|
||||
checksum: d82c86556e131ba7bef00233aa0aa7a51230e6deac11a971ce0f47cd43e2a5e968a3e3914cd082f07cd0d69425653b2f96735b0a7d5c5c03fef3ab857a531367
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-stream@npm:^5.0.0, get-stream@npm:^5.1.0":
|
||||
version: 5.2.0
|
||||
resolution: "get-stream@npm:5.2.0"
|
||||
|
@ -8395,6 +8454,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graceful-fs@npm:^4.1.10":
|
||||
version: 4.2.11
|
||||
resolution: "graceful-fs@npm:4.2.11"
|
||||
checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graceful-fs@npm:^4.2.0":
|
||||
version: 4.2.8
|
||||
resolution: "graceful-fs@npm:4.2.8"
|
||||
|
@ -8402,7 +8468,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
|
||||
"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
|
||||
version: 4.2.10
|
||||
resolution: "graceful-fs@npm:4.2.10"
|
||||
checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da
|
||||
|
@ -9063,7 +9129,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.0, inherits@npm:~2.0.3":
|
||||
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1
|
||||
|
@ -9494,6 +9560,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-natural-number@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "is-natural-number@npm:4.0.1"
|
||||
checksum: 3e5e3d52e0dfa4fea923b5d2b8a5cdbd9bf110c4598d30304b98528b02f40c9058a2abf1bae10bcbaf2bac18ace41cff7bc9673aff339f8c8297fae74ae0e75d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-negated-glob@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "is-negated-glob@npm:1.0.0"
|
||||
|
@ -9619,6 +9692,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "is-stream@npm:1.1.0"
|
||||
checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "is-stream@npm:2.0.0"
|
||||
|
@ -11183,13 +11263,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listenercount@npm:~1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "listenercount@npm:1.0.1"
|
||||
checksum: 0f1c9077cdaf2ebc16473c7d72eb7de6d983898ca42500f03da63c3914b6b312dd5f7a90d2657691ea25adf3fe0ac5a43226e8b2c673fd73415ed038041f4757
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listr2@npm:^3.8.3":
|
||||
version: 3.11.0
|
||||
resolution: "listr2@npm:3.11.0"
|
||||
|
@ -11502,6 +11575,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^1.0.0":
|
||||
version: 1.3.0
|
||||
resolution: "make-dir@npm:1.3.0"
|
||||
dependencies:
|
||||
pify: ^3.0.0
|
||||
checksum: c564f6e7bb5ace1c02ad56b3a5f5e07d074af0c0b693c55c7b2c2b148882827c8c2afc7b57e43338a9f90c125b58d604e8cf3e6990a48bf949dfea8c79668c0b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "make-dir@npm:3.1.0"
|
||||
|
@ -11947,7 +12029,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp@npm:>=0.5 0, mkdirp@npm:^0.5.4":
|
||||
"mkdirp@npm:^0.5.4":
|
||||
version: 0.5.6
|
||||
resolution: "mkdirp@npm:0.5.6"
|
||||
dependencies:
|
||||
|
@ -13316,13 +13398,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pify@npm:^2.0.0, pify@npm:^2.2.0":
|
||||
"pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "pify@npm:2.3.0"
|
||||
checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pify@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "pify@npm:3.0.0"
|
||||
checksum: 6cdcbc3567d5c412450c53261a3f10991665d660961e06605decf4544a61a97a54fefe70a68d5c37080ff9d6f4cf51444c90198d1ba9f9309a6c0d6e9f5c4fde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pify@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "pify@npm:4.0.1"
|
||||
|
@ -14426,6 +14515,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^2.3.0":
|
||||
version: 2.3.8
|
||||
resolution: "readable-stream@npm:2.3.8"
|
||||
dependencies:
|
||||
core-util-is: ~1.0.0
|
||||
inherits: ~2.0.3
|
||||
isarray: ~1.0.0
|
||||
process-nextick-args: ~2.0.0
|
||||
safe-buffer: ~5.1.1
|
||||
string_decoder: ~1.1.1
|
||||
util-deprecate: ~1.0.1
|
||||
checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-web-to-node-stream@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "readable-web-to-node-stream@npm:3.0.2"
|
||||
|
@ -14922,17 +15026,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:2":
|
||||
version: 2.7.1
|
||||
resolution: "rimraf@npm:2.7.1"
|
||||
dependencies:
|
||||
glob: ^7.1.3
|
||||
bin:
|
||||
rimraf: ./bin.js
|
||||
checksum: cdc7f6eacb17927f2a075117a823e1c5951792c6498ebcce81ca8203454a811d4cf8900314154d3259bb8f0b42ab17f67396a8694a54cae3283326e57ad250cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "rimraf@npm:3.0.2"
|
||||
|
@ -15002,7 +15095,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:5.2.1":
|
||||
"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.1.1":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491
|
||||
|
@ -15123,6 +15216,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"seek-bzip@npm:^1.0.5":
|
||||
version: 1.0.6
|
||||
resolution: "seek-bzip@npm:1.0.6"
|
||||
dependencies:
|
||||
commander: ^2.8.1
|
||||
bin:
|
||||
seek-bunzip: bin/seek-bunzip
|
||||
seek-table: bin/seek-bzip-table
|
||||
checksum: c2ab3291e7085558499efd4e99d1466ee6782f6c4a4e4c417aa859e1cd2f5117fb3b5444f3d27c38ec5908c0f0312e2a0bc69dff087751f97b3921b5bde4f9ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver-greatest-satisfied-range@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "semver-greatest-satisfied-range@npm:1.1.0"
|
||||
|
@ -15211,7 +15316,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"setimmediate@npm:^1.0.5, setimmediate@npm:~1.0.4":
|
||||
"setimmediate@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "setimmediate@npm:1.0.5"
|
||||
checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd
|
||||
|
@ -15935,6 +16040,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-dirs@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "strip-dirs@npm:2.1.0"
|
||||
dependencies:
|
||||
is-natural-number: ^4.0.1
|
||||
checksum: 9465547d71d8819daa7a5c9d4d783289ed8eac72eb06bd687bed382ce62af8ab8e6ffbda229805f5d2e71acce2ca4915e781c94190d284994cbc0b7cdc8303cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-final-newline@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "strip-final-newline@npm:2.0.0"
|
||||
|
@ -16175,6 +16289,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^1.5.2":
|
||||
version: 1.6.2
|
||||
resolution: "tar-stream@npm:1.6.2"
|
||||
dependencies:
|
||||
bl: ^1.0.0
|
||||
buffer-alloc: ^1.2.0
|
||||
end-of-stream: ^1.0.0
|
||||
fs-constants: ^1.0.0
|
||||
readable-stream: ^2.3.0
|
||||
to-buffer: ^1.1.1
|
||||
xtend: ^4.0.0
|
||||
checksum: a5d49e232d3e33321bbd150381b6a4e5046bf12b1c2618acb95435b7871efde4d98bd1891eb2200478a7142ef7e304e033eb29bbcbc90451a2cdfa1890e05245
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "tar-stream@npm:2.2.0"
|
||||
|
@ -16372,6 +16501,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-buffer@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "to-buffer@npm:1.1.1"
|
||||
checksum: 6c897f58c2bdd8b8b1645ea515297732fec6dafb089bf36d12370c102ff5d64abf2be9410e0b1b7cfc707bada22d9a4084558010bfc78dd7023748dc5dd9a1ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-fast-properties@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "to-fast-properties@npm:2.0.0"
|
||||
|
@ -16506,13 +16642,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"traverse@npm:>=0.3.0 <0.4":
|
||||
version: 0.3.9
|
||||
resolution: "traverse@npm:0.3.9"
|
||||
checksum: 982982e4e249e9bbf063732a41fe5595939892758524bbef5d547c67cdf371b13af72b5434c6a61d88d4bb4351d6dabc6e22d832e0d16bc1bc684ef97a1cc59e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"trim-newlines@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "trim-newlines@npm:3.0.1"
|
||||
|
@ -16964,6 +17093,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unbzip2-stream@npm:^1.0.9":
|
||||
version: 1.4.3
|
||||
resolution: "unbzip2-stream@npm:1.4.3"
|
||||
dependencies:
|
||||
buffer: ^5.2.1
|
||||
through: ^2.3.8
|
||||
checksum: 0e67c4a91f4fa0fc7b4045f8b914d3498c2fc2e8c39c359977708ec85ac6d6029840e97f508675fdbdf21fcb8d276ca502043406f3682b70f075e69aae626d1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unc-path-regex@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "unc-path-regex@npm:0.1.2"
|
||||
|
@ -17111,24 +17250,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unzipper@npm:0.10.11":
|
||||
version: 0.10.11
|
||||
resolution: "unzipper@npm:0.10.11"
|
||||
dependencies:
|
||||
big-integer: ^1.6.17
|
||||
binary: ~0.3.0
|
||||
bluebird: ~3.4.1
|
||||
buffer-indexof-polyfill: ~1.0.0
|
||||
duplexer2: ~0.1.4
|
||||
fstream: ^1.0.12
|
||||
graceful-fs: ^4.2.2
|
||||
listenercount: ~1.0.1
|
||||
readable-stream: ~2.3.6
|
||||
setimmediate: ~1.0.4
|
||||
checksum: 006cd43ec4d6df47d86aa6b15044a606f50cdcd6a3d6f96f64f54ca0b663c09abb221f76edca0e9592511036d37ea094b1d76ce92c5bf10d7c6eb56f0be678f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"update-browserslist-db@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "update-browserslist-db@npm:1.0.5"
|
||||
|
@ -18066,7 +18187,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yauzl@npm:^2.10.0":
|
||||
"yauzl@npm:^2.10.0, yauzl@npm:^2.4.2":
|
||||
version: 2.10.0
|
||||
resolution: "yauzl@npm:2.10.0"
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue