Merge branch 'develop' into renovate/lock-file-maintenance

This commit is contained in:
naskya 2024-07-21 06:01:17 +09:00
commit 629a62fa0a
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
48 changed files with 460 additions and 6788 deletions

181
Cargo.lock generated
View file

@ -235,6 +235,7 @@ dependencies = [
"url", "url",
"urlencoding", "urlencoding",
"web-push", "web-push",
"zhconv",
] ]
[[package]] [[package]]
@ -499,6 +500,16 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.6.2" version = "0.6.2"
@ -680,6 +691,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "daachorse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b7ef7a4be509357f4804d0a22e830daddb48f19fd604e4ad32ddce04a94c36"
[[package]] [[package]]
name = "der" name = "der"
version = "0.4.5" version = "0.4.5"
@ -1187,6 +1204,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.12.4" version = "0.12.4"
@ -1517,6 +1540,8 @@ dependencies = [
"mime", "mime",
"once_cell", "once_cell",
"polling", "polling",
"serde",
"serde_json",
"slab", "slab",
"sluice", "sluice",
"tracing", "tracing",
@ -1525,6 +1550,15 @@ dependencies = [
"waker-fn", "waker-fn",
] ]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -2037,6 +2071,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.1" version = "0.36.1"
@ -2529,7 +2572,7 @@ dependencies = [
"built", "built",
"cfg-if", "cfg-if",
"interpolate_name", "interpolate_name",
"itertools", "itertools 0.12.1",
"libc", "libc",
"libfuzzer-sys", "libfuzzer-sys",
"log", "log",
@ -2797,6 +2840,23 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ruzstd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc"
dependencies = [
"byteorder",
"thiserror-core",
"twox-hash",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -2859,7 +2919,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"strum", "strum 0.25.0",
"thiserror", "thiserror",
"time", "time",
"tracing", "tracing",
@ -3380,12 +3440,34 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.25.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -3491,6 +3573,26 @@ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]]
name = "thiserror-core"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c001ee18b7e5e3f62cbf58c7fe220119e68d902bb7443179c0c8aef30090e999"
dependencies = [
"thiserror-core-impl",
]
[[package]]
name = "thiserror-core-impl"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.71",
]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.63" version = "1.0.63"
@ -3530,7 +3632,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa",
"libc",
"num-conv", "num-conv",
"num_threads",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@ -3732,6 +3837,16 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"static_assertions",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@ -3850,6 +3965,18 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "8.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566"
dependencies = [
"anyhow",
"cfg-if",
"rustversion",
"time",
]
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.0"
@ -4286,6 +4413,56 @@ dependencies = [
"syn 2.0.71", "syn 2.0.71",
] ]
[[package]]
name = "zhconv"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a5764e8c3c48dce7dd281cdae65c785536d1da3078b484c2254e7bea7b42323"
dependencies = [
"console_error_panic_hook",
"daachorse",
"hex-literal",
"itertools 0.10.5",
"lazy_static",
"once_cell",
"regex",
"ruzstd",
"sha2",
"strum 0.24.1",
"vergen",
"wasm-bindgen",
"zstd",
]
[[package]]
name = "zstd"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "6.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.12+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.4.12" version = "0.4.12"

View file

@ -49,10 +49,11 @@ tracing-subscriber = { version = "0.3.18", default-features = false }
url = { version = "2.5.2", default-features = false } url = { version = "2.5.2", default-features = false }
urlencoding = { version = "2.1.3", default-features = false } urlencoding = { version = "2.1.3", default-features = false }
web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656", default-features = false } web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656", default-features = false }
zhconv = "0.3.1"
# subdependencies # subdependencies
## explicitly list OpenSSL to use the vendored version ## explicitly list OpenSSL to use the vendored version
openssl = "0.10.64" openssl = "0.10.65"
## some subdependencies require higher Rust version than 1.74 (our MSRV) ## some subdependencies require higher Rust version than 1.74 (our MSRV)
## cargo update && cargo update ravif --precise 0.11.5 && cargo update bitstream-io --precise 2.3.0 ## cargo update && cargo update ravif --precise 0.11.5 && cargo update bitstream-io --precise 2.3.0

View file

@ -29,7 +29,7 @@ cuid2 = { workspace = true }
emojis = { workspace = true } emojis = { workspace = true }
idna = { workspace = true, features = ["std", "compiled_data"] } idna = { workspace = true, features = ["std", "compiled_data"] }
image = { workspace = true, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "tiff", "webp"] } image = { workspace = true, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "tiff", "webp"] }
isahc = { workspace = true, features = ["http2", "text-decoding"] } isahc = { workspace = true, features = ["http2", "text-decoding", "json"] }
nom-exif = { workspace = true } nom-exif = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
openssl = { workspace = true, features = ["vendored"] } openssl = { workspace = true, features = ["vendored"] }
@ -49,6 +49,7 @@ tracing-subscriber = { workspace = true, features = ["ansi"] }
url = { workspace = true } url = { workspace = true }
urlencoding = { workspace = true } urlencoding = { workspace = true }
web-push = { workspace = true, features = ["isahc-client"] } web-push = { workspace = true, features = ["isahc-client"] }
zhconv = { workspace = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true, features = ["std"] } pretty_assertions = { workspace = true, features = ["std"] }

View file

@ -1349,6 +1349,13 @@ export declare function toDbReaction(reaction?: string | undefined | null, host?
export declare function toPuny(host: string): string export declare function toPuny(host: string): string
export declare function translate(text: string, sourceLang: string | undefined | null, targetLang: string): Promise<Translation>
export interface Translation {
sourceLang: string
text: string
}
export declare function unwatchNote(watcherId: string, noteId: string): Promise<void> export declare function unwatchNote(watcherId: string, noteId: string): Promise<void>
export declare function updateAntennaCache(): Promise<void> export declare function updateAntennaCache(): Promise<void>

View file

@ -443,6 +443,7 @@ module.exports.storageUsage = nativeBinding.storageUsage
module.exports.stringToAcct = nativeBinding.stringToAcct module.exports.stringToAcct = nativeBinding.stringToAcct
module.exports.toDbReaction = nativeBinding.toDbReaction module.exports.toDbReaction = nativeBinding.toDbReaction
module.exports.toPuny = nativeBinding.toPuny module.exports.toPuny = nativeBinding.toPuny
module.exports.translate = nativeBinding.translate
module.exports.unwatchNote = nativeBinding.unwatchNote module.exports.unwatchNote = nativeBinding.unwatchNote
module.exports.updateAntennaCache = nativeBinding.updateAntennaCache module.exports.updateAntennaCache = nativeBinding.updateAntennaCache
module.exports.updateAntennasOnNewNote = nativeBinding.updateAntennasOnNewNote module.exports.updateAntennasOnNewNote = nativeBinding.updateAntennasOnNewNote

View file

@ -17,4 +17,5 @@ pub mod reaction;
pub mod remove_old_attestation_challenges; pub mod remove_old_attestation_challenges;
pub mod should_nyaify; pub mod should_nyaify;
pub mod system_info; pub mod system_info;
pub mod translate;
pub mod user; pub mod user;

View file

@ -0,0 +1,243 @@
use crate::{
config::{local_server_info, server, CONFIG},
util::http_client,
};
#[macros::errors]
pub enum Error {
#[doc = "database error"]
#[error(transparent)]
Db(#[from] sea_orm::DbErr),
#[error("failed to acquire an HTTP client")]
HttpClient(#[from] http_client::Error),
#[error("invalid http request body")]
InvalidRequestBody(#[from] isahc::http::Error),
#[error("http request failed")]
HttpRequest(#[from] isahc::Error),
#[error("failed to serialize the request body")]
Serialize(#[from] serde_json::Error),
#[error("Libretranslate API url is not set")]
MissingApiUrl,
#[error("DeepL API key is not set")]
MissingApiKey,
#[error("no response")]
NoResponse,
#[error("translator is not set")]
NoTranslator,
}
#[macros::export(object)]
pub struct Translation {
pub source_lang: String,
pub text: String,
}
#[macros::export]
pub async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
) -> Result<Translation, Error> {
let config = local_server_info().await?;
let mut translation = if let Some(api_key) = config.deepl_auth_key {
deepl_translate::translate(
text,
source_lang,
target_lang,
&api_key,
config.deepl_is_pro,
)
.await?
} else if let Some(api_url) = config.libre_translate_api_url {
libre_translate::translate(
text,
source_lang,
target_lang,
&api_url,
config.libre_translate_api_key.as_deref(),
)
.await?
} else if let Some(server::DeepLConfig {
auth_key, is_pro, ..
}) = CONFIG.deepl.as_ref()
{
deepl_translate::translate(
text,
source_lang,
target_lang,
auth_key.as_ref().ok_or(Error::MissingApiKey)?,
is_pro.unwrap_or(false),
)
.await?
} else if let Some(server::LibreTranslateConfig {
api_url, api_key, ..
}) = CONFIG.libre_translate.as_ref()
{
libre_translate::translate(
text,
source_lang,
target_lang,
api_url.as_ref().ok_or(Error::MissingApiUrl)?,
api_key.as_deref(),
)
.await?
} else {
return Err(Error::NoTranslator);
};
// DeepL translate and LibreTranslate don't provide zh-Hant-TW translations,
// so we convert zh-Hans-CN translations into zh-Hant-TW using zhconv.
if ["zh-tw", "zh-hant", "zh-hant-tw"].contains(&target_lang.to_ascii_lowercase().as_str()) {
translation.text = zhconv::zhconv(&translation.text, zhconv::Variant::ZhTW)
}
Ok(translation)
}
mod deepl_translate {
use crate::util::http_client;
use isahc::{AsyncReadResponseExt, Request};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Response {
translations: Vec<Translation>,
}
#[derive(Deserialize, Clone)]
struct Translation {
detected_source_language: Option<String>,
text: String,
}
pub(super) async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
api_key: &str,
is_pro: bool,
) -> Result<super::Translation, super::Error> {
let client = http_client::client()?;
let api_url = if is_pro {
"https://api.deepl.com/v2/translate"
} else {
"https://api-free.deepl.com/v2/translate"
};
let mut target_lang = target_lang.split('-').collect::<Vec<&str>>()[0];
// DeepL API requires us to specify "en-US" or "en-GB" for English
// translations ("en" does not work), so we need to address it
if target_lang == "en" {
target_lang = "en-US";
}
let body = if let Some(source_lang) = source_lang {
let source_lang = source_lang.split('-').collect::<Vec<&str>>()[0];
json!({
"text": [text],
"source_lang": source_lang,
"target_lang": target_lang
})
} else {
json!({
"text": [text],
"target_lang": target_lang
})
};
let request = Request::post(api_url)
.header("Authorization", format!("DeepL-Auth-Key {}", api_key))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&body)?)?;
let response = client.send_async(request).await?.json::<Response>().await?;
let result = response
.translations
.first()
.ok_or(super::Error::NoResponse)?
.to_owned();
Ok(super::Translation {
source_lang: source_lang
.map(|s| s.to_owned())
.or(result.detected_source_language)
.unwrap_or_else(|| "unknown".to_owned()),
text: result.text,
})
}
}
mod libre_translate {
use crate::util::http_client;
use isahc::{AsyncReadResponseExt, Request};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct Translation {
translated_text: String,
detected_language: DetectedLanguage,
}
#[derive(Deserialize, Clone)]
struct DetectedLanguage {
language: String,
}
pub(super) async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
api_url: &str,
api_key: Option<&str>,
) -> Result<super::Translation, super::Error> {
let client = http_client::client()?;
let target_lang = target_lang.split('-').collect::<Vec<&str>>()[0];
let body = if let Some(source_lang) = source_lang {
let source_lang = source_lang.split('-').collect::<Vec<&str>>()[0];
json!({
"q": [text],
"source": source_lang,
"target": target_lang,
"format": "text",
"alternatives": 1,
"api_key": api_key.unwrap_or_default()
})
} else {
json!({
"q": [text],
"source": "auto",
"target": target_lang,
"format": "text",
"alternatives": 1,
"api_key": api_key.unwrap_or_default()
})
};
let request = Request::post(api_url)
.header("Content-Type", "application/json")
.body(serde_json::to_string(&body)?)?;
let result = client
.send_async(request)
.await?
.json::<Translation>()
.await?;
Ok(super::Translation {
source_lang: source_lang
.map(|s| s.to_owned())
.unwrap_or(result.detected_language.language),
text: result.translated_text,
})
}
}

View file

@ -1,10 +0,0 @@
{
"extension": ["ts","js","cjs","mjs"],
"node-option": [
"experimental-specifier-resolution=node",
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 30000,
"exit": true
}

View file

@ -1,10 +0,0 @@
import { loadConfig } from "./built/config.js";
import { createRedisConnection } from "./built/redis.js";
const config = loadConfig();
const redis = createRedisConnection(config);
redis.on("connect", () => redis.disconnect());
redis.on("error", (e) => {
throw e;
});

View file

@ -10,13 +10,10 @@
"migration:run": "typeorm migration:run --dataSource ./built/ormconfig.js", "migration:run": "typeorm migration:run --dataSource ./built/ormconfig.js",
"migration:revert": "typeorm migration:revert --dataSource ./built/ormconfig.js", "migration:revert": "typeorm migration:revert --dataSource ./built/ormconfig.js",
"migration:new": "pnpm node ./scripts/create-migration.mjs", "migration:new": "pnpm node ./scripts/create-migration.mjs",
"check:connect": "node ./check_connect.js",
"build": "pnpm tsc --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json", "build": "pnpm tsc --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json",
"build:debug": "pnpm tsc --sourceMap --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json", "build:debug": "pnpm tsc --sourceMap --project tsconfig.json ; pnpm tsc-alias --project tsconfig.json",
"watch": "pnpm tsc --project tsconfig.json --watch ; pnpm tsc-alias --project tsconfig.json --watch", "watch": "pnpm tsc --project tsconfig.json --watch ; pnpm tsc-alias --project tsconfig.json --watch",
"lint": "pnpm biome check --write **/*.ts", "lint": "pnpm biome check --write **/*.ts",
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "pnpm run mocha",
"format": "pnpm biome format * --write" "format": "pnpm biome format * --write"
}, },
"dependencies": { "dependencies": {
@ -50,7 +47,6 @@
"date-fns": "3.6.0", "date-fns": "3.6.0",
"decompress": "4.2.1", "decompress": "4.2.1",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"deepl-node": "1.13.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.2.0", "file-type": "19.2.0",
@ -84,7 +80,6 @@
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"opencc-js": "1.0.5",
"otpauth": "9.3.1", "otpauth": "9.3.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.12.0", "pg": "8.12.0",
@ -140,7 +135,6 @@
"@types/koa__cors": "5.0.0", "@types/koa__cors": "5.0.0",
"@types/koa__multer": "2.0.7", "@types/koa__multer": "2.0.7",
"@types/koa__router": "12.0.4", "@types/koa__router": "12.0.4",
"@types/mocha": "10.0.7",
"@types/node": "20.14.11", "@types/node": "20.14.11",
"@types/node-fetch": "2.6.11", "@types/node-fetch": "2.6.11",
"@types/nodemailer": "6.4.15", "@types/nodemailer": "6.4.15",
@ -165,7 +159,6 @@
"@types/websocket": "1.0.10", "@types/websocket": "1.0.10",
"@types/ws": "8.5.11", "@types/ws": "8.5.11",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"mocha": "10.6.0",
"pug": "3.0.3", "pug": "3.0.3",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"ts-loader": "9.5.1", "ts-loader": "9.5.1",

View file

@ -1,93 +0,0 @@
import fetch from "node-fetch";
import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "backend-rs";
import type { PostLanguage } from "firefish-js";
import * as deepl from "deepl-node";
// DeepL translate and LibreTranslate don't provide
// zh-Hant-TW translations, so we convert zh-Hans-CN
// translations into zh-Hant-TW using opencc-js.
function convertChinese(convert: boolean, src: string) {
if (!convert) return src;
const converter = Converter({ from: "cn", to: "twp" });
return converter(src);
}
function stem(lang: PostLanguage): string {
let toReturn = lang as string;
if (toReturn.includes("-")) toReturn = toReturn.split("-")[0];
if (toReturn.includes("_")) toReturn = toReturn.split("_")[0];
return toReturn;
}
export async function translate(
text: string,
from: PostLanguage | null,
to: PostLanguage,
) {
const instance = await fetchMeta();
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
throw Error("No translator is set up on this server.");
}
const source = from == null ? null : stem(from);
const target = stem(to);
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: text,
source: source ?? "auto",
target,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: source ?? json.detectedLanguage?.language,
text: convertChinese(
["zh-hant", "zh-TW"].includes(to),
json.translatedText,
),
};
}
const deeplTranslator = new deepl.Translator(instance.deeplAuthKey ?? "");
const result = await deeplTranslator.translateText(
text,
source as deepl.SourceLanguageCode | null,
// DeepL API requires us to specify "en-US" or "en-GB" for English
// translations ("en" does not work), so we need to address it
(target === "en" ? to : target) as deepl.TargetLanguageCode,
);
return {
sourceLang: source ?? result.detectedSourceLang,
text: convertChinese(["zh-hant", "zh-TW"].includes(to), result.text),
};
}

