Compare commits

...

31 Commits

Author SHA1 Message Date
vib 2cf288a9fb Merge tag 'v13.0.0-preview6' into snug.moe 2023-07-02 12:19:32 +03:00
Johann150 77358c8f4b
13.0.0-preview6 2023-07-02 11:13:19 +02:00
Johann150 62cd1e7ed6 Translated using Weblate (German)
Currently translated at 100.0% (1206 of 1206 strings)

Co-authored-by: Johann <johann@qwertqwefsday.eu>
Translate-URL: http://translate.akkoma.dev/projects/foundkey/foundkey/de/
Translation: Foundkey/foundkey
2023-07-02 09:11:00 +00:00
Johann150 796adc8599
trigger side effects after updating note 2023-07-02 10:06:54 +02:00
Johann150 ff8b9b6651
refactor note creation side effects
Refactoring the side effects in this way should allow them to be
reused for updating notes as well.
2023-07-02 10:06:53 +02:00
Johann150 c1268c04f8
server: add noteStream handling for updated notes 2023-07-02 10:06:53 +02:00
Johann150 42b555e5e4
activitypub: handle incoming Update Note activities
Changelog: Added
2023-07-02 10:06:53 +02:00
Johann150 5c3e7c132a
add function to update a note 2023-07-02 10:06:53 +02:00
Johann150 76c8e6b11b
client: display note update time 2023-07-02 10:06:52 +02:00
Johann150 ca24080596
add update timestamp & notification 2023-07-02 10:06:38 +02:00
Johann150 a12debb7b6
server: replace unzipper with decompress
The unzipper package did not seem to work any more and was
mangling the meta.json file in its extracted form and potentially
other files which lead to the emoji import not working properly.

Changelog: Fixed
2023-07-02 00:09:45 +02:00
Johann150 f760426142
fix internal download in emoji import
Changelog: Fixed
2023-07-02 00:09:35 +02:00
Johann150 2f30af1812
server: fix instance actor creation
Because findBy returns an array which is always truthy, this would
mean the user is not actually created as requested and instead an
empty array is returned.
2023-06-29 21:21:26 +02:00
Johann150 2d46cf7c1e
fixup! client: fix MFM overflow
Turning the MFM render container into a div changes it to display
as a block which messes up rendering in some places, e.g. when
it is used to render user names in "Renoted by".
2023-06-27 22:26:13 +02:00
Johann150 2ea6daaf7a
rename extractDbHost to extractPunyHost 2023-06-27 22:02:32 +02:00
Johann150 597de07465
server: refactor HTTP signature validation 2023-06-27 21:46:00 +02:00
Johann150 9289b0e8ed
adjust config file example 2023-06-25 20:44:08 +02:00
Hélène b600efae0d
BREAKING: activitypub: validate fetch signatures
Enforces HTTP signatures on object fetches, and rejects fetches from blocked
instances. This should mean proper and full blocking of remote instances.

This is now default behavior, which makes it a breaking change. To disable
it (mostly for development purposes), the configuration item
`allowUnsignedFetches` can be set to true. It is not the default for
development environments as it is important to have as close as possible
behavior to real environments for ActivityPub development.

Co-authored-by: nullobsi <me@nullob.si>
Co-authored-by: Norm <normandy@biribiri.dev>
Changelog: Added
2023-06-25 20:42:14 +02:00
Johann150 ecca5a164e
client: always forbid MFM overflow
Some MFM overlays UI components. This should remove any possibility that
rendered MFM escapes a Note's body.

Changelog: Changed
2023-06-25 18:41:08 +02:00
Johann150 1125a623a7
Revert "client: fix MFM overflow"
This reverts commit f7904a240a.

Changelog: Fixed
2023-06-25 18:40:48 +02:00
Johann150 51a319e8ca
use extractDbHost 2023-06-23 22:00:31 +02:00
Johann150 f7904a240a
client: fix MFM overflow
closes FoundKeyGang/FoundKey#397

Changelog: Fixed
2023-06-21 23:25:16 +02:00
Johann150 693dd3ad97
remove unused parameter from MFM component 2023-06-21 22:26:39 +02:00
Johann150 777b981bf1
client: disable sound for received note by default
Lessen the sound pollution.

closes FoundKeyGang/FoundKey#394

Changelog: Changed
Co-authored-by: Jeder <jeder+git@jeder.pl>
2023-06-09 17:58:44 +02:00
Norm bbd35054e6
change my name in mailmap 2023-06-07 16:20:52 -04:00
kazari e9862d480c Translated using Weblate (Japanese)
Currently translated at 99.8% (1203 of 1205 strings)

Co-authored-by: kazari <6c577a54-aac9-482a-955e-745c858445e3@simplelogin.com>
Translate-URL: http://translate.akkoma.dev/projects/foundkey/foundkey/ja/
Translation: Foundkey/foundkey
2023-06-05 21:43:27 +00:00
Johann150 cc0915775b
server: add webhook stat to nodeinfo
This will show the number of active web hooks in the node info.
This is desired to be able to gauge webhook usage in Foundkey.