View file

@ -1,7 +1,6 @@
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
import { getNote } from "@/server/api/common/getters.js"; import { getNote } from "@/server/api/common/getters.js";
import { translate } from "@/misc/translate.js"; import { translate } from "backend-rs";
import type { PostLanguage } from "firefish-js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
export const meta = { export const meta = {
@ -47,7 +46,7 @@ export default define(meta, paramDef, async (ps, user) => {
return translate( return translate(
note.text, note.text,
note.lang as PostLanguage | null, note.lang as string | null,
ps.targetLang as PostLanguage, ps.targetLang,
); );
}); });

View file

@ -40,7 +40,7 @@ import {
getStubMastoContext, getStubMastoContext,
type MastoContext, type MastoContext,
} from "@/server/api/mastodon/index.js"; } from "@/server/api/mastodon/index.js";
import { translate } from "@/misc/translate.js"; import { translate } from "backend-rs";
import { createScheduledNoteJob } from "@/queue/index.js"; import { createScheduledNoteJob } from "@/queue/index.js";
export class NoteHelpers { export class NoteHelpers {

View file

@ -1,102 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import rndstr from "rndstr";
import { initDb } from "../src/db/postgre.js";
import { initTestDb } from "./utils.js";
describe("ActivityPub", () => {
before(async () => {
//await initTestDb();
await initDb();
});
describe("Parse minimum object", () => {
const host = "https://host1.test";
const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const actor = {
"@context": "https://www.w3.org/ns/activitystreams",
id: actorId,
type: "Person",
preferredUsername,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
const post = {
"@context": "https://www.w3.org/ns/activitystreams",
id: `${host}/users/${rndstr("0-9a-z", 8)}`,
type: "Note",
attributedTo: actor.id,
to: "https://www.w3.org/ns/activitystreams#Public",
content: "あ",
};
it("Minimum Actor", async () => {
const { MockResolver } = await import("./misc/mock-resolver.js");
const { createPerson } = await import(
"../src/remote/activitypub/models/person.js"
);
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.uri, actor.id);
assert.deepStrictEqual(user.username, actor.preferredUsername);
assert.deepStrictEqual(user.inbox, actor.inbox);
});
it("Minimum Note", async () => {
const { MockResolver } = await import("./misc/mock-resolver.js");
const { createNote } = await import(
"../src/remote/activitypub/models/note.js"
);
const resolver = new MockResolver();
resolver._register(actor.id, actor);
resolver._register(post.id, post);
const note = await createNote(post.id, resolver, true);
assert.deepStrictEqual(note?.uri, post.id);
assert.deepStrictEqual(note.visibility, "public");
assert.deepStrictEqual(note.text, post.content);
});
});
describe("Truncate long name", () => {
const host = "https://host1.test";
const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const name = rndstr("0-9a-z", 129);
const actor = {
"@context": "https://www.w3.org/ns/activitystreams",
id: actorId,
type: "Person",
preferredUsername,
name,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
it("Actor", async () => {
const { MockResolver } = await import("./misc/mock-resolver.js");
const { createPerson } = await import(
"../src/remote/activitypub/models/person.js"
);
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.name, actor.name.substr(0, 128));
});
});
});

View file

@ -1,75 +0,0 @@
import * as assert from "node:assert";
import httpSignature from "@peertube/http-signature";
import { genRsaKeyPair } from "../src/misc/gen-key-pair.js";
import {
createSignedGet,
createSignedPost,
} from "../src/remote/activitypub/ap-request.js";
export const buildParsedSignature = (
signingString: string,
signature: string,
algorithm: string,
) => {
return {
scheme: "Signature",
params: {
keyId: "KeyID", // dummy, not used for verify
algorithm: algorithm,
headers: ["(request-target)", "date", "host", "digest"], // dummy, not used for verify
signature: signature,
},
signingString: signingString,
algorithm: algorithm.toUpperCase(),
keyId: "KeyID", // dummy, not used for verify
};
};
describe("ap-request", () => {
it("createSignedPost with verify", async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: "x", privateKeyPem: keypair.privateKey };
const url = "https://example.com/inbox";
const activity = { a: 1 };
const body = JSON.stringify(activity);
const headers = {
"User-Agent": "UA",
};
const req = createSignedPost({
key,
url,
body,
additionalHeaders: headers,
});
const parsed = buildParsedSignature(
req.signingString,
req.signature,
"rsa-sha256",
);
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
it("createSignedGet with verify", async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: "x", privateKeyPem: keypair.privateKey };
const url = "https://example.com/outbox";
const headers = {
"User-Agent": "UA",
};
const req = createSignedGet({ key, url, additionalHeaders: headers });
const parsed = buildParsedSignature(
req.signingString,
req.signature,
"rsa-sha256",
);
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
});

View file

@ -1,535 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
post,
request,
shutdownServer,
signup,
startServer,
} from "./utils.js";
describe("API visibility", () => {
let p: childProcess.ChildProcess;
before(async () => {
p = await startServer();
});
after(async () => {
await shutdownServer(p);
});
describe("Note visibility", async () => {
//#region vars
/** ヒロイン */
let alice: any;
/** フォロワー */
let follower: any;
/** 非フォロワー */
let other: any;
/** 非フォロワーでもリプライやメンションをされた人 */
let target: any;
/** specified mentionでmentionを飛ばされる人 */
let target2: any;
/** public-post */
let pub: any;
/** home-post */
let home: any;
/** followers-post */
let fol: any;
/** specified-post */
let spe: any;
/** public-reply to target's post */
let pubR: any;
/** home-reply to target's post */
let homeR: any;
/** followers-reply to target's post */
let folR: any;
/** specified-reply to target's post */
let speR: any;
/** public-mention to target */
let pubM: any;
/** home-mention to target */
let homeM: any;
/** followers-mention to target */
let folM: any;
/** specified-mention to target */
let speM: any;
/** reply target post */
let tgt: any;
//#endregion
const show = async (noteId: any, by: any) => {
return await request(
"/notes/show",
{
noteId,
},
by,
);
};
before(async () => {
//#region prepare
// signup
alice = await signup({ username: "alice" });
follower = await signup({ username: "follower" });
other = await signup({ username: "other" });
target = await signup({ username: "target" });
target2 = await signup({ username: "target2" });
// follow alice <= follower
await request("/following/create", { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: "x", visibility: "public" });
home = await post(alice, { text: "x", visibility: "home" });
fol = await post(alice, { text: "x", visibility: "followers" });
spe = await post(alice, {
text: "x",
visibility: "specified",
visibleUserIds: [target.id],
});
// replies
tgt = await post(target, { text: "y", visibility: "public" });
pubR = await post(alice, {
text: "x",
replyId: tgt.id,
visibility: "public",
});
homeR = await post(alice, {
text: "x",
replyId: tgt.id,
visibility: "home",
});
folR = await post(alice, {
text: "x",
replyId: tgt.id,
visibility: "followers",
});
speR = await post(alice, {
text: "x",
replyId: tgt.id,
visibility: "specified",
});
// mentions
pubM = await post(alice, {
text: "@target x",
replyId: tgt.id,
visibility: "public",
});
homeM = await post(alice, {
text: "@target x",
replyId: tgt.id,
visibility: "home",
});
folM = await post(alice, {
text: "@target x",
replyId: tgt.id,
visibility: "followers",
});
speM = await post(alice, {
text: "@target2 x",
replyId: tgt.id,
visibility: "specified",
});
//#endregion
});
//#region show post
// public
it("[show] public-postを自分が見れる", async(async () => {
const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-postをフォロワーが見れる", async(async () => {
const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-postを非フォロワーが見れる", async(async () => {
const res = await show(pub.id, other);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-postを未認証が見れる", async(async () => {
const res = await show(pub.id, null);
assert.strictEqual(res.body.text, "x");
}));
// home
it("[show] home-postを自分が見れる", async(async () => {
const res = await show(home.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-postをフォロワーが見れる", async(async () => {
const res = await show(home.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-postを非フォロワーが見れる", async(async () => {
const res = await show(home.id, other);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-postを未認証が見れる", async(async () => {
const res = await show(home.id, null);
assert.strictEqual(res.body.text, "x");
}));
// followers
it("[show] followers-postを自分が見れる", async(async () => {
const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] followers-postをフォロワーが見れる", async(async () => {
const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] followers-postを非フォロワーが見れない", async(async () => {
const res = await show(fol.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] followers-postを未認証が見れない", async(async () => {
const res = await show(fol.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it("[show] specified-postを自分が見れる", async(async () => {
const res = await show(spe.id, alice);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-postを指定ユーザーが見れる", async(async () => {
const res = await show(spe.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] specified-postをフォロワーが見れない", async(async () => {
const res = await show(spe.id, follower);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-postを非フォロワーが見れない", async(async () => {
const res = await show(spe.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-postを未認証が見れない", async(async () => {
const res = await show(spe.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region show reply
// public
it("[show] public-replyを自分が見れる", async(async () => {
const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-replyをされた人が見れる", async(async () => {
const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-replyをフォロワーが見れる", async(async () => {
const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-replyを非フォロワーが見れる", async(async () => {
const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, "x");
}));
it("[show] public-replyを未認証が見れる", async(async () => {
const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, "x");
}));
// home
it("[show] home-replyを自分が見れる", async(async () => {
const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-replyをされた人が見れる", async(async () => {
const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-replyをフォロワーが見れる", async(async () => {
const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-replyを非フォロワーが見れる", async(async () => {
const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, "x");
}));
it("[show] home-replyを未認証が見れる", async(async () => {
const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, "x");
}));
// followers
it("[show] followers-replyを自分が見れる", async(async () => {
const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] followers-replyを非フォロワーでもリプライされていれば見れる", async(async () => {
const res = await show(folR.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] followers-replyをフォロワーが見れる", async(async () => {
const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, "x");
}));
it("[show] followers-replyを非フォロワーが見れない", async(async () => {
const res = await show(folR.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] followers-replyを未認証が見れない", async(async () => {
const res = await show(folR.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it("[show] specified-replyを自分が見れる", async(async () => {
const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, "x");
}));
it("[show] specified-replyを指定ユーザーが見れる", async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] specified-replyをされた人が指定されてなくても見れる", async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, "x");
}));
it("[show] specified-replyをフォロワーが見れない", async(async () => {
const res = await show(speR.id, follower);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-replyを非フォロワーが見れない", async(async () => {
const res = await show(speR.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-replyを未認証が見れない", async(async () => {
const res = await show(speR.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region show mention
// public
it("[show] public-mentionを自分が見れる", async(async () => {
const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] public-mentionをされた人が見れる", async(async () => {
const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] public-mentionをフォロワーが見れる", async(async () => {
const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] public-mentionを非フォロワーが見れる", async(async () => {
const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] public-mentionを未認証が見れる", async(async () => {
const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, "@target x");
}));
// home
it("[show] home-mentionを自分が見れる", async(async () => {
const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] home-mentionをされた人が見れる", async(async () => {
const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] home-mentionをフォロワーが見れる", async(async () => {
const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] home-mentionを非フォロワーが見れる", async(async () => {
const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] home-mentionを未認証が見れる", async(async () => {
const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, "@target x");
}));
// followers
it("[show] followers-mentionを自分が見れる", async(async () => {
const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] followers-mentionをメンションされていれば非フォロワーでも見れる", async(async () => {
const res = await show(folM.id, target);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] followers-mentionをフォロワーが見れる", async(async () => {
const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, "@target x");
}));
it("[show] followers-mentionを非フォロワーが見れない", async(async () => {
const res = await show(folM.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] followers-mentionを未認証が見れない", async(async () => {
const res = await show(folM.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it("[show] specified-mentionを自分が見れる", async(async () => {
const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, "@target2 x");
}));
it("[show] specified-mentionを指定ユーザーが見れる", async(async () => {
const res = await show(speM.id, target);
assert.strictEqual(res.body.text, "@target2 x");
}));
it("[show] specified-mentionをされた人が指定されてなかったら見れない", async(async () => {
const res = await show(speM.id, target2);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-mentionをフォロワーが見れない", async(async () => {
const res = await show(speM.id, follower);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-mentionを非フォロワーが見れない", async(async () => {
const res = await show(speM.id, other);
assert.strictEqual(res.status, 404);
}));
it("[show] specified-mentionを未認証が見れない", async(async () => {
const res = await show(speM.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region HTL
it("[HTL] public-post が 自分が見れる", 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);
assert.strictEqual(notes[0].text, "x");
}));
it("[HTL] public-post が 非フォロワーから見れない", 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);
assert.strictEqual(notes.length, 0);
}));
it("[HTL] followers-post が フォロワーから見れる", 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);
assert.strictEqual(notes[0].text, "x");
}));
//#endregion
//#region RTL
it("[replies] followers-reply が フォロワーから見れる", 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);
assert.strictEqual(notes[0].text, "x");
}));
it("[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない", 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);
assert.strictEqual(notes.length, 0);
}));
it("[replies] followers-reply が 非フォロワー (リプライ先である) から見れる", 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);
assert.strictEqual(notes[0].text, "x");
}));
//#endregion
//#region MTL
it("[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる", 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);
assert.strictEqual(notes[0].text, "x");
}));
it("[mentions] followers-mention が 非フォロワー (メンション先である) から見れる", 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);
assert.strictEqual(notes[0].text, "@target x");
}));
//#endregion
});
});

View file

@ -1,92 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
post,
react,
request,
shutdownServer,
signup,
startServer,
uploadFile,
} from "./utils.js";
describe("API", () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
carol = await signup({ username: "carol" });
});
after(async () => {
await shutdownServer(p);
});
describe("General validation", () => {
it("wrong type", async(async () => {
const res = await request("/test", {
required: true,
string: 42,
});
assert.strictEqual(res.status, 400);
}));
it("missing require param", async(async () => {
const res = await request("/test", {
string: "a",
});
assert.strictEqual(res.status, 400);
}));
it("invalid misskey:id (empty string)", async(async () => {
const res = await request("/test", {
required: true,
id: "",
});
assert.strictEqual(res.status, 400);
}));
it("valid misskey:id", async(async () => {
const res = await request("/test", {
required: true,
id: "8wvhjghbxu",
});
assert.strictEqual(res.status, 200);
}));
it("default value", async(async () => {
const res = await request("/test", {
required: true,
string: "a",
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.default, "hello");
}));
it("can set null even if it has default value", async(async () => {
const res = await request("/test", {
required: true,
nullableDefault: null,
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, null);
}));
it("cannot set undefined if it has default value", async(async () => {
const res = await request("/test", {
required: true,
nullableDefault: undefined,
});
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.nullableDefault, "hello");
}));
});
});

View file

@ -1,129 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
post,
request,
shutdownServer,
signup,
startServer,
} from "./utils.js";
describe("Block", () => {
let p: childProcess.ChildProcess;
// alice blocks bob
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
carol = await signup({ username: "carol" });
});
after(async () => {
await shutdownServer(p);
});
it("Block作成", async(async () => {
const res = await request(
"/blocking/create",
{
userId: bob.id,
},
alice,
);
assert.strictEqual(res.status, 200);
}));
it("ブロックされているユーザーをフォローできない", async(async () => {
const res = await request("/following/create", { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(
res.body.error.id,
"c4ab57cc-4e41-45e9-bfd9-584f61e35ce0",
);
}));
it("ブロックされているユーザーにリアクションできない", async(async () => {
const note = await post(alice, { text: "hello" });
const res = await request(
"/notes/reactions/create",
{ noteId: note.id, reaction: "👍" },
bob,
);
assert.strictEqual(res.status, 400);
assert.strictEqual(
res.body.error.id,
"20ef5475-9f38-4e4c-bd33-de6d979498ec",
);
}));
it("ブロックされているユーザーに返信できない", async(async () => {
const note = await post(alice, { text: "hello" });
const res = await request(
"/notes/create",
{ replyId: note.id, text: "yo" },
bob,
);
assert.strictEqual(res.status, 400);
assert.strictEqual(
res.body.error.id,
"b390d7e1-8a5e-46ed-b625-06271cafd3d3",
);
}));
it("ブロックされているユーザーのートをRenoteできない", async(async () => {
const note = await post(alice, { text: "hello" });
const res = await request(
"/notes/create",
{ renoteId: note.id, text: "yo" },
bob,
);
assert.strictEqual(res.status, 400);
assert.strictEqual(
res.body.error.id,
"b390d7e1-8a5e-46ed-b625-06271cafd3d3",
);
}));
// TODO: ユーザーリストに入れられないテスト
// TODO: ユーザーリストから除外されるテスト
it("タイムライン(LTL)にブロックされているユーザーの投稿が含まれない", async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request("/notes/local-timeline", {}, bob);
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,
);
}));
});

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

File diff suppressed because it is too large Load diff

View file