Changelog: Added
2023-06-05 23:39:43 +02:00
Johann150 f181a8805d
docs: remove bannerColor
This is a fixup for commit a673647fba.
2023-06-05 23:39:43 +02:00
Johann150 ac482e6eec
fix lockfile 2023-06-01 23:24:11 +02:00
Johann150 38786b6999
transform tests from ts to js
This allows to get rid of the special loader for ts files. There is
no need for the test files to be written in TypeScript, plain
JavaScript should be fine for this purpose.
2023-06-01 23:21:03 +02:00
Jeder 680d1f1459
docker: only publish port on localhost
Changelog: Changed
2023-05-31 15:19:10 -04:00
94 changed files with 1457 additions and 1090 deletions

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ services:
- redis
# - es
ports:
- "3000:3000"
- "127.0.0.1:3000:3000"
networks:
- internal_network
- external_network

View File

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

View File

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

View File

@ -748,6 +748,7 @@ _ffVisibility:
followers: "フォロワーだけに公開"
private: "非公開"
nobody: 誰にも見せない (あなたにさえも)
_signup:
almostThere: "ほとんど完了です"
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"

View File

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

View File

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

View File

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

View File

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

View File

@ -61,6 +61,7 @@ export function loadConfig(): Config {
proxyRemoteFiles: false,
maxFileSize: 262144000, // 250 MiB
maxNoteTextLength: 3000,
allowUnsignedFetches: false,
}, config);
mixin.version = meta.version;

View File

@ -68,6 +68,8 @@ export type Source = {
notFound?: string;
error?: string;
};
allowUnsignedFetches?: boolean;
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`);
@ -93,7 +95,7 @@ async function processContent(actor: IRemoteUser, note: IPost, quoteUri: string
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[];
});
@ -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);
}

View File

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

View File

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

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

View File

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

View File

@ -36,6 +36,5 @@ export default async (ctx: Router.RouterContext) => {
);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,6 +128,9 @@ export interface NoteStreamTypes {
reaction: string;
userId: User['id'];
};
updated: {
note: Note;
};
}
type NoteStreamEventTypes = {
[key in keyof NoteStreamTypes]: {

View File

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

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

View File

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

View File

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

View 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,
};
}

View File

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

View File

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

View File

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

View File

@ -2,12 +2,12 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
before(async () => {
p = await startServer();
@ -20,48 +20,48 @@ describe('API visibility', function() {
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);
@ -412,21 +412,21 @@ describe('API visibility', function() {
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
@ -435,21 +435,21 @@ describe('API visibility', function() {
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
@ -458,14 +458,14 @@ describe('API visibility', function() {
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

View File

@ -2,15 +2,13 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
let p;
let alice, bob, carol;
before(async () => {
p = await startServer();

View File

@ -2,17 +2,15 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
// alice blocks bob
let alice: any;
let bob: any;
let carol: any;
let alice, bob, carol;
before(async () => {
p = await startServer();
@ -80,8 +78,8 @@ describe('Block', function() {
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);
}));
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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';
@ -19,10 +19,9 @@ const HTML = 'text/html; charset=utf-8';
describe('Fetch resource', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
let alice: any;
let alicesPost: any;
let alice, alicesPost;
before(async () => {
p = await startServer();

View File

@ -2,16 +2,14 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
let alice: any;
let bob: any;
let follower: any;
let alice, bob, follower;
before(async () => {
p = await startServer();

View File

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

View File

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

View File

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

View File

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

View File

@ -2,17 +2,15 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
// alice mutes carol
let alice: any;
let bob: any;
let carol: any;
let alice, bob, carol;
before(async () => {
p = await startServer();
@ -41,8 +39,8 @@ describe('Mute', function() {
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', function() {
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', function() {
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', function() {
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);
}));
});
});

View File

@ -2,17 +2,16 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let Notes: any;
let p;
let Notes;
let alice: any;
let bob: any;
let alice, bob;
before(async () => {
p = await startServer();

View File

@ -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', () => {

View File

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

View File

@ -3,17 +3,15 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
// alice blocks bob
let alice: any;
let bob: any;
let carol: any;
let alice, bob, carol;
before(async () => {
await initTestDb();
@ -34,10 +32,10 @@ describe('Creating a block activity', function() {
});
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', function() {
}));
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);

View File

@ -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(),
@ -28,16 +28,12 @@ describe('Streaming', () => {
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 () => {
p = await startServer();
@ -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,7 +402,7 @@ describe('Streaming', () => {
});
}));
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 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;

View File

@ -2,16 +2,14 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
let alice: any;
let bob: any;
let carol: any;
let alice, bob, carol;
before(async () => {
p = await startServer();
@ -37,9 +35,9 @@ describe('Note thread mute', function() {
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', function() {
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の投稿はスレッドミュート前に行われたため通知に含まれていてもよい
}));

View File

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

View File

@ -2,17 +2,14 @@ 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', function() {
this.timeout(20*60*1000);
let p: childProcess.ChildProcess;
let p;
let alice: any;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
let alice, jpgNote, pngNote, jpgPngNote;
before(async () => {
p = await startServer();
@ -43,8 +40,8 @@ describe('users/notes', function() {
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', function() {
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);
}));
});

View File

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

View File

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

View File

@ -6,7 +6,7 @@
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :custom-emojis="note.emojis"/>
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -142,6 +142,12 @@ export type NoteUpdatedEvent = {
choice: number;
userId: User['id'];
};
} | {
id: Note['id'];
type: 'updated';
body: {
note: Note;
};
};
export type BroadcastEvents = {

View File

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

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