@ -1,865 +0,0 @@
/*
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';
describe('API: Endpoints', () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
describe('signup', () => {
it('不正なユーザー名でアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test.',
password: 'test'
});
assert.strictEqual(res.status, 400);
}));
it('空のパスワードでアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test',
password: ''
});
assert.strictEqual(res.status, 400);
}));
it('正しくアカウントが作成できる', async(async () => {
const me = {
username: 'test1',
password: 'test1'
};
const res = await request('/signup', me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.username, me.username);
}));
it('同じユーザー名のアカウントは作成できない', async(async () => {
await signup({
username: 'test2'
});
const res = await request('/signup', {
username: 'test2',
password: 'test2'
});
assert.strictEqual(res.status, 400);
}));
});
describe('signin', () => {
it('間違ったパスワードでサインインできない', async(async () => {
await signup({
username: 'test3',
password: 'foo'
});
const res = await request('/signin', {
username: 'test3',
password: 'bar'
});
assert.strictEqual(res.status, 403);
}));
it('クエリをインジェクションできない', async(async () => {
await signup({
username: 'test4'
});
const res = await request('/signin', {
username: 'test4',
password: {
$gt: ''
}
});
assert.strictEqual(res.status, 400);
}));
it('正しい情報でサインインできる', async(async () => {
await signup({
username: 'test5',
password: 'foo'
});
const res = await request('/signin', {
username: 'test5',
password: 'foo'
});
assert.strictEqual(res.status, 200);
}));
});
describe('i/update', () => {
it('アカウント設定を更新できる', async(async () => {
const myName = '大室櫻子';
const myLocation = '七森中';
const myBirthday = '2000-09-07';
const res = await request('/i/update', {
name: myName,
location: myLocation,
birthday: myBirthday
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, myName);
assert.strictEqual(res.body.location, myLocation);
assert.strictEqual(res.body.birthday, myBirthday);
}));
it('名前を空白にできない', async(async () => {
const res = await request('/i/update', {
name: ' '
}, alice);
assert.strictEqual(res.status, 400);
}));
it('誕生日の設定を削除できる', async(async () => {
await request('/i/update', {
birthday: '2000-09-07'
}, alice);
const res = await request('/i/update', {
birthday: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.birthday, null);
}));
it('不正な誕生日の形式で怒られる', async(async () => {
const res = await request('/i/update', {
birthday: '2000/09/07'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('users/show', () => {
it('ユーザーが取得できる', async(async () => {
const res = await request('/users/show', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, alice.id);
}));
it('ユーザーが存在しなかったら怒る', async(async () => {
const res = await request('/users/show', {
userId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/users/show', {
userId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/show', () => {
it('投稿が取得できる', async(async () => {
const myPost = await post(alice, {
text: 'test'
});
const res = await request('/notes/show', {
noteId: myPost.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, myPost.id);
assert.strictEqual(res.body.text, myPost.text);
}));
it('投稿が存在しなかったら怒る', async(async () => {
const res = await request('/notes/show', {
noteId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/show', {
noteId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/reactions/create', () => {
it('リアクションできる', async(async () => {
const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
const resNote = await request('/notes/show', {
noteId: bobPost.id,
}, alice);
assert.strictEqual(resNote.status, 200);
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]);
}));
it('自分の投稿にもリアクションできる', async(async () => {
const myPost = await post(alice);
const res = await request('/notes/reactions/create', {
noteId: myPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
}));
it('二重にリアクションできない', async(async () => {
const bobPost = await post(bob);
await react(alice, bobPost, 'like');
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しない投稿にはリアクションできない', async(async () => {
const res = await request('/notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/notes/reactions/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/reactions/create', {
noteId: 'kyoppie',
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('following/create', () => {
it('フォローできる', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('既にフォローしている場合は怒る', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォローできない', async(async () => {
const res = await request('/following/create', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォローできない', async(async () => {
const res = await request('/following/create', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/following/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/following/create', {
userId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('following/delete', () => {
it('フォロー解除できる', async(async () => {
await request('/following/create', {
userId: alice.id
}, bob);
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('フォローしていない場合は怒る', async(async () => {
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォロー解除できない', async(async () => {
const res = await request('/following/delete', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォロー解除できない', async(async () => {
const res = await request('/following/delete', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const res = await request('/following/delete', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/following/delete', {
userId: 'kyoppie'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive', () => {
it('ドライブ情報を取得できる', async(async () => {
await uploadFile({
userId: alice.id,
size: 256
});
await uploadFile({
userId: alice.id,
size: 512
});
await uploadFile({
userId: alice.id,
size: 1024
});
const res = await request('/drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('usage').eql(1792);
}));
});
describe('drive/files/create', () => {
it('ファイルを作成できる', async(async () => {
const res = await uploadFile(alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.png');
}));
it('ファイルに名前を付けられる', async(async () => {
const res = await assert.request(server)
.post('/drive/files/create')
.field('i', alice.token)
.field('name', 'Belmond.png')
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
expect(res).have.status(200);
expect(res.body).be.a('object');
expect(res.body).have.property('name').eql('Belmond.png');
}));
it('ファイル無しで怒られる', async(async () => {
const res = await request('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('SVGファイルを作成できる', async(async () => {
const res = await uploadFile(alice, __dirname + '/resources/image.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml');
}));
});
describe('drive/files/update', () => {
it('名前を更新できる', async(async () => {
const file = await uploadFile(alice);
const newName = 'いちごパスタ.png';
const res = await request('/drive/files/update', {
fileId: file.id,
name: newName
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, newName);
}));
it('他人のファイルは更新できない', async(async () => {
const file = await uploadFile(bob);
const res = await request('/drive/files/update', {
fileId: file.id,
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, folder.id);
}));
it('親フォルダを無しにできる', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, null);
}));
it('他人のフォルダには入れられない', async(async () => {
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダで怒られる', async(async () => {
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('ファイルが存在しなかったら怒る', async(async () => {
const res = await request('/drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive/folders/create', () => {
it('フォルダを作成できる', async(async () => {
const res = await request('/drive/folders/create', {
name: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'test');
}));
});
describe('drive/folders/update', () => {
it('名前を更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'new name');
}));
it('他人のフォルダを更新できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, parentFolder.id);
}));
it('親フォルダを無しに更新できる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, null);
}));
it('他人のフォルダを親フォルダに設定できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderB = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderC = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id
}, alice);
await request('/drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(自身)', async(async () => {
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しない親フォルダを設定できない', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正な親フォルダIDで怒られる', async(async () => {
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダを更新できない', async(async () => {
const res = await request('/drive/folders/update', {
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const res = await request('/drive/folders/update', {
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('messaging/messages/create', () => {
it('メッセージを送信できる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.text, 'test');
}));
it('自分自身にはメッセージを送信できない', async(async () => {
const res = await request('/messaging/messages/create', {
userId: alice.id,
text: 'Yo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにはメッセージを送信できない', async(async () => {
const res = await request('/messaging/messages/create', {
userId: '000000000000000000000000',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なユーザーIDで怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: 'foo',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('テキストが無くて怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('文字数オーバーで怒られる', async(async () => {
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: '!'.repeat(1001)
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('notes/replies', () => {
it('自分に閲覧権限のない投稿は含まれない', async(async () => {
const alicePost = await post(alice, {
text: 'foo'
});
await post(bob, {
replyId: alicePost.id,
text: 'bar',
visibility: 'specified',
visibleUserIds: [alice.id]
});
const res = await request('/notes/replies', {
noteId: alicePost.id
}, carol);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 0);
}));
});
describe('notes/timeline', () => {
it('フォロワー限定投稿が含まれる', async(async () => {
await request('/following/create', {
userId: alice.id
}, bob);
const alicePost = await post(alice, {
text: 'foo',
visibility: 'followers'
});
const res = await request('/notes/timeline', {}, bob);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, alicePost.id);
}));
});
});
*/

View file

@ -1,50 +0,0 @@
import * as assert from "node:assert";
import { parse } from "mfm-js";
import { extractMentions } from "../src/misc/extract-mentions.js";
describe("Extract mentions", () => {
it("simple", () => {
const ast = parse("@foo @bar @baz")!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [
{
username: "foo",
acct: "@foo",
host: null,
},
{
username: "bar",
acct: "@bar",
host: null,
},
{
username: "baz",
acct: "@baz",
host: null,
},
]);
});
it("nested", () => {
const ast = parse("@foo **@bar** @baz")!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [
{
username: "foo",
acct: "@foo",
host: null,
},
{
username: "bar",
acct: "@bar",
host: null,
},
{
username: "baz",
acct: "@baz",
host: null,
},
]);
});
});

View file

@ -1,213 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import * as openapi from "@redocly/openapi-core";
import {
async,
port,
post,
request,
shutdownServer,
signup,
simpleGet,
startServer,
} from "./utils.js";
// Request Accept
const ONLY_AP = "application/activity+json";
const PREFER_AP = "application/activity+json, */*";
const PREFER_HTML = "text/html, */*";
const UNSPECIFIED = "*/*";
// Response Contet-Type
const AP = "application/activity+json; charset=utf-8";
const JSON = "application/json; charset=utf-8";
const HTML = "text/html; charset=utf-8";
describe("Fetch resource", () => {
let p: childProcess.ChildProcess;
let alice: any;
let alicesPost: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
alicesPost = await post(alice, {
text: "test",
});
});
after(async () => {
await shutdownServer(p);
});
describe("Common", () => {
it("meta", async(async () => {
const res = await request("/meta", {});
assert.strictEqual(res.status, 200);
}));
it("GET root", async(async () => {
const res = await simpleGet("/");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it("GET docs", async(async () => {
const res = await simpleGet("/docs/ja-JP/about");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it("GET api-doc", async(async () => {
const res = await simpleGet("/api-doc");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it("GET api.json", async(async () => {
const res = await simpleGet("/api.json");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON);
}));
it("Validate api.json", async(async () => {
const config = await openapi.loadConfig();
const result = await openapi.bundle({
config,
ref: `http://localhost:${port}/api.json`,
});
for (const problem of result.problems) {
console.log(`${problem.message} - ${problem.location[0]?.pointer}`);
}
assert.strictEqual(result.problems.length, 0);
}));
it("GET favicon.ico", async(async () => {
const res = await simpleGet("/favicon.ico");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "image/x-icon");
}));
it("GET apple-touch-icon.png", async(async () => {
const res = await simpleGet("/apple-touch-icon.png");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "image/png");
}));
it("GET twemoji svg", async(async () => {
const res = await simpleGet("/twemoji/2764.svg");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "image/svg+xml");
}));
it("GET twemoji svg with hyphen", async(async () => {
const res = await simpleGet("/twemoji/2764-fe0f-200d-1f525.svg");
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "image/svg+xml");
}));
});
describe("/@:username", () => {
it("Only AP => AP", async(async () => {
const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer AP => AP", async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer HTML => HTML", async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it("Unspecified => HTML", async(async () => {
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe("/users/:id", () => {
it("Only AP => AP", async(async () => {
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer AP => AP", async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer HTML => Redirect to /@:username", async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
it("Undecided => HTML", async(async () => {
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
});
describe("/notes/:id", () => {
it("Only AP => AP", async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer AP => AP", async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it("Prefer HTML => HTML", async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it("Unspecified => HTML", async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe("Feeds", () => {
it("RSS", async(async () => {
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "application/rss+xml; charset=utf-8");
}));
it("ATOM", async(async () => {
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "application/atom+xml; charset=utf-8");
}));
it("JSON", async(async () => {
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, "application/json; charset=utf-8");
}));
});
});

View file

@ -1,283 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
connectStream,
post,
react,
request,
shutdownServer,
signup,
simpleGet,
startServer,
} from "./utils.js";
describe("FF visibility", () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
carol = await signup({ username: "carol" });
});
after(async () => {
await shutdownServer(p);
});
it("ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる", async(async () => {
await request(
"/i/update",
{
ffVisibility: "public",
},
alice,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
bob,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
bob,
);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it("ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる", async(async () => {
await request(
"/i/update",
{
ffVisibility: "followers",
},
alice,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
alice,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
alice,
);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it("ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない", async(async () => {
await request(
"/i/update",
{
ffVisibility: "followers",
},
alice,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
bob,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
bob,
);
assert.strictEqual(followingRes.status, 400);
assert.strictEqual(followersRes.status, 400);
}));
it("ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる", async(async () => {
await request(
"/i/update",
{
ffVisibility: "followers",
},
alice,
);
await request(
"/following/create",
{
userId: alice.id,
},
bob,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
bob,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
bob,
);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it("ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる", async(async () => {
await request(
"/i/update",
{
ffVisibility: "private",
},
alice,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
alice,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
alice,
);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it("ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない", async(async () => {
await request(
"/i/update",
{
ffVisibility: "private",
},
alice,
);
const followingRes = await request(
"/users/following",
{
userId: alice.id,
},
bob,
);
const followersRes = await request(
"/users/followers",
{
userId: alice.id,
},
bob,
);
assert.strictEqual(followingRes.status, 400);
assert.strictEqual(followersRes.status, 400);
}));
describe("AP", () => {
it("ffVisibility が public 以外ならばAPからは取得できない", async(async () => {
{
await request(
"/i/update",
{
ffVisibility: "public",
},
alice,
);
const followingRes = await simpleGet(
`/users/${alice.id}/following`,
"application/activity+json",
);
const followersRes = await simpleGet(
`/users/${alice.id}/followers`,
"application/activity+json",
);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(followersRes.status, 200);
}
{
await request(
"/i/update",
{
ffVisibility: "followers",
},
alice,
);
const followingRes = await simpleGet(
`/users/${alice.id}/following`,
"application/activity+json",
).catch((res) => ({ status: res.statusCode }));
const followersRes = await simpleGet(
`/users/${alice.id}/followers`,
"application/activity+json",
).catch((res) => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
{
await request(
"/i/update",
{
ffVisibility: "private",
},
alice,
);
const followingRes = await simpleGet(
`/users/${alice.id}/following`,
"application/activity+json",
).catch((res) => ({ status: res.statusCode }));
const followersRes = await simpleGet(
`/users/${alice.id}/followers`,
"application/activity+json",
).catch((res) => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
}));
});
});

View file

@ -1,209 +0,0 @@
import * as assert from "node:assert";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { getFileInfo } from "../src/misc/get-file-info.js";
import { async } from "./utils.js";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
describe("Get file info", () => {
it("Empty file", async(async () => {
const path = `${_dirname}/resources/emptyfile`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 0,
md5: "d41d8cd98f00b204e9800998ecf8427e",
type: {
mime: "application/octet-stream",
ext: null,
},
width: undefined,
height: undefined,
orientation: undefined,
});
}));
it("Generic JPEG", async(async () => {
const path = `${_dirname}/resources/Lenna.jpg`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 25360,
md5: "091b3f259662aa31e2ffef4519951168",
type: {
mime: "image/jpeg",
ext: "jpg",
},
width: 512,
height: 512,
orientation: undefined,
});
}));
it("Generic APNG", async(async () => {
const path = `${_dirname}/resources/anime.png`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 1868,
md5: "08189c607bea3b952704676bb3c979e0",
type: {
mime: "image/apng",
ext: "apng",
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it("Generic AGIF", async(async () => {
const path = `${_dirname}/resources/anime.gif`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 2248,
md5: "32c47a11555675d9267aee1a86571e7e",
type: {
mime: "image/gif",
ext: "gif",
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it("PNG with alpha", async(async () => {
const path = `${_dirname}/resources/with-alpha.png`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 3772,
md5: "f73535c3e1e27508885b69b10cf6e991",
type: {
mime: "image/png",
ext: "png",
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it("Generic SVG", async(async () => {
const path = `${_dirname}/resources/image.svg`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 505,
md5: "b6f52b4b021e7b92cdd04509c7267965",
type: {
mime: "image/svg+xml",
ext: "svg",
},
width: 256,
height: 256,
orientation: undefined,
});
}));
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, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 544,
md5: "4b7a346cde9ccbeb267e812567e33397",
type: {
mime: "image/svg+xml",
ext: "svg",
},
width: 256,
height: 256,
orientation: undefined,
});
}));
it("Dimension limit", async(async () => {
const path = `${_dirname}/resources/25000x25000.png`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 75933,
md5: "268c5dde99e17cf8fe09f1ab3f97df56",
type: {
mime: "application/octet-stream", // do not treat as image
ext: null,
},
width: 25000,
height: 25000,
orientation: undefined,
});
}));
it("Rotate JPEG", async(async () => {
const path = `${_dirname}/resources/rotate.jpg`;
const info = (await getFileInfo(path, {
skipSensitiveDetection: true,
})) as any;
delete info.warnings;
delete info.blurhash;
delete info.sensitive;
delete info.porn;
assert.deepStrictEqual(info, {
size: 12624,
md5: "68d5b2d8d1d1acbbce99203e3ec3857e",
type: {
mime: "image/jpeg",
ext: "jpg",
},
width: 512,
height: 256,
orientation: 8,
});
}));
});

View file

@ -1,37 +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,127 +0,0 @@
import * as assert from "node:assert";
import * as mfm from "mfm-js";
import { fromHtml } from "../src/mfm/from-html.js";
import { toHtml } from "../src/mfm/to-html.js";
describe("toHtml", () => {
it("br", () => {
const input = "foo\nbar\nbaz";
const output = "<p><span>foo<br>bar<br>baz</span></p>";
assert.equal(toHtml(mfm.parse(input)), output);
});
it("br alt", () => {
const input = "foo\r\nbar\rbaz";
const output = "<p><span>foo<br>bar<br>baz</span></p>";
assert.equal(toHtml(mfm.parse(input)), output);
});
});
describe("fromHtml", () => {
it("p", () => {
assert.deepStrictEqual(fromHtml("<p>a</p><p>b</p>"), "a\n\nb");
});
it("block element", () => {
assert.deepStrictEqual(fromHtml("<div>a</div><div>b</div>"), "a\nb");
});
it("inline element", () => {
assert.deepStrictEqual(fromHtml("<ul><li>a</li><li>b</li></ul>"), "a\nb");
});
it("block code", () => {
assert.deepStrictEqual(
fromHtml("<pre><code>a\nb</code></pre>"),
"```\na\nb\n```",
);
});
it("inline code", () => {
assert.deepStrictEqual(fromHtml("<code>a</code>"), "`a`");
});
it("quote", () => {
assert.deepStrictEqual(
fromHtml("<blockquote>a\nb</blockquote>"),
"> a\n> b",
);
});
it("br", () => {
assert.deepStrictEqual(fromHtml("<p>abc<br><br/>d</p>"), "abc\n\nd");
});
it("link with different text", () => {
assert.deepStrictEqual(
fromHtml('<p>a <a href="https://firefish.dev/firefish">c</a> d</p>'),
"a [c](https://firefish.dev/firefish) d",
);
});
it("link with different text, but not encoded", () => {
assert.deepStrictEqual(
fromHtml('<p>a <a href="https://firefish.dev/ä">c</a> d</p>'),
"a [c](<https://firefish.dev/ä>) d",
);
});
it("link with same text", () => {
assert.deepStrictEqual(
fromHtml(
'<p>a <a href="https://firefish.dev/firefish/firefish">https://firefish.dev/firefish/firefish</a> d</p>',
),
"a https://firefish.dev/firefish/firefish d",
);
});
it("link with same text, but not encoded", () => {
assert.deepStrictEqual(
fromHtml(
'<p>a <a href="https://firefish.dev/ä">https://firefish.dev/ä</a> d</p>',
),
"a <https://firefish.dev/ä> d",
);
});
it("link with no url", () => {
assert.deepStrictEqual(
fromHtml('<p>a <a href="b">c</a> d</p>'),
"a [c](b) d",
);
});
it("link without href", () => {
assert.deepStrictEqual(fromHtml("<p>a <a>c</a> d</p>"), "a c d");
});
it("link without text", () => {
assert.deepStrictEqual(
fromHtml('<p>a <a href="https://firefish.dev/b"></a> d</p>'),
"a https://firefish.dev/b d",
);
});
it("link without both", () => {
assert.deepStrictEqual(fromHtml("<p>a <a></a> d</p>"), "a d");
});
it("mention", () => {
assert.deepStrictEqual(
fromHtml(
'<p>a <a href="https://info.firefish.dev/@firefish" class="u-url mention">@firefish</a> d</p>',
),
"a @firefish@info.firefish.dev d",
);
});
it("hashtag", () => {
assert.deepStrictEqual(
fromHtml('<p>a <a href="https://info.firefish.dev/tags/a">#a</a> d</p>', [
"#a",
]),
"a #a d",
);
});
});

View file

@ -1,39 +0,0 @@
import Resolver from "../../src/remote/activitypub/resolver.js";
import { IObject } from "../../src/remote/activitypub/type.js";
type MockResponse = {
type: string;
content: string;
};
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",
) {
this._rs.set(uri, {
type,
content: typeof content === "string" ? content : JSON.stringify(content),
});
}
public async resolve(value: string | IObject): Promise<IObject> {
if (typeof value !== "string") return value;
const r = this._rs.get(value);
if (!r) {
throw {
name: "StatusError",
statusCode: 404,
message: "Not registed for mock",
};
}
const object = JSON.parse(r.content);
return object;
}
}

View file

@ -1,176 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
post,
react,
request,
shutdownServer,
signup,
startServer,
waitFire,
} from "./utils.js";
describe("Mute", () => {
let p: childProcess.ChildProcess;
// alice mutes carol
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
carol = await signup({ username: "carol" });
});
after(async () => {
await shutdownServer(p);
});
it("ミュート作成", async(async () => {
const res = await request(
"/mute/create",
{
userId: carol.id,
},
alice,
);
assert.strictEqual(res.status, 204);
}));
it("「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない", async(async () => {
const bobNote = await post(bob, { text: "@alice hi" });
const carolNote = await post(carol, { text: "@alice hi" });
const res = await request("/notes/mentions", {}, alice);
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,
);
}));
it("ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない", async(async () => {
// 状態リセット
await request("/i/read-all-unread-notes", {}, alice);
await post(carol, { text: "@alice hi" });
const res = await request("/i", {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it("ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない", async () => {
// 状態リセット
await request("/i/read-all-unread-notes", {}, alice);
const fired = await waitFire(
alice,
"main",
() => post(carol, { text: "@alice hi" }),
(msg) => msg.type === "unreadMention",
);
assert.strictEqual(fired, false);
});
it("ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない", async () => {
// 状態リセット
await request("/i/read-all-unread-notes", {}, alice);
await request("/notifications/mark-all-as-read", {}, alice);
const fired = await waitFire(
alice,
"main",
() => post(carol, { text: "@alice hi" }),
(msg) => msg.type === "unreadNotification",
);
assert.strictEqual(fired, false);
});
describe("Timeline", () => {
it("タイムラインにミュートしているユーザーの投稿が含まれない", async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request("/notes/local-timeline", {}, alice);
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,
);
}));
it("タイムラインにミュートしているユーザーの投稿のRenoteが含まれない", async(async () => {
const aliceNote = await post(alice);
const carolNote = await post(carol);
const bobNote = await post(bob, {
renoteId: carolNote.id,
});
const res = await request("/notes/local-timeline", {}, alice);
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,
);
}));
});
describe("Notification", () => {
it("通知にミュートしているユーザーの通知が含まれない(リアクション)", async(async () => {
const aliceNote = await post(alice);
await react(bob, aliceNote, "like");
await react(carol, aliceNote, "like");
const res = await request("/i/notifications", {}, alice);
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,
);
}));
});
});

View file

@ -1,517 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import { Note } from "../src/models/entities/note.js";
import {
api,
async,
initTestDb,
post,
request,
shutdownServer,
signup,
startServer,
uploadUrl,
} from "./utils.js";
describe("Note", () => {
let p: childProcess.ChildProcess;
let Notes: any;
let alice: any;
let bob: any;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Notes = connection.getRepository(Note);
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
});
after(async () => {
await shutdownServer(p);
});
it("投稿できる", async(async () => {
const post = {
text: "test",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it("ファイルを添付できる", async(async () => {
const file = await uploadUrl(
alice,
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
);
const res = await request(
"/notes/create",
{
fileIds: [file.id],
},
alice,
);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
}));
it("他人のファイルは無視", async(async () => {
const file = await uploadUrl(
bob,
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
);
const res = await request(
"/notes/create",
{
text: "test",
fileIds: [file.id],
},
alice,
);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it("存在しないファイルは無視", async(async () => {
const res = await request(
"/notes/create",
{
text: "test",
fileIds: ["000000000000000000000000"],
},
alice,
);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it("不正なファイルIDは無視", async(async () => {
const res = await request(
"/notes/create",
{
fileIds: ["kyoppie"],
},
alice,
);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it("返信できる", async(async () => {
const bobPost = await post(bob, {
text: "foo",
});
const alicePost = {
text: "bar",
replyId: bobPost.id,
};
const res = await request("/notes/create", alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
}));
it("renoteできる", async(async () => {
const bobPost = await post(bob, {
text: "test",
});
const alicePost = {
renoteId: bobPost.id,
};
const res = await request("/notes/create", alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it("引用renoteできる", async(async () => {
const bobPost = await post(bob, {
text: "test",
});
const alicePost = {
text: "test",
renoteId: bobPost.id,
};
const res = await request("/notes/create", alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it("文字数ぎりぎりで怒られない", async(async () => {
const post = {
text: "!".repeat(3000),
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 200);
}));
it("文字数オーバーで怒られる", async(async () => {
const post = {
text: "!".repeat(3001),
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 400);
}));
it("存在しないリプライ先で怒られる", async(async () => {
const post = {
text: "test",
replyId: "000000000000000000000000",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 400);
}));
it("存在しないrenote対象で怒られる", async(async () => {
const post = {
renoteId: "000000000000000000000000",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 400);
}));
it("不正なリプライ先IDで怒られる", async(async () => {
const post = {
text: "test",
replyId: "foo",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 400);
}));
it("不正なrenote対象IDで怒られる", async(async () => {
const post = {
renoteId: "foo",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 400);
}));
it("存在しないユーザーにメンションできる", async(async () => {
const post = {
text: "@ghost yo",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it("同じユーザーに複数メンションしても内部的にまとめられる", async(async () => {
const post = {
text: "@bob @bob @bob yo",
};
const res = await request("/notes/create", post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.text, post.text);
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
}));
describe("notes/create", () => {
it("投票を添付できる", async(async () => {
const res = await request(
"/notes/create",
{
text: "test",
poll: {
choices: ["foo", "bar"],
},
},
alice,
);
assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.body === "object" && !Array.isArray(res.body),
true,
);
assert.strictEqual(res.body.createdNote.poll != null, true);
}));
it("投票の選択肢が無くて怒られる", async(async () => {
const res = await request(
"/notes/create",
{
poll: {},
},
alice,
);
assert.strictEqual(res.status, 400);
}));
it("投票の選択肢が無くて怒られる (空の配列)", async(async () => {
const res = await request(
"/notes/create",
{
poll: {
choices: [],
},
},
alice,
);
assert.strictEqual(res.status, 400);
}));
it("投票の選択肢が1つで怒られる", async(async () => {
const res = await request(
"/notes/create",
{
poll: {
choices: ["Strawberry Pasta"],
},
},
alice,
);
assert.strictEqual(res.status, 400);
}));
it("投票できる", async(async () => {
const { body } = await request(
"/notes/create",
{
text: "test",
poll: {
choices: ["sakura", "izumi", "ako"],
},
},
alice,
);
const res = await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 1,
},
alice,
);
assert.strictEqual(res.status, 204);
}));
it("複数投票できない", async(async () => {
const { body } = await request(
"/notes/create",
{
text: "test",
poll: {
choices: ["sakura", "izumi", "ako"],
},
},
alice,
);
await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 0,
},
alice,
);
const res = await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 2,
},
alice,
);
assert.strictEqual(res.status, 400);
}));
it("許可されている場合は複数投票できる", async(async () => {
const { body } = await request(
"/notes/create",
{
text: "test",
poll: {
choices: ["sakura", "izumi", "ako"],
multiple: true,
},
},
alice,
);
await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 0,
},
alice,
);
await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 1,
},
alice,
);
const res = await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 2,
},
alice,
);
assert.strictEqual(res.status, 204);
}));
it("締め切られている場合は投票できない", async(async () => {
const { body } = await request(
"/notes/create",
{
text: "test",
poll: {
choices: ["sakura", "izumi", "ako"],
expiredAfter: 1,
},
},
alice,
);
await new Promise((x) => setTimeout(x, 2));
const res = await request(
"/notes/polls/vote",
{
noteId: body.createdNote.id,
choice: 1,
},
alice,
);
assert.strictEqual(res.status, 400);
}));
});
describe("notes/delete", () => {
it("delete a reply", async(async () => {
const mainNoteRes = await api(
"notes/create",
{
text: "main post",
},
alice,
);
const replyOneRes = await api(
"notes/create",
{
text: "reply one",
replyId: mainNoteRes.body.createdNote.id,
},
alice,
);
const replyTwoRes = await api(
"notes/create",
{
text: "reply two",
replyId: mainNoteRes.body.createdNote.id,
},
alice,
);
const deleteOneRes = await api(
"notes/delete",
{
noteId: replyOneRes.body.createdNote.id,
},
alice,
);
assert.strictEqual(deleteOneRes.status, 204);
let mainNote = await Notes.findOneBy({
id: mainNoteRes.body.createdNote.id,
});
assert.strictEqual(mainNote.repliesCount, 1);
const deleteTwoRes = await api(
"notes/delete",
{
noteId: replyTwoRes.body.createdNote.id,
},
alice,
);
assert.strictEqual(deleteTwoRes.status, 204);
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.strictEqual(mainNote.repliesCount, 0);
}));
});
});

View file

@ -1,18 +0,0 @@
import * as assert from "node:assert";
import { just, nothing } from "../../src/prelude/maybe.js";
describe("just", () => {
it("has a value", () => {
assert.deepStrictEqual(just(3).isJust(), true);
});
it("has the inverse called get", () => {
assert.deepStrictEqual(just(3).get(), 3);
});
});
describe("nothing", () => {
it("has no value", () => {
assert.deepStrictEqual(nothing().isJust(), false);
});
});

View file

@ -1,13 +0,0 @@
import * as assert from "node:assert";
import { query } from "../../src/prelude/url.js";
describe("url", () => {
it("query", () => {
const s = query({
foo: "ふぅ",
bar: "b a r",
baz: undefined,
});
assert.deepStrictEqual(s, "foo=%E3%81%B5%E3%81%85&bar=b%20a%20r");
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>

Before

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>

Before

Width:  |  Height:  |  Size: 504 B

View file

@ -1,766 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import { Following } from "../src/models/entities/following.js";
import {
api,
connectStream,
initTestDb,
post,
shutdownServer,
signup,
startServer,
waitFire,
} from "./utils.js";
describe("Streaming", () => {
let p: childProcess.ChildProcess;
let Followings: any;
const follow = async (follower: any, followee: any) => {
await Followings.save({
id: "a",
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
followerHost: follower.host,
followerInbox: null,
followerSharedInbox: null,
followeeHost: followee.host,
followeeInbox: null,
followeeSharedInbox: null,
});
};
describe("Streaming", () => {
// Local users
let ayano: any;
let kyoko: any;
let chitose: any;
// Remote users
let akari: any;
let chinatsu: any;
let kyokoNote: any;
let list: any;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Followings = connection.getRepository(Following);
ayano = await signup({ username: "ayano" });
kyoko = await signup({ username: "kyoko" });
chitose = await signup({ username: "chitose" });
akari = await signup({ username: "akari", host: "example.com" });
chinatsu = await signup({ username: "chinatsu", host: "example.com" });
kyokoNote = await post(kyoko, { text: "foo" });
// Follow: ayano => kyoko
await api("following/create", { userId: kyoko.id }, ayano);
// Follow: ayano => akari
await follow(ayano, akari);
// List: chitose => ayano, kyoko
list = await api(
"users/lists/create",
{
name: "my list",
},
chitose,
).then((x) => x.body);
await api(
"users/lists/push",
{
listId: list.id,
userId: ayano.id,
},
chitose,
);
await api(
"users/lists/push",
{
listId: list.id,
userId: kyoko.id,
},
chitose,
);
});
after(async () => {
await shutdownServer(p);
});
describe("Events", () => {
it("mention event", async () => {
const fired = await waitFire(
kyoko,
"main", // kyoko:main
() => post(ayano, { text: "foo @kyoko bar" }), // ayano mention => kyoko
(msg) => msg.type === "mention" && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, true);
});
it("renote event", async () => {
const fired = await waitFire(
kyoko,
"main", // kyoko:main
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
(msg) => msg.type === "renote" && msg.body.renoteId === kyokoNote.id, // wait renote
);
assert.strictEqual(fired, true);
});
});
describe("Home Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:Home
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() => api("notes/create", { text: "foo" }, kyoko), // kyoko posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしていないユーザーの投稿は流れない", async () => {
const fired = await waitFire(
kyoko,
"homeTimeline", // kyoko:home
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, false);
});
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko dm => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [chitose.id],
},
kyoko,
), // kyoko dm => chitose
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
}); // Home
describe("Local Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("リモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, false);
});
it("ホーム指定の投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko DM => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, false);
});
});
describe("Recommended Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("リモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, false);
});
it("ホーム指定の投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko DM => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, false);
});
});
describe("Hybrid Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("フォローしているリモートユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, true);
});
it("フォローしていないリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーのホーム投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーのホーム投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api("notes/create", { text: "foo", visibility: "home" }, chitose),
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", () =>
async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
);
assert.strictEqual(fired, false);
});
});
describe("Global Timeline", () => {
it("フォローしていないローカルユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("フォローしていないリモートユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, true);
});
it("ホーム投稿は流れない", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
});
describe("UserList Timeline", () => {
it("リストに入れているユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
chitose,
"userList",
() => api("notes/create", { text: "foo" }, ayano),
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
{ listId: list.id },
);
assert.strictEqual(fired, true);
});
it("リストに入れていないユーザーの投稿は流れない", () => async () => {
const fired = await waitFire(
chitose,
"userList",
() => api("notes/create", { text: "foo" }, chinatsu),
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id,
{ listId: list.id },
);
assert.strictEqual(fired, false);
});
// #4471
it("リストに入れているユーザーのダイレクト投稿が流れる", () =>
async () => {
const fired = await waitFire(
chitose,
"userList",
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [chitose.id],
},
ayano,
),
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
{ listId: list.id },
);
assert.strictEqual(fired, true);
});
// #4335
it("リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない", () =>
async () => {
const fired = await waitFire(
chitose,
"userList",
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
kyoko,
),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id,
{ listId: list.id },
);
assert.strictEqual(fired, false);
});
});
describe("Hashtag Timeline", () => {
it("指定したハッシュタグの投稿が流れる", () =>
new Promise<void>(async (done) => {
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
assert.deepStrictEqual(body.text, "#foo");
ws.close();
done();
}
},
{
q: [["foo"]],
},
);
post(chitose, {
text: "#foo",
});
}));
it("指定したハッシュタグの投稿が流れる (AND)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
}
},
{
q: [["foo", "bar"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
ws.close();
done();
}, 3000);
}));
it("指定したハッシュタグの投稿が流れる (OR)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
if (body.text === "#piyo") piyoCount++;
}
},
{
q: [["foo"], ["bar"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
post(chitose, {
text: "#piyo",
});
setTimeout(() => {
assert.strictEqual(fooCount, 1);
assert.strictEqual(barCount, 1);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 0);
ws.close();
done();
}, 3000);
}));
it("指定したハッシュタグの投稿が流れる (AND + OR)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
let waaaCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
if (body.text === "#piyo") piyoCount++;
if (body.text === "#waaa") waaaCount++;
}
},
{
q: [["foo", "bar"], ["piyo"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
post(chitose, {
text: "#piyo",
});
post(chitose, {
text: "#waaa",
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 1);
assert.strictEqual(waaaCount, 0);
ws.close();
done();
}, 3000);
}));
});
});
});

View file

@ -1,161 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
connectStream,
post,
react,
request,
shutdownServer,
signup,
startServer,
} from "./utils.js";
describe("Note thread mute", () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
bob = await signup({ username: "bob" });
carol = await signup({ username: "carol" });
});
after(async () => {
await shutdownServer(p);
});
it("notes/mentions にミュートしているスレッドの投稿が含まれない", async(async () => {
const bobNote = await post(bob, { text: "@alice @carol root note" });
const aliceReply = await post(alice, {
replyId: bobNote.id,
text: "@bob @carol child note",
});
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
const carolReply = await post(carol, {
replyId: bobNote.id,
text: "@bob @alice child note",
});
const carolReplyWithoutMention = await post(carol, {
replyId: aliceReply.id,
text: "child note",
});
const res = await request("/notes/mentions", {}, alice);
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,
);
}));
it("ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない", async(async () => {
// 状態リセット
await request("/i/read-all-unread-notes", {}, alice);
const bobNote = await post(bob, { text: "@alice @carol root note" });
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
const carolReply = await post(carol, {
replyId: bobNote.id,
text: "@bob @alice child note",
});
const res = await request("/i", {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it("ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない", () =>
new Promise(async (done) => {
// 状態リセット
await request("/i/read-all-unread-notes", {}, alice);
const bobNote = await post(bob, { text: "@alice @carol root note" });
await request(
"/notes/thread-muting/create",
{ noteId: bobNote.id },
alice,
);
let fired = false;
const ws = await connectStream(alice, "main", async ({ type, body }) => {
if (type === "unreadMention") {
if (body === bobNote.id) return;
fired = true;
}
});
const carolReply = await post(carol, {
replyId: bobNote.id,
text: "@bob @alice child note",
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
it("i/notifications にミュートしているスレッドの通知が含まれない", async(async () => {
const bobNote = await post(bob, { text: "@alice @carol root note" });
const aliceReply = await post(alice, {
replyId: bobNote.id,
text: "@bob @carol child note",
});
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
const carolReply = await post(carol, {
replyId: bobNote.id,
text: "@bob @alice child note",
});
const carolReplyWithoutMention = await post(carol, {
replyId: aliceReply.id,
text: "child note",
});
const res = await request("/i/notifications", {}, alice);
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,
);
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
}));
});

View file

@ -1,34 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"noEmitOnError": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"sourceMap": true,
"target": "es2021",
"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

@ -1,98 +0,0 @@
process.env.NODE_ENV = "test";
import * as assert from "node:assert";
import type * as childProcess from "node:child_process";
import {
async,
post,
request,
shutdownServer,
signup,
startServer,
uploadUrl,
} from "./utils.js";
describe("users/notes", () => {
let p: childProcess.ChildProcess;
let alice: any;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
before(async () => {
p = await startServer();
alice = await signup({ username: "alice" });
const jpg = await uploadUrl(
alice,
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
);
const png = await uploadUrl(
alice,
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png",
);
jpgNote = await post(alice, {
fileIds: [jpg.id],
});
pngNote = await post(alice, {
fileIds: [png.id],
});
jpgPngNote = await post(alice, {
fileIds: [jpg.id, png.id],
});
});
after(async () => {
await shutdownServer(p);
});
it("ファイルタイプ指定 (jpg)", async(async () => {
const res = await request(
"/users/notes",
{
userId: alice.id,
fileType: ["image/jpeg"],
},
alice,
);
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,
);
}));
it("ファイルタイプ指定 (jpg or png)", async(async () => {
const res = await request(
"/users/notes",
{
userId: alice.id,
fileType: ["image/jpeg", "image/png"],
},
alice,
);
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,
);
}));
});

View file

@ -1,403 +0,0 @@
import { SIGKILL } from "constants";
import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as http from "node:http";
import * as path from "node:path";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import type { Entities, endpoints } from "firefish-js";
import FormData from "form-data";
import got from "got";
import fetch from "node-fetch";
import { DataSource } from "typeorm";
import WebSocket from "ws";
import loadConfig from "../src/config/load.js";
import { entities } from "../src/db/postgre.js";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const config = loadConfig();
export const port = config.port;
export const async = (fn: Function) => (done: Function) => {
fn().then(
() => {
done();
},
(err: Error) => {
done(err);
},
);
};
export const api = async (endpoint: string, params: any, me?: any) => {
endpoint = endpoint.replace(/^\//, "");
const auth = me
? {
i: me.token,
}
: {};
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Object.assign(auth, params)),
retry: {
limit: 0,
},
hooks: {
beforeError: [
(error) => {
const { response } = error;
if (response && response.body) console.warn(response.body);
return error;
},
],
},
});
const status = res.statusCode;
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return {
status,
body,
};
};
export const request = async (
endpoint: string,
params: any,
me?: any,
): Promise<{ body: any; status: number }> => {
const auth = me
? {
i: me.token,
}
: {};
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Object.assign(auth, params)),
});
const status = res.status;
const body = res.status !== 204 ? await res.json().catch() : null;
return {
body,
status,
};
};
export const signup = async (params?: any): Promise<any> => {
const q = Object.assign(
{
username: "test",
password: "test",
},
params,
);
const res = await api("signup", q);
return res.body;
};
export const post = async (
user: any,
params?: Endpoints["notes/create"]["req"],
): Promise<entities.Note> => {
const q = Object.assign(
{
text: "test",
},
params,
);
const res = await api("notes/create", q, user);
return res.body ? res.body.createdNote : null;
};
export const react = async (
user: any,
note: any,
reaction: string,
): Promise<any> => {
await api(
"notes/reactions/create",
{
noteId: note.id,
reaction: reaction,
},
user,
);
};
/**
* Upload file
* @param user User
* @param _path Optional, absolute path or relative from ./resources/
*/
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
const absPath =
_path == null
? `${_dirname}/resources/Lenna.jpg`
: path.isAbsolute(_path)
? _path
: `${_dirname}/resources/${_path}`;
const formData = new FormData() as any;
formData.append("i", user.token);
formData.append("file", fs.createReadStream(absPath));
formData.append("force", "true");
const res = await got<string>(
`http://localhost:${port}/api/drive/files/create`,
{
method: "POST",
body: formData,
retry: {
limit: 0,
},
},
);
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return body;
};
export const uploadUrl = async (user: any, url: string) => {
let file: any;
const ws = await connectStream(user, "main", (msg) => {
if (msg.type === "driveFileCreated") {
file = msg.body;
}
});
await api(
"drive/files/upload-from-url",
{
url,
force: true,
},
user,
);
await sleep(5000);
ws.close();
return file;
};
export function connectStream(
user: any,
channel: string,
listener: (message: Record<string, any>) => any,
params?: any,
): Promise<WebSocket> {
return new Promise((res, rej) => {
const ws = new WebSocket(
`ws://localhost:${port}/streaming?i=${user.token}`,
);
ws.on("open", () => {
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "channel" && msg.body.id === "a") {
listener(msg.body);
} else if (msg.type === "connected" && msg.body.id === "a") {
res(ws);
}
});
ws.send(
JSON.stringify({
type: "connect",
body: {
channel: channel,
id: "a",
pong: true,
params: params,
},
}),
);
});
});
}
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;
let ws: WebSocket;
try {
ws = await connectStream(
user,
channel,
(msg) => {
if (cond(msg)) {
ws.close();
if (timer) clearTimeout(timer);
res(true);
}
},
params,
);
} catch (e) {
rej(e);
}
if (!ws!) return;
timer = setTimeout(() => {
ws.close();
res(false);
}, 3000);
try {
await trgr();
} catch (e) {
ws.close();
if (timer) clearTimeout(timer);
rej(e);
}
});
};
export const simpleGet = async (
path: string,
accept = "*/*",
): Promise<{ status?: number; type?: string; location?: string }> => {
// node-fetchだと3xxを取れない
return await new Promise((resolve, reject) => {
const req = http.request(
`http://localhost:${port}${path}`,
{
headers: {
Accept: accept,
},
},
(res) => {
if (res.statusCode! >= 400) {
reject(res);
} else {
resolve({
status: res.statusCode,
type: res.headers["content-type"],
location: res.headers.location,
});
}
},
);
req.end();
});
};
export function launchServer(
callbackSpawnedProcess: (p: childProcess.ChildProcess) => void,
moreProcess: () => Promise<void> = async () => {},
) {
return (done: (err?: Error) => any) => {
const p = childProcess.spawn("node", [_dirname + "/../index.js"], {
stdio: ["inherit", "inherit", "inherit", "ipc"],
env: { NODE_ENV: "test", PATH: process.env.PATH },
});
callbackSpawnedProcess(p);
p.on("message", (message) => {
if (message === "ok")
moreProcess()
.then(() => done())
.catch((e) => done(e));
});
};
}
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
if (process.env.NODE_ENV !== "test") throw "NODE_ENV is not a test";
const db = new DataSource({
type: "postgres",
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
synchronize: true && !justBorrow,
dropSchema: true && !justBorrow,
entities: initEntities || entities,
});
await db.initialize();
return db;
}
export function startServer(
timeout = 60 * 1000,
): Promise<childProcess.ChildProcess> {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
rej("timeout to start");
}, timeout);
const p = childProcess.spawn("node", [_dirname + "/../built/index.js"], {
stdio: ["inherit", "inherit", "inherit", "ipc"],
env: { NODE_ENV: "test", PATH: process.env.PATH },
});
p.on("error", (e) => rej(e));
p.on("message", (message) => {
if (message === "ok") {
clearTimeout(t);
res(p);
}
});
});
}
export function shutdownServer(
p: childProcess.ChildProcess,
timeout = 20 * 1000,
) {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
res("force exit");
}, timeout);
p.once("exit", () => {
clearTimeout(t);
res("exited");
});
p.kill();
});
}
export function sleep(msec: number) {
return new Promise<void>((res) => {
setTimeout(() => {
res();
}, msec);
});
}

View file

@ -129,9 +129,6 @@ importers:
deep-email-validator: deep-email-validator:
specifier: 0.1.21 specifier: 0.1.21
version: 0.1.21 version: 0.1.21
deepl-node:
specifier: 1.13.0
version: 1.13.0
escape-regexp: escape-regexp:
specifier: 0.0.1 specifier: 0.0.1
version: 0.0.1 version: 0.0.1
@ -231,9 +228,6 @@ importers:
nodemailer: nodemailer:
specifier: 6.9.14 specifier: 6.9.14
version: 6.9.14 version: 6.9.14
opencc-js:
specifier: 1.0.5
version: 1.0.5
otpauth: otpauth:
specifier: 9.3.1 specifier: 9.3.1
version: 9.3.1 version: 9.3.1
@ -394,9 +388,6 @@ importers:
'@types/koa__router': '@types/koa__router':
specifier: 12.0.4 specifier: 12.0.4
version: 12.0.4 version: 12.0.4
'@types/mocha':
specifier: 10.0.7
version: 10.0.7
'@types/node': '@types/node':
specifier: 20.14.11 specifier: 20.14.11
version: 20.14.11 version: 20.14.11
@ -469,9 +460,6 @@ importers:
cross-env: cross-env:
specifier: 7.0.3 specifier: 7.0.3
version: 7.0.3 version: 7.0.3
mocha:
specifier: 10.6.0
version: 10.6.0
pug: pug:
specifier: 3.0.3 specifier: 3.0.3
version: 3.0.3 version: 3.0.3
@ -1001,13 +989,11 @@ packages:
'@biomejs/cli-darwin-arm64@1.8.3': '@biomejs/cli-darwin-arm64@1.8.3':
resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==} resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.8.3': '@biomejs/cli-darwin-x64@1.8.3':
resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==} resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.8.3': '@biomejs/cli-linux-arm64-musl@1.8.3':
@ -1019,7 +1005,6 @@ packages:
'@biomejs/cli-linux-arm64@1.8.3': '@biomejs/cli-linux-arm64@1.8.3':
resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==} resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@1.8.3': '@biomejs/cli-linux-x64-musl@1.8.3':
@ -1031,7 +1016,6 @@ packages:
'@biomejs/cli-linux-x64@1.8.3': '@biomejs/cli-linux-x64@1.8.3':
resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==} resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@1.8.3': '@biomejs/cli-win32-arm64@1.8.3':
@ -2500,9 +2484,6 @@ packages:
'@types/minimist@1.2.5': '@types/minimist@1.2.5':
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
'@types/mocha@10.0.7':
resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==}
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
@ -2780,10 +2761,6 @@ packages:
ajv@8.17.1: ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-escapes@4.3.2: ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2979,9 +2956,6 @@ packages:
broadcast-channel@7.0.0: broadcast-channel@7.0.0:
resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==} resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==}
browser-stdout@1.3.1:
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
browserify-zlib@0.1.4: browserify-zlib@0.1.4:
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
@ -3608,10 +3582,6 @@ packages:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
decamelize@4.0.0:
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
engines: {node: '>=10'}
decimal.js@10.4.3: decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@ -3653,10 +3623,6 @@ packages:
deep-equal@1.0.1: deep-equal@1.0.1:
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
deepl-node@1.13.0:
resolution: {integrity: sha512-pm8Al5B+/fRHiIKoreoSmv2RlXidF18+CznhtLILiYcj3EbxZpIhxWO8cgXCCsCTrUDMAbScIl8CuH3AqLPpGg==}
engines: {node: '>=12.0'}
deepmerge@4.3.1: deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -3711,10 +3677,6 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
dijkstrajs@1.0.3: dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
@ -4032,17 +3994,9 @@ packages:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'} engines: {node: '>=8'}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
fix-esm@1.0.1: fix-esm@1.0.1:
resolution: {integrity: sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==} resolution: {integrity: sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw==}
flat@5.0.2:
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
hasBin: true
fluent-ffmpeg@2.1.3: fluent-ffmpeg@2.1.3:
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -4076,10 +4030,6 @@ packages:
resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
form-data@3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
form-data@4.0.0: form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -4179,11 +4129,6 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Glob versions prior to v9 are no longer supported
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
deprecated: Glob versions prior to v9 are no longer supported
globals@11.12.0: globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4498,10 +4443,6 @@ packages:
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-plain-obj@2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
is-plain-obj@4.1.0: is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -4964,10 +4905,6 @@ packages:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'} engines: {node: '>=8'}
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21: lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@ -5023,10 +4960,6 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'} engines: {node: '>=10'}
loglevel@1.9.1:
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
engines: {node: '>= 0.6.0'}
long@5.2.3: long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
@ -5172,11 +5105,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
mocha@10.6.0:
resolution: {integrity: sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==}
engines: {node: '>= 14.0.0'}
hasBin: true
mock-socket@9.3.1: mock-socket@9.3.1:
resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -5354,9 +5282,6 @@ packages:
only@0.0.2: only@0.0.2:
resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==}
opencc-js@1.0.5:
resolution: {integrity: sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==}
opencollective-postinstall@2.0.3: opencollective-postinstall@2.0.3:
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
hasBin: true hasBin: true
@ -5396,10 +5321,6 @@ packages:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'} engines: {node: '>=8'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-queue@6.6.2: p-queue@6.6.2:
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -6835,9 +6756,6 @@ packages:
resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
workerpool@6.5.1:
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -6931,10 +6849,6 @@ packages:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
yargs-unparser@2.0.0:
resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
engines: {node: '>=10'}
yargs@15.4.1: yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -7007,7 +6921,7 @@ snapshots:
'@babel/traverse': 7.24.8 '@babel/traverse': 7.24.8
'@babel/types': 7.24.9 '@babel/types': 7.24.9
convert-source-map: 2.0.0 convert-source-map: 2.0.0
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
gensync: 1.0.0-beta.2 gensync: 1.0.0-beta.2
json5: 2.2.3 json5: 2.2.3
semver: 6.3.1 semver: 6.3.1
@ -7205,7 +7119,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.24.8 '@babel/parser': 7.24.8
'@babel/types': 7.24.9 '@babel/types': 7.24.9
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -7905,7 +7819,7 @@ snapshots:
'@koa/router@12.0.1': '@koa/router@12.0.1':
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
http-errors: 2.0.0 http-errors: 2.0.0
koa-compose: 4.1.0 koa-compose: 4.1.0
methods: 1.1.2 methods: 1.1.2
@ -7925,7 +7839,7 @@ snapshots:
'@ladjs/koa-views@9.0.0(@babel/core@7.24.9)(@types/koa@2.15.0)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3)': '@ladjs/koa-views@9.0.0(@babel/core@7.24.9)(@types/koa@2.15.0)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3)':
dependencies: dependencies:
'@ladjs/consolidate': 1.0.4(@babel/core@7.24.9)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3) '@ladjs/consolidate': 1.0.4(@babel/core@7.24.9)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3)
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
get-paths: 0.0.7 get-paths: 0.0.7
koa-send: 5.0.1 koa-send: 5.0.1
mz: 2.7.0 mz: 2.7.0
@ -8011,7 +7925,7 @@ snapshots:
'@octokit/rest': 21.0.1 '@octokit/rest': 21.0.1
clipanion: 3.2.1(typanion@3.14.0) clipanion: 3.2.1(typanion@3.14.0)
colorette: 2.0.20 colorette: 2.0.20
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
inquirer: 10.0.4 inquirer: 10.0.4
js-yaml: 4.1.0 js-yaml: 4.1.0
lodash-es: 4.17.21 lodash-es: 4.17.21
@ -8034,7 +7948,7 @@ snapshots:
dependencies: dependencies:
'@napi-rs/lzma': 1.3.1 '@napi-rs/lzma': 1.3.1
'@napi-rs/tar': 0.1.1 '@napi-rs/tar': 0.1.1
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8686,8 +8600,6 @@ snapshots:
'@types/minimist@1.2.5': {} '@types/minimist@1.2.5': {}
'@types/mocha@10.0.7': {}
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 20.14.11 '@types/node': 20.14.11
@ -9006,7 +8918,7 @@ snapshots:
agent-base@7.1.1: agent-base@7.1.1:
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -9028,8 +8940,6 @@ snapshots:
json-schema-traverse: 1.0.0 json-schema-traverse: 1.0.0
require-from-string: 2.0.2 require-from-string: 2.0.2
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2: ansi-escapes@4.3.2:
dependencies: dependencies:
type-fest: 0.21.3 type-fest: 0.21.3
@ -9256,8 +9166,6 @@ snapshots:
p-queue: 6.6.2 p-queue: 6.6.2
unload: 2.4.1 unload: 2.4.1
browser-stdout@1.3.1: {}
browserify-zlib@0.1.4: browserify-zlib@0.1.4:
dependencies: dependencies:
pako: 0.2.9 pako: 0.2.9
@ -9756,11 +9664,9 @@ snapshots:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
debug@4.3.5(supports-color@8.1.1): debug@4.3.5:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
optionalDependencies:
supports-color: 8.1.1
decamelize-keys@1.1.1: decamelize-keys@1.1.1:
dependencies: dependencies:
@ -9769,8 +9675,6 @@ snapshots:
decamelize@1.2.0: {} decamelize@1.2.0: {}
decamelize@4.0.0: {}
decimal.js@10.4.3: {} decimal.js@10.4.3: {}
decompress-response@6.0.0: decompress-response@6.0.0:
@ -9828,15 +9732,6 @@ snapshots:
deep-equal@1.0.1: {} deep-equal@1.0.1: {}
deepl-node@1.13.0:
dependencies:
'@types/node': 20.14.11
axios: 1.7.2
form-data: 3.0.1
loglevel: 1.9.1
transitivePeerDependencies:
- debug
deepmerge@4.3.1: {} deepmerge@4.3.1: {}
defer-to-connect@2.0.1: {} defer-to-connect@2.0.1: {}
@ -9872,8 +9767,6 @@ snapshots:
diff@4.0.2: {} diff@4.0.2: {}
diff@5.2.0: {}
dijkstrajs@1.0.3: {} dijkstrajs@1.0.3: {}
dir-glob@3.0.1: dir-glob@3.0.1:
@ -10226,11 +10119,6 @@ snapshots:
locate-path: 5.0.0 locate-path: 5.0.0
path-exists: 4.0.0 path-exists: 4.0.0
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
fix-esm@1.0.1: fix-esm@1.0.1:
dependencies: dependencies:
'@babel/core': 7.24.9 '@babel/core': 7.24.9
@ -10239,8 +10127,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
flat@5.0.2: {}
fluent-ffmpeg@2.1.3: fluent-ffmpeg@2.1.3:
dependencies: dependencies:
async: 0.2.10 async: 0.2.10
@ -10268,12 +10154,6 @@ snapshots:
form-data-encoder@4.0.2: {} form-data-encoder@4.0.2: {}
form-data@3.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.0: form-data@4.0.0:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
@ -10380,14 +10260,6 @@ snapshots:
once: 1.4.0 once: 1.4.0
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
glob@8.1.0:
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.6
once: 1.4.0
globals@11.12.0: {} globals@11.12.0: {}
globby@11.1.0: globby@11.1.0:
@ -10540,7 +10412,7 @@ snapshots:
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
dependencies: dependencies:
agent-base: 7.1.1 agent-base: 7.1.1
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -10557,7 +10429,7 @@ snapshots:
https-proxy-agent@7.0.5: https-proxy-agent@7.0.5:
dependencies: dependencies:
agent-base: 7.1.1 agent-base: 7.1.1
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -10623,7 +10495,7 @@ snapshots:
dependencies: dependencies:
'@ioredis/commands': 1.2.0 '@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2 cluster-key-slot: 1.1.2
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
denque: 2.1.0 denque: 2.1.0
lodash.defaults: 4.2.0 lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0 lodash.isarguments: 3.1.0
@ -10708,8 +10580,6 @@ snapshots:
is-plain-obj@1.1.0: {} is-plain-obj@1.1.0: {}
is-plain-obj@2.1.0: {}
is-plain-obj@4.1.0: {} is-plain-obj@4.1.0: {}
is-plain-object@5.0.0: {} is-plain-object@5.0.0: {}
@ -10781,7 +10651,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1: istanbul-lib-source-maps@4.0.1:
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
istanbul-lib-coverage: 3.2.2 istanbul-lib-coverage: 3.2.2
source-map: 0.6.1 source-map: 0.6.1
transitivePeerDependencies: transitivePeerDependencies:
@ -11309,7 +11179,7 @@ snapshots:
koa-mount@4.0.0: koa-mount@4.0.0:
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
koa-compose: 4.1.0 koa-compose: 4.1.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -11318,7 +11188,7 @@ snapshots:
koa-router@10.1.1: koa-router@10.1.1:
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
http-errors: 1.8.1 http-errors: 1.8.1
koa-compose: 4.1.0 koa-compose: 4.1.0
methods: 1.1.2 methods: 1.1.2
@ -11328,7 +11198,7 @@ snapshots:
koa-send@5.0.1: koa-send@5.0.1:
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
http-errors: 1.8.1 http-errors: 1.8.1
resolve-path: 1.4.0 resolve-path: 1.4.0
transitivePeerDependencies: transitivePeerDependencies:
@ -11344,7 +11214,7 @@ snapshots:
koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3): koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3):
dependencies: dependencies:
consolidate: 0.16.0(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3) consolidate: 0.16.0(ejs@3.1.10)(lodash@4.17.21)(pug@3.0.3)
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
get-paths: 0.0.7 get-paths: 0.0.7
koa-send: 5.0.1 koa-send: 5.0.1
mz: 2.7.0 mz: 2.7.0
@ -11415,7 +11285,7 @@ snapshots:
content-disposition: 0.5.4 content-disposition: 0.5.4
content-type: 1.0.5 content-type: 1.0.5
cookies: 0.8.0 cookies: 0.8.0
debug: 4.3.3 debug: 4.3.5
delegates: 1.0.0 delegates: 1.0.0
depd: 2.0.0 depd: 2.0.0
destroy: 1.2.0 destroy: 1.2.0
@ -11443,7 +11313,7 @@ snapshots:
content-disposition: 0.5.4 content-disposition: 0.5.4
content-type: 1.0.5 content-type: 1.0.5
cookies: 0.9.1 cookies: 0.9.1
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
delegates: 1.0.0 delegates: 1.0.0
depd: 2.0.0 depd: 2.0.0
destroy: 1.2.0 destroy: 1.2.0
@ -11492,10 +11362,6 @@ snapshots:
dependencies: dependencies:
p-locate: 4.1.0 p-locate: 4.1.0
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {} lodash-es@4.17.21: {}
lodash.assignin@4.2.0: {} lodash.assignin@4.2.0: {}
@ -11535,8 +11401,6 @@ snapshots:
chalk: 4.1.2 chalk: 4.1.2
is-unicode-supported: 0.1.0 is-unicode-supported: 0.1.0
loglevel@1.9.1: {}
long@5.2.3: {} long@5.2.3: {}
lowercase-keys@2.0.0: {} lowercase-keys@2.0.0: {}
@ -11661,29 +11525,6 @@ snapshots:
mkdirp@2.1.6: {} mkdirp@2.1.6: {}
mocha@10.6.0:
dependencies:
ansi-colors: 4.1.3
browser-stdout: 1.3.1
chokidar: 3.6.0
debug: 4.3.5(supports-color@8.1.1)
diff: 5.2.0
escape-string-regexp: 4.0.0
find-up: 5.0.0
glob: 8.1.0
he: 1.2.0
js-yaml: 4.1.0
log-symbols: 4.1.0
minimatch: 5.1.6
ms: 2.1.3
serialize-javascript: 6.0.2
strip-json-comments: 3.1.1
supports-color: 8.1.1
workerpool: 6.5.1
yargs: 16.2.0
yargs-parser: 20.2.9
yargs-unparser: 2.0.0
mock-socket@9.3.1: {} mock-socket@9.3.1: {}
moment@2.30.1: {} moment@2.30.1: {}
@ -11844,8 +11685,6 @@ snapshots:
only@0.0.2: {} only@0.0.2: {}
opencc-js@1.0.5: {}
opencollective-postinstall@2.0.3: {} opencollective-postinstall@2.0.3: {}
opentype.js@0.4.11: {} opentype.js@0.4.11: {}
@ -11874,10 +11713,6 @@ snapshots:
dependencies: dependencies:
p-limit: 2.3.0 p-limit: 2.3.0
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
p-queue@6.6.2: p-queue@6.6.2:
dependencies: dependencies:
eventemitter3: 4.0.7 eventemitter3: 4.0.7
@ -12359,7 +12194,7 @@ snapshots:
redis-semaphore@5.6.0(ioredis@5.4.1): redis-semaphore@5.6.0(ioredis@5.4.1):
dependencies: dependencies:
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
optionalDependencies: optionalDependencies:
ioredis: 5.4.1 ioredis: 5.4.1
transitivePeerDependencies: transitivePeerDependencies:
@ -13020,7 +12855,7 @@ snapshots:
chalk: 4.1.2 chalk: 4.1.2
cli-highlight: 2.1.11 cli-highlight: 2.1.11
dayjs: 1.11.12 dayjs: 1.11.12
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
dotenv: 16.4.5 dotenv: 16.4.5
glob: 10.4.5 glob: 10.4.5
mkdirp: 2.1.6 mkdirp: 2.1.6
@ -13146,7 +12981,7 @@ snapshots:
vite-plugin-compression@0.5.1(vite@5.3.4(@types/node@20.14.11)(sass@1.77.8)(terser@5.31.3)): vite-plugin-compression@0.5.1(vite@5.3.4(@types/node@20.14.11)(sass@1.77.8)(terser@5.31.3)):
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
debug: 4.3.5(supports-color@8.1.1) debug: 4.3.5
fs-extra: 10.1.0 fs-extra: 10.1.0
vite: 5.3.4(@types/node@20.14.11)(sass@1.77.8)(terser@5.31.3) vite: 5.3.4(@types/node@20.14.11)(sass@1.77.8)(terser@5.31.3)
transitivePeerDependencies: transitivePeerDependencies:
@ -13315,8 +13150,6 @@ snapshots:
assert-never: 1.3.0 assert-never: 1.3.0
babel-walk: 3.0.0-canary-5 babel-walk: 3.0.0-canary-5
workerpool@6.5.1: {}
wrap-ansi@6.2.0: wrap-ansi@6.2.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@ -13392,13 +13225,6 @@ snapshots:
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs-unparser@2.0.0:
dependencies:
camelcase: 6.3.0
decamelize: 4.0.0
flat: 5.0.2
is-plain-obj: 2.1.0
yargs@15.4.1: yargs@15.4.1:
dependencies: dependencies:
cliui: 6.0.0 cliui: 6.0.0