Compare commits

...

5 Commits

Author SHA1 Message Date
laozhoubuluo 97623e6a71 Merge branch 'feat/post_import_export' into 'develop'
feat: import firefish renote and reply from export, import self-reply from mastodon export

Closes #9947, #10661, and #10807

See merge request firefish/firefish!10689
2024-05-15 08:48:59 +00:00
naskya 28e2a24585
chore (backend-rs): cleanup 2024-05-15 16:45:35 +09:00
naskya 2884b2fb42
chore (backend-rs): apply clippy fix 2024-05-15 16:36:26 +09:00
naskya d8e1ab63c0
refactor: port system information checker to backend-rs
network stat is removed because it might be inaccurate and/or
it should be monitored by other system tools, but it may be added back
later if it is wanted
2024-05-15 16:26:46 +09:00
老周部落 469ca68e2e
feat: import firefish renote and reply from export, import self-reply from mastodon export 2024-05-12 09:42:19 +08:00
42 changed files with 458 additions and 423 deletions

35
Cargo.lock generated
View File

@ -236,6 +236,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"strum 0.26.2",
"sysinfo",
"thiserror",
"tokio",
"tracing",
@ -1644,6 +1645,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -3167,6 +3177,21 @@ dependencies = [
"syn 2.0.62",
]
[[package]]
name = "sysinfo"
version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"ntapi",
"once_cell",
"rayon",
"windows",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -3673,6 +3698,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-core"
version = "0.52.0"

View File

@ -35,6 +35,7 @@ serde_json = "1.0.117"
serde_yaml = "0.9.34"
strum = "0.26.2"
syn = "2.0.62"
sysinfo = "0.30.12"
thiserror = "1.0.60"
tokio = "1.37.0"
tracing = "0.1.40"

View File

@ -4,6 +4,8 @@ Breaking changes are indicated by the :warning: icon.
## Unreleased
- :warning: `server-info` (an endpoint to get server hardware information) now requires credentials.
- :warning: `net` (server's default network interface) has been removed from `admin/server-info`.
- Adding `lang` to the response of `i` and the request parameter of `i/update`.
## v20240504

View File

@ -74,6 +74,34 @@ mentions: "Mentions"
directNotes: "Direct messages"
cw: "Content warning"
importAndExport: "Import/Export Data"
importAndExportWarn: "The Import/Export Data feature is an experimental feature and
implementation may change at any time without prior notice.\n
Due to differences in the exported data of different software versions, the actual
conditions of the import program, and the server health of the exported data link,
the imported data may be incomplete or the access permissions may not be set
correctly (for example, there is no access permission mark in the
Mastodon/Akkoma/Pleroma exported data, so all posts makes public after import),
so please be sure to check the imported data carefully integrity and configure
the correct access permissions for it."
importAndExportInfo: "Since some data cannot be obtained after the original account is
frozen or the original server goes offline, it is strongly recommendedthat you import
the data before the original account is frozen (migrated, logged out) or the original
server goes offline.\n
If the original account is frozen or the original server is offline but you have the
original images, you can try uploading them to the network disk before importing the
data, which may help with data import.\n
Since some data is obtained from its server using your current account when importing
data, data that the current account does not have permission to access will be regarded
as broken. Please make adjustments including but not limited to access permissions,
Manually following accounts and other methods allow the current account to obtain
relevant data, so that the import program can normally obtain the data it needs to
obtain to help you import.\n
Since it is impossible to confirm whether the broken link content posted by someone other
than you is posted by him/her, if there is broken link content posted by others in the
discussion thread, the related content and subsequent replies will not be imported.\n
Since data import is greatly affected by network communication, it is recommended that you
pay attention to data recovery after a period of time. If the data is still not restored,
you can try importing the same backup file again and try again."
import: "Import"
export: "Export"
files: "Files"

View File

@ -61,6 +61,16 @@ mention: "提及"
mentions: "提及"
directNotes: "私信"
importAndExport: "导入 / 导出数据"
importAndExportWarn: "导入 / 导出数据功能是一项实验性功能,实现可能会随时变化而无预先通知。\n
由于不同软件不同版本的导出数据、导入程序实际情况以及导出数据链接的服务器运行状况不同,导入的数据可能会不完整或未被正确设置访问权限
(例如 Mastodon/Akkoma/Pleroma 导出数据内无访问权限标记,因此所有帖子导入后均为公开状态),因此请务必谨慎核对导入数据的完整性,
并为其配置正确的访问权限。"
importAndExportInfo: "由于原账号冻结或者原服务器下线后部分数据无法获取,因此强烈建议您在原账号冻结(迁移、注销)或原服务器下线前导入数据。\n
在原账号冻结或者原服务器下线但您拥有原始图片的情况下,可以尝试在导入数据之前将其上传到网盘上,可能对数据导入有所帮助。\n
由于导入数据时部分数据是使用您当前账号到其服务器上获取,因此当前账号无权访问的数据会视为断链。请通过包括但不限于访问权限调整、
手动关注账户等方式让当前帐号可以获取到相关数据,以便导入程序能够正常获取到需要获取的数据从而帮助您进行导入。\n
由于无法确认非您本人发表的断链内容的是否由其本人发表,因此如果讨论串内有其他人发表的断链内容,则相关内容以及后续回复不会被导入。\n
由于数据导入受网络通信影响较大,因此建议您一段时间之后再关注数据恢复情况。如果数据仍未恢复可以尝试再次导入同样的备份文件重试一次。"
import: "导入"
export: "导出"
files: "文件"

View File

@ -39,6 +39,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
strum = { workspace = true, features = ["derive"] }
sysinfo = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }

View File

@ -212,6 +212,8 @@ export interface Acct {
}
export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
export function initializeRustLogger(): void
export function showServerInfo(): void
export function addNoteToAntenna(antennaId: string, note: Note): void
/**
* Checks if a server is blocked.
@ -299,6 +301,28 @@ export function countReactions(reactions: Record<string, number>): Record<string
export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
/** Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago */
export function removeOldAttestationChallenges(): Promise<void>
export interface Cpu {
model: string
cores: number
}
export interface Memory {
/** Total memory amount in bytes */
total: number
/** Used memory amount in bytes */
used: number
/** Available (for (re)use) memory amount in bytes */
available: number
}
export interface Storage {
/** Total storage space in bytes */
total: number
/** Used storage space in bytes */
used: number
}
export function cpuInfo(): Cpu
export function cpuUsage(): number
export function memoryUsage(): Memory
export function storageUsage(): Storage | null
export interface AbuseUserReport {
id: string
createdAt: Date
@ -1156,7 +1180,6 @@ export interface Webhook {
latestSentAt: Date | null
latestStatus: number | null
}
export function initializeRustLogger(): void
export function fetchNodeinfo(host: string): Promise<Nodeinfo>
export function nodeinfo_2_1(): Promise<any>
export function nodeinfo_2_0(): Promise<any>

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, initializeRustLogger, showServerInfo, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -323,6 +323,8 @@ module.exports.loadEnv = loadEnv
module.exports.loadConfig = loadConfig
module.exports.stringToAcct = stringToAcct
module.exports.acctToString = acctToString
module.exports.initializeRustLogger = initializeRustLogger
module.exports.showServerInfo = showServerInfo
module.exports.addNoteToAntenna = addNoteToAntenna
module.exports.isBlockedServer = isBlockedServer
module.exports.isSilencedServer = isSilencedServer
@ -353,6 +355,10 @@ module.exports.decodeReaction = decodeReaction
module.exports.countReactions = countReactions
module.exports.toDbReaction = toDbReaction
module.exports.removeOldAttestationChallenges = removeOldAttestationChallenges
module.exports.cpuInfo = cpuInfo
module.exports.cpuUsage = cpuUsage
module.exports.memoryUsage = memoryUsage
module.exports.storageUsage = storageUsage
module.exports.AntennaSrcEnum = AntennaSrcEnum
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
@ -364,7 +370,6 @@ module.exports.RelayStatusEnum = RelayStatusEnum
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
module.exports.initializeRustLogger = initializeRustLogger
module.exports.fetchNodeinfo = fetchNodeinfo
module.exports.nodeinfo_2_1 = nodeinfo_2_1
module.exports.nodeinfo_2_0 = nodeinfo_2_0

View File

@ -22,7 +22,7 @@ struct ServerConfig {
pub proxy_bypass_hosts: Option<Vec<String>>,
pub allowed_private_networks: Option<Vec<String>>,
/// `NapiValue` is not implemented for `u64`
// TODO: i64 -> u64 (NapiValue is not implemented for u64)
pub max_file_size: Option<i64>,
pub access_log: Option<String>,
pub cluster_limits: Option<WorkerConfigInternal>,
@ -298,7 +298,7 @@ fn read_manifest() -> Manifest {
}
#[crate::export]
fn load_config() -> Config {
pub fn load_config() -> Config {
let server_config = read_config_file();
let version = read_meta().version;
let manifest = read_manifest();

View File

@ -1,8 +1,9 @@
use crate::config::CONFIG;
use once_cell::sync::OnceCell;
use sea_orm::{ConnectOptions, Database, DbConn, DbErr};
use tracing::log::LevelFilter;
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
static DB_CONN: OnceCell<DbConn> = OnceCell::new();
async fn init_database() -> Result<&'static DbConn, DbErr> {
let database_uri = format!(

View File

@ -1,7 +1,8 @@
use crate::config::CONFIG;
use once_cell::sync::OnceCell;
use redis::{Client, Connection, RedisError};
static REDIS_CLIENT: once_cell::sync::OnceCell<Client> = once_cell::sync::OnceCell::new();
static REDIS_CLIENT: OnceCell<Client> = OnceCell::new();
fn init_redis() -> Result<Client, RedisError> {
let redis_url = {
@ -26,7 +27,7 @@ fn init_redis() -> Result<Client, RedisError> {
params.concat()
};
tracing::info!("Initializing Redis connection");
tracing::info!("Initializing Redis client");
Client::open(redis_url)
}
@ -38,8 +39,8 @@ pub fn redis_conn() -> Result<Connection, RedisError> {
}
}
#[inline]
/// prefix redis key
#[inline]
pub fn key(key: impl ToString) -> String {
format!("{}:{}", CONFIG.redis_key_prefix, key.to_string())
}

View File

@ -0,0 +1,39 @@
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
use sysinfo::System;
pub type SystemMutexError = PoisonError<MutexGuard<'static, System>>;
// TODO: handle this in a more proper way when we move the entry point to backend-rs
pub fn system() -> Result<MutexGuard<'static, System>, SystemMutexError> {
pub static SYSTEM: OnceLock<Mutex<System>> = OnceLock::new();
SYSTEM.get_or_init(|| Mutex::new(System::new_all())).lock()
}
#[crate::export]
pub fn show_server_info() -> Result<(), SystemMutexError> {
let system_info = system()?;
tracing::info!(
"Hostname: {}",
System::host_name().unwrap_or("unknown".to_string())
);
tracing::info!(
"OS: {}",
System::long_os_version().unwrap_or("unknown".to_string())
);
tracing::info!(
"Kernel: {}",
System::kernel_version().unwrap_or("unknown".to_string())
);
tracing::info!(
"CPU architecture: {}",
System::cpu_arch().unwrap_or("unknown".to_string())
);
tracing::info!("CPU threads: {}", system_info.cpus().len());
tracing::info!("Total memory: {} MiB", system_info.total_memory() / 1048576);
tracing::info!("Free memory: {} MiB", system_info.free_memory() / 1048576);
tracing::info!("Total swap: {} MiB", system_info.total_swap() / 1048576);
tracing::info!("Free swap: {} MiB", system_info.free_swap() / 1048576);
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod hardware_stats;
pub mod log;

View File

@ -3,6 +3,7 @@ pub use macro_rs::{export, ts_only_warn};
pub mod config;
pub mod database;
pub mod federation;
pub mod init;
pub mod misc;
pub mod model;
pub mod service;

View File

@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
use sea_orm::{prelude::*, QuerySelect};
/// TODO: handle name collisions better
// TODO: handle name collisions in a better way
#[crate::export(object, js_name = "NoteLikeForCheckWordMute")]
pub struct NoteLike {
pub file_ids: Vec<String>,

View File

@ -1,4 +1,4 @@
/// TODO: handle name collisions better
// TODO: handle name collisions in a better way
#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
pub struct NoteLike {
pub file_ids: Vec<String>,

View File

@ -0,0 +1,90 @@
use crate::init::hardware_stats::{system, SystemMutexError};
use sysinfo::{Disks, MemoryRefreshKind};
// TODO: i64 -> u64 (we can't export u64 to Node.js)
#[crate::export(object)]
pub struct Cpu {
pub model: String,
// TODO: u16 -> usize (we can't export usize to Node.js)
pub cores: u16,
}
#[crate::export(object)]
pub struct Memory {
/// Total memory amount in bytes
pub total: i64,
/// Used memory amount in bytes
pub used: i64,
/// Available (for (re)use) memory amount in bytes
pub available: i64,
}
#[crate::export(object)]
pub struct Storage {
/// Total storage space in bytes
pub total: i64,
/// Used storage space in bytes
pub used: i64,
}
#[crate::export]
pub fn cpu_info() -> Result<Cpu, SystemMutexError> {
let system_info = system()?;
Ok(Cpu {
model: match system_info.cpus() {
[] => {
tracing::debug!("failed to get CPU info");
"unknown".to_string()
}
cpus => cpus[0].brand().to_string(),
},
cores: system_info.cpus().len() as u16,
})
}
#[crate::export]
pub fn cpu_usage() -> Result<f32, SystemMutexError> {
let mut system_info = system()?;
system_info.refresh_cpu_usage();
let total_cpu_usage: f32 = system_info.cpus().iter().map(|cpu| cpu.cpu_usage()).sum();
let cpu_threads = system_info.cpus().len();
Ok(total_cpu_usage / (cpu_threads as f32))
}
#[crate::export]
pub fn memory_usage() -> Result<Memory, SystemMutexError> {
let mut system_info = system()?;
system_info.refresh_memory_specifics(MemoryRefreshKind::new().with_ram());
Ok(Memory {
total: system_info.total_memory() as i64,
used: system_info.used_memory() as i64,
available: system_info.available_memory() as i64,
})
}
#[crate::export]
pub fn storage_usage() -> Option<Storage> {
// Get the first disk that is actualy used.
let disks = Disks::new_with_refreshed_list();
let disk = disks
.iter()
.find(|disk| disk.available_space() > 0 && disk.total_space() > disk.available_space());
if let Some(disk) = disk {
let total = disk.total_space() as i64;
let available = disk.available_space() as i64;
return Some(Storage {
total,
used: total - available,
});
}
tracing::debug!("failed to get stats");
None
}

View File

@ -7,6 +7,7 @@ pub mod escape_sql;
pub mod format_milliseconds;
pub mod get_image_size;
pub mod get_note_summary;
pub mod hardware_stats;
pub mod is_safe_url;
pub mod latest_version;
pub mod mastodon_id;

View File

@ -1,4 +1,3 @@
pub mod log;
pub mod nodeinfo;
pub mod note;
pub mod stream;

View File

@ -87,7 +87,6 @@
"node-fetch": "3.3.2",
"nodemailer": "6.9.13",
"opencc-js": "1.0.5",
"os-utils": "0.0.14",
"otpauth": "9.2.4",
"parse5": "7.1.2",
"pg": "8.11.5",
@ -111,7 +110,6 @@
"stringz": "2.1.0",
"summaly": "2.7.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.22.8",
"tar-stream": "3.1.7",
"tesseract.js": "5.1.0",
"tinycolor2": "1.6.0",

View File

@ -1,33 +0,0 @@
declare module "os-utils" {
type FreeCommandCallback = (usedmem: number) => void;
type HarddriveCallback = (total: number, free: number, used: number) => void;
type GetProcessesCallback = (result: string) => void;
type CPUCallback = (perc: number) => void;
export function platform(): NodeJS.Platform;
export function cpuCount(): number;
export function sysUptime(): number;
export function processUptime(): number;
export function freemem(): number;
export function totalmem(): number;
export function freememPercentage(): number;
export function freeCommand(callback: FreeCommandCallback): void;
export function harddrive(callback: HarddriveCallback): void;
export function getProcesses(callback: GetProcessesCallback): void;
export function getProcesses(
nProcess: number,
callback: GetProcessesCallback,
): void;
export function allLoadavg(): string;
export function loadavg(_time?: number): number;
export function cpuFree(callback: CPUCallback): void;
export function cpuUsage(callback: CPUCallback): void;
}

View File

@ -12,10 +12,10 @@ import {
fetchMeta,
initializeRustLogger,
removeOldAttestationChallenges,
showServerInfo,
type Config,
} from "backend-rs";
import { config, envOption } from "@/config.js";
import { showMachineInfo } from "@/misc/show-machine-info.js";
import { db, initDb } from "@/db/postgre.js";
import { inspect } from "node:util";
@ -93,12 +93,12 @@ function greet() {
export async function masterMain() {
// initialize app
try {
initializeRustLogger();
greet();
showEnvironment();
await showMachineInfo(bootLogger);
showServerInfo();
showNodejsVersion();
await connectDb();
initializeRustLogger();
} catch (e) {
bootLogger.error(
`Fatal error occurred during initialization:\n${inspect(e)}`,

View File

@ -1,15 +1,8 @@
import si from "systeminformation";
import Xev from "xev";
import * as osUtils from "os-utils";
import { fetchMeta } from "backend-rs";
import { fetchMeta, cpuUsage, memoryUsage } from "backend-rs";
const ev = new Xev();
const interval = 2000;
const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
const round = (num: number) => Math.round(num * 10) / 10;
/**
* Report server stats regularly
*/
@ -24,26 +17,9 @@ export default async function () {
if (!meta.enableServerMachineStats) return;
async function tick() {
const cpu = await cpuUsage();
const memStats = await mem();
const netStats = await net();
const fsStats = await fs();
const stats = {
cpu: roundCpu(cpu),
mem: {
used: round(memStats.used - memStats.buffers - memStats.cached),
active: round(memStats.active),
total: round(memStats.total),
},
net: {
rx: round(Math.max(0, netStats.rx_sec)),
tx: round(Math.max(0, netStats.tx_sec)),
},
fs: {
r: round(Math.max(0, fsStats.rIO_sec ?? 0)),
w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
},
cpu: cpuUsage(),
mem: memoryUsage(),
};
ev.emit("serverStats", stats);
log.unshift(stats);
@ -52,33 +28,5 @@ export default async function () {
tick();
setInterval(tick, interval);
}
// CPU STAT
function cpuUsage(): Promise<number> {
return new Promise((res, rej) => {
osUtils.cpuUsage((cpuUsage) => {
res(cpuUsage);
});
});
}
// MEMORY STAT
async function mem() {
const data = await si.mem();
return data;
}
// NETWORK STAT
async function net() {
const iface = await si.networkInterfaceDefault();
const data = await si.networkStats(iface);
return data[0];
}
// FS STAT
async function fs() {
const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
return data || { rIO_sec: 0, wIO_sec: 0 };
setInterval(tick, 3000);
}

View File

@ -1,17 +0,0 @@
import * as os from "node:os";
import sysUtils from "systeminformation";
import type Logger from "@/services/logger.js";
export async function showMachineInfo(parentLogger: Logger) {
const logger = parentLogger.createSubLogger("machine");
logger.debug(`Hostname: ${os.hostname()}`);
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
const mem = await sysUtils.mem();
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
logger.debug(
`CPU: ${
os.cpus().length
} core MEM: ${totalmem}GB (available: ${availmem}GB)`,
);
}

View File

@ -335,6 +335,7 @@ export function createImportMastoPostJob(
user: ThinUser,
post: any,
signatureCheck: boolean,
parent: Note | null = null,
) {
return dbQueue.add(
"importMastoPost",
@ -342,6 +343,7 @@ export function createImportMastoPostJob(
user: user,
post: post,
signatureCheck: signatureCheck,
parent: parent,
},
{
removeOnComplete: true,

View File

@ -11,6 +11,7 @@ import type { Poll } from "@/models/entities/poll.js";
import type { DbUserJobData } from "@/queue/types.js";
import { createTemp } from "@/misc/create-temp.js";
import { inspect } from "node:util";
import { config } from "@/config.js";
const logger = queueLogger.createSubLogger("export-notes");
@ -131,5 +132,6 @@ async function serialize(
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
localOnly: note.localOnly,
objectUrl: `${config.url}/notes/${note.id}`,
};
}

View File

@ -1,12 +1,14 @@
import * as Post from "@/misc/post.js";
import create from "@/services/note/create.js";
import { NoteFiles, Users } from "@/models/index.js";
import Resolver from "@/remote/activitypub/resolver.js";
import { DriveFiles, NoteFiles, Users } from "@/models/index.js";
import type { DbUserImportMastoPostJobData } from "@/queue/types.js";
import { queueLogger } from "../../logger.js";
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
import type { DriveFile } from "@/models/entities/drive-file.js";
import type Bull from "bull";
import { createImportCkPostJob } from "@/queue/index.js";
import { resolveNote } from "@/remote/activitypub/models/note.js";
import { Notes, NoteEdits } from "@/models/index.js";
import type { Note } from "@/models/entities/note.js";
import { genId } from "backend-rs";
@ -23,20 +25,37 @@ export async function importCkPost(
return;
}
const post = job.data.post;
/*
if (post.replyId != null) {
done();
return;
const parent = job.data.parent;
const isRenote = post.renoteId !== null;
let reply: Note | null = null;
let renote: Note | null = null;
job.progress(20);
if (!isRenote && post.replyId !== null) {
if (
!parent &&
typeof post.objectUrl !== "undefined" &&
post.objectUrl !== null
) {
const resolver = new Resolver();
const originalNote = await resolver.resolve(post.objectUrl);
reply = await resolveNote(originalNote.inReplyTo);
} else {
reply = post.replyId !== null ? parent : null;
}
}
if (post.renoteId != null) {
done();
return;
// renote also need resolve original note
if (
isRenote &&
!parent &&
typeof post.objectUrl !== "undefined" &&
post.objectUrl !== null
) {
const resolver = new Resolver();
const originalNote = await resolver.resolve(post.objectUrl);
renote = await resolveNote(originalNote.quoteUrl);
} else {
renote = isRenote ? parent : null;
}
if (post.visibility !== "public") {
done();
return;
}
*/
const urls = (post.files || [])
.map((x: any) => x.url)
.filter((x: String) => x.startsWith("http"));
@ -49,7 +68,17 @@ export async function importCkPost(
});
files.push(file);
} catch (e) {
logger.info(`Skipped adding file to drive: ${url}`);
// try to get the same md5 file from user drive
const md5 = post.files.map((x: any) => x.url).find(url).md5;
const much = await DriveFiles.findOneBy({
md5: md5,
userId: user.id,
});
if (much) {
files.push(much);
} else {
logger.info(`Skipped adding file to drive: ${url}`);
}
}
}
const { text, cw, localOnly, createdAt, visibility } = Post.parse(post);
@ -88,8 +117,8 @@ export async function importCkPost(
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply: post.replyId ? job.data.parent : null,
renote: post.renoteId ? job.data.parent : null,
reply,
renote,
cw: cw,
localOnly,
visibility: visibility,

View File

@ -10,6 +10,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
import { Notes, NoteEdits } from "@/models/index.js";
import type { Note } from "@/models/entities/note.js";
import { genId } from "backend-rs";
import { createImportMastoPostJob } from "@/queue/index.js";
const logger = queueLogger.createSubLogger("import-masto-post");
@ -23,12 +24,17 @@ export async function importMastoPost(
return;
}
const post = job.data.post;
const parent = job.data.parent;
const isRenote = post.type === "Announce";
let reply: Note | null = null;
let renote: Note | null = null;
job.progress(20);
if (!isRenote && post.object.inReplyTo != null) {
reply = await resolveNote(post.object.inReplyTo);
if (parent == null) {
reply = await resolveNote(post.object.inReplyTo);
} else {
reply = parent;
}
}
// renote also need resolve original note
if (isRenote) {
@ -135,4 +141,14 @@ export async function importMastoPost(
done();
logger.info("Imported");
if (post.childNotes) {
for (const child of post.childNotes) {
createImportMastoPostJob(
job.data.user,
child,
job.data.signatureCheck,
note,
);
}
}
}

View File

@ -40,7 +40,10 @@ export async function importPosts(
file.url,
job.data.user.id,
);
for (const post of outbox.orderedItems) {
logger.info("Parsing mastodon style posts");
const arr = recreateChainForMastodon(outbox.orderedItems);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
}
} catch (e) {
@ -60,12 +63,15 @@ export async function importPosts(
if (Array.isArray(parsed)) {
logger.info("Parsing *key posts");
const arr = recreateChain(parsed);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportCkPostJob(job.data.user, post, job.data.signatureCheck);
}
} else if (parsed instanceof Object) {
logger.info("Parsing Mastodon posts");
for (const post of parsed.orderedItems) {
const arr = recreateChainForMastodon(parsed.orderedItems);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
}
}
@ -96,9 +102,56 @@ function recreateChain(arr: any[]): any {
let parent = null;
if (note.replyId != null) {
parent = lookup[`${note.replyId}`];
// Accept URL, let import process to resolveNote
if (
!parent &&
typeof note.objectUrl !== "undefined" &&
note.objectUrl.startsWith("http")
) {
notesTree.push(note);
}
}
if (note.renoteId != null) {
parent = lookup[`${note.renoteId}`];
// Accept URL, let import process to resolveNote
if (
!parent &&
typeof note.objectUrl !== "undefined" &&
note.objectUrl.startsWith("http")
) {
notesTree.push(note);
}
}
if (parent) {
parent.childNotes.push(note);
}
}
return notesTree;
}
function recreateChainForMastodon(arr: any[]): any {
type NotesMap = {
[id: string]: any;
};
const notesTree: any[] = [];
const lookup: NotesMap = {};
for (const note of arr) {
lookup[`${note.id}`] = note;
note.childNotes = [];
if (note.object.inReplyTo == null) {
notesTree.push(note);
}
}
for (const note of arr) {
let parent = null;
if (note.object.inReplyTo != null) {
const inReplyToIdForLookup = `${note.object.inReplyTo}/activity`;
parent = lookup[`${inReplyToIdForLookup}`];
// Accept URL, let import process to resolveNote
if (!parent && note.object.inReplyTo.startsWith("http")) {
notesTree.push(note);
}
}
if (parent) {

View File

@ -1,8 +1,12 @@
import * as os from "node:os";
import si from "systeminformation";
import define from "@/server/api/define.js";
import { redisClient } from "@/db/redis.js";
import { db } from "@/db/postgre.js";
import {
cpuInfo,
memoryUsage,
storageUsage,
} from "backend-rs";
export const meta = {
requireCredential: true,
@ -85,19 +89,6 @@ export const meta = {
},
},
},
net: {
type: "object",
optional: false,
nullable: false,
properties: {
interface: {
type: "string",
optional: false,
nullable: false,
example: "eth0",
},
},
},
},
},
} as const;
@ -109,13 +100,10 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async () => {
const memStats = await si.mem();
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
const redisServerInfo = await redisClient.info("Server");
const m = redisServerInfo.match(new RegExp("^redis_version:(.*)", "m"));
const m = redisServerInfo.match(/^redis_version:(.*)/m);
const redis_version = m?.[1];
const storage = storageUsage();
return {
machine: os.hostname(),
@ -125,19 +113,13 @@ export default define(meta, paramDef, async () => {
.query("SHOW server_version")
.then((x) => x[0].server_version),
redis: redis_version,
cpu: {
model: os.cpus()[0].model,
cores: os.cpus().length,
},
cpu: cpuInfo(),
mem: {
total: memStats.total,
total: memoryUsage().total,
},
fs: {
total: fsStats[0].size,
used: fsStats[0].used,
},
net: {
interface: netInterface,
total: storage?.total ?? 0,
used: storage?.used ?? 0,
},
};
});

View File

@ -1,10 +1,9 @@
import * as os from "node:os";
import si from "systeminformation";
import define from "@/server/api/define.js";
import { fetchMeta } from "backend-rs";
import { fetchMeta, cpuInfo, memoryUsage, storageUsage } from "backend-rs";
export const meta = {
requireCredential: false,
requireCredential: true,
requireCredentialPrivateMode: true,
allowGet: true,
cacheSec: 30,
@ -18,19 +17,8 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async () => {
const memStats = await si.mem();
const fsStats = await si.fsSize();
let fsIndex = 0;
// Get the first index of fs sizes that are actualy used.
for (const [i, stat] of fsStats.entries()) {
if (stat.rw === true && stat.used > 0) {
fsIndex = i;
break;
}
}
const instanceMeta = await fetchMeta(true);
if (!instanceMeta.enableServerMachineStats) {
return {
machine: "Not specified",
@ -47,18 +35,19 @@ export default define(meta, paramDef, async () => {
},
};
}
const memory = memoryUsage();
const storage = storageUsage();
return {
machine: os.hostname(),
cpu: {
model: os.cpus()[0].model,
cores: os.cpus().length,
},
cpu: cpuInfo(),
mem: {
total: memStats.total,
total: memory.total,
},
fs: {
total: fsStats[fsIndex].size,
used: fsStats[fsIndex].used,
total: storage?.total ?? 0,
used: storage?.used ?? 0,
},
};
});

View File

@ -44,7 +44,6 @@ import icon from "@/scripts/icon";
const stream = useStream();
const meta = await os.api("server-info", {});
const serverStats = await os.api("stats");
const cpuUsage = ref(0);

View File

@ -1,5 +1,14 @@
<template>
<div class="_formRoot">
<FormSection>
<template #label>{{ i18n.ts.importAndExport }}</template>
<FormInfo warn class="_formBlock">{{
i18n.ts.importAndExportWarn
}}</FormInfo>
<FormInfo class="_formBlock">{{
i18n.ts.importAndExportInfo
}}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._exportOrImport.allNotes }}</template>
<FormFolder>
@ -177,6 +186,7 @@
<script lang="ts" setup>
import { ref } from "vue";
import FormInfo from "@/components/MkInfo.vue";
import MkButton from "@/components/MkButton.vue";
import FormSection from "@/components/form/section.vue";
import FormFolder from "@/components/form/folder.vue";

View File

@ -36,7 +36,7 @@
/>
<text x="1" y="5">
CPU
<tspan>{{ cpuP }}%</tspan>
<tspan>{{ cpuUsage }}%</tspan>
</text>
</svg>
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
@ -75,7 +75,7 @@
/>
<text x="1" y="5">
MEM
<tspan>{{ memP }}%</tspan>
<tspan>{{ memUsage }}%</tspan>
</text>
</svg>
</div>
@ -87,26 +87,25 @@ import { v4 as uuid } from "uuid";
const props = defineProps<{
connection: any;
meta: any;
}>();
const viewBoxX: number = ref(50);
const viewBoxY: number = ref(30);
const stats: any[] = ref([]);
const viewBoxX = ref(50);
const viewBoxY = ref(30);
const stats = ref<any[]>([]);
const cpuGradientId = uuid();
const cpuMaskId = uuid();
const memGradientId = uuid();
const memMaskId = uuid();
const cpuPolylinePoints: string = ref("");
const memPolylinePoints: string = ref("");
const cpuPolygonPoints: string = ref("");
const memPolygonPoints: string = ref("");
const cpuHeadX: any = ref(null);
const cpuHeadY: any = ref(null);
const memHeadX: any = ref(null);
const memHeadY: any = ref(null);
const cpuP: string = ref("");
const memP: string = ref("");
const cpuPolylinePoints = ref("");
const memPolylinePoints = ref("");
const cpuPolygonPoints = ref("");
const memPolygonPoints = ref("");
const cpuHeadX = ref<number>();
const cpuHeadY = ref<number>();
const memHeadX = ref<number>();
const memHeadY = ref<number>();
const cpuUsage = ref<string>();
const memUsage = ref<string>();
onMounted(() => {
props.connection.on("stats", onStats);
@ -127,11 +126,11 @@ function onStats(connStats) {
const cpuPolylinePointsStats = stats.value.map((s, i) => [
viewBoxX.value - (stats.value.length - 1 - i),
(1 - s.cpu) * viewBoxY.value,
(1 - s.cpu / 100) * viewBoxY.value,
]);
const memPolylinePointsStats = stats.value.map((s, i) => [
viewBoxX.value - (stats.value.length - 1 - i),
(1 - s.mem.active / s.mem.total) * viewBoxY.value,
(1 - s.mem.used / s.mem.total) * viewBoxY.value,
]);
cpuPolylinePoints.value = cpuPolylinePointsStats
.map((xy) => `${xy[0]},${xy[1]}`)
@ -152,8 +151,10 @@ function onStats(connStats) {
memHeadX.value = memPolylinePointsStats[memPolylinePointsStats.length - 1][0];
memHeadY.value = memPolylinePointsStats[memPolylinePointsStats.length - 1][1];
cpuP.value = (connStats.cpu * 100).toFixed(0);
memP.value = ((connStats.mem.active / connStats.mem.total) * 100).toFixed(0);
cpuUsage.value = connStats.cpu.toFixed(1);
memUsage.value = ((connStats.mem.used / connStats.mem.total) * 100).toFixed(
1,
);
}
function onStatsLog(statsLog) {

View File

@ -19,10 +19,10 @@ const props = defineProps<{
meta: any;
}>();
const usage: number = ref(0);
const usage = ref(0);
function onStats(stats) {
usage.value = stats.cpu;
usage.value = stats.cpu / 100;
}
onMounted(() => {

View File

@ -4,7 +4,7 @@
<div>
<p><i :class="icon('ph-hard-drives')"></i>Disk</p>
<p>Total: {{ bytes(total, 1) }}</p>
<p>Free: {{ bytes(available, 1) }}</p>
<p>Available: {{ bytes(available, 1) }}</p>
<p>Used: {{ bytes(used, 1) }}</p>
</div>
</div>
@ -18,7 +18,12 @@ import bytes from "@/filters/bytes";
import icon from "@/scripts/icon";
const props = defineProps<{
meta: any; // TODO
meta: {
fs: {
used: number;
total: number;
};
};
}>();
const usage = computed(() => props.meta.fs.used / props.meta.fs.total);

View File

@ -26,23 +26,18 @@
:connection="connection"
:meta="meta"
/>
<XNet
<XCpu
v-else-if="widgetProps.view === 1"
:connection="connection"
:meta="meta"
/>
<XCpu
<XMemory
v-else-if="widgetProps.view === 2"
:connection="connection"
:meta="meta"
/>
<XMemory
v-else-if="widgetProps.view === 3"
:connection="connection"
:meta="meta"
/>
<XDisk
v-else-if="widgetProps.view === 4"
v-else-if="widgetProps.view === 3"
:connection="connection"
:meta="meta"
/>
@ -52,10 +47,13 @@
<script lang="ts" setup>
import { onUnmounted, ref } from "vue";
import type { Widget, WidgetComponentExpose } from "../widget";
import type {
WidgetComponentEmits,
WidgetComponentExpose,
WidgetComponentProps,
} from "../widget";
import { useWidgetPropsManager } from "../widget";
import XCpuMemory from "./cpu-mem.vue";
import XNet from "./net.vue";
import XCpu from "./cpu.vue";
import XMemory from "./mem.vue";
import XDisk from "./disk.vue";
@ -87,11 +85,8 @@ const widgetPropsDef = {
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
// const props = defineProps<WidgetComponentProps<WidgetProps>>();
// const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps> }>();
const emit = defineEmits<{ (ev: "updateProps", props: WidgetProps) }>();
const props = defineProps<WidgetComponentProps<WidgetProps>>();
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const { widgetProps, configure, save } = useWidgetPropsManager(
name,
@ -107,14 +102,7 @@ os.apiGet("server-info", {}).then((res) => {
});
const toggleView = () => {
if (
(widgetProps.view === 5 && instance.features.searchFilters) ||
(widgetProps.view === 4 && !instance.features.searchFilters)
) {
widgetProps.view = 0;
} else {
widgetProps.view++;
}
widgetProps.view = (widgetProps.view + 1) % 4;
save();
};

View File

@ -5,7 +5,7 @@
<p><i :class="icon('ph-microchip')"></i>RAM</p>
<p>Total: {{ bytes(total, 1) }}</p>
<p>Used: {{ bytes(used, 1) }}</p>
<p>Free: {{ bytes(free, 1) }}</p>
<p>Available: {{ bytes(available, 1) }}</p>
</div>
</div>
</template>
@ -18,19 +18,18 @@ import icon from "@/scripts/icon";
const props = defineProps<{
connection: any;
meta: any;
}>();
const usage = ref<number>(0);
const total = ref<number>(0);
const used = ref<number>(0);
const free = ref<number>(0);
const available = ref<number>(0);
function onStats(stats) {
usage.value = stats.mem.active / stats.mem.total;
usage.value = stats.mem.used / stats.mem.total;
total.value = stats.mem.total;
used.value = stats.mem.active;
free.value = total.value - used.value;
used.value = stats.mem.used;
available.value = stats.mem.available;
}
onMounted(() => {

View File

@ -1,156 +0,0 @@
<template>
<div class="oxxrhrto">
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<polygon
:points="inPolygonPoints"
fill="#f6c177"
fill-opacity="0.5"
/>
<polyline
:points="inPolylinePoints"
fill="none"
stroke="#f6c177"
stroke-width="1"
/>
<circle :cx="inHeadX" :cy="inHeadY" r="1.5" fill="#f6c177" />
<text x="1" y="5">
NET rx
<tspan>{{ bytes(inRecent) }}</tspan>
</text>
</svg>
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
<polygon
:points="outPolygonPoints"
fill="#31748f"
fill-opacity="0.5"
/>
<polyline
:points="outPolylinePoints"
fill="none"
stroke="#31748f"
stroke-width="1"
/>
<circle :cx="outHeadX" :cy="outHeadY" r="1.5" fill="#31748f" />
<text x="1" y="5">
NET tx
<tspan>{{ bytes(outRecent) }}</tspan>
</text>
</svg>
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from "vue";
import bytes from "@/filters/bytes";
const props = defineProps<{
connection: any;
meta: any;
}>();
const viewBoxX: number = ref(50);
const viewBoxY: number = ref(30);
const stats: any[] = ref([]);
const inPolylinePoints: string = ref("");
const outPolylinePoints: string = ref("");
const inPolygonPoints: string = ref("");
const outPolygonPoints: string = ref("");
const inHeadX: any = ref(null);
const inHeadY: any = ref(null);
const outHeadX: any = ref(null);
const outHeadY: any = ref(null);
const inRecent: number = ref(0);
const outRecent: number = ref(0);
onMounted(() => {
props.connection.on("stats", onStats);
props.connection.on("statsLog", onStatsLog);
props.connection.send("requestLog", {
id: Math.random().toString().substring(2, 10),
});
});
onBeforeUnmount(() => {
props.connection.off("stats", onStats);
props.connection.off("statsLog", onStatsLog);
});
function onStats(connStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
const inPeak = Math.max(
1024 * 64,
Math.max(...stats.value.map((s) => s.net.rx)),
);
const outPeak = Math.max(
1024 * 64,
Math.max(...stats.value.map((s) => s.net.tx)),
);
const inPolylinePointsStats = stats.value.map((s, i) => [
viewBoxX.value - (stats.value.length - 1 - i),
(1 - s.net.rx / inPeak) * viewBoxY.value,
]);
const outPolylinePointsStats = stats.value.map((s, i) => [
viewBoxX.value - (stats.value.length - 1 - i),
(1 - s.net.tx / outPeak) * viewBoxY.value,
]);
inPolylinePoints.value = inPolylinePointsStats
.map((xy) => `${xy[0]},${xy[1]}`)
.join(" ");
outPolylinePoints.value = outPolylinePointsStats
.map((xy) => `${xy[0]},${xy[1]}`)
.join(" ");
inPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${
viewBoxY.value
} ${inPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
outPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${
viewBoxY.value
} ${outPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
inHeadX.value = inPolylinePointsStats[inPolylinePointsStats.length - 1][0];
inHeadY.value = inPolylinePointsStats[inPolylinePointsStats.length - 1][1];
outHeadX.value = outPolylinePointsStats[outPolylinePointsStats.length - 1][0];
outHeadY.value = outPolylinePointsStats[outPolylinePointsStats.length - 1][1];
inRecent.value = connStats.net.rx;
outRecent.value = connStats.net.tx;
}
function onStatsLog(statsLog) {
for (const revStats of [...statsLog].reverse()) {
onStats(revStats);
}
}
</script>
<style lang="scss" scoped>
.oxxrhrto {
display: flex;
> svg {
display: block;
padding: 10px;
width: 50%;
&:first-child {
padding-right: 5px;
}
&:last-child {
padding-left: 5px;
}
> text {
font-size: 5px;
fill: currentColor;
> tspan {
opacity: 0.5;
}
}
}
}
</style>

View File

@ -19,7 +19,7 @@
:stroke="color"
/>
<text x="50%" y="50%" dy="0.05" text-anchor="middle">
{{ (value * 100).toFixed(0) }}%
{{ (value * 100).toFixed(1) }}%
</text>
</svg>
</template>

View File

@ -237,9 +237,6 @@ importers:
opencc-js:
specifier: 1.0.5
version: 1.0.5
os-utils:
specifier: 0.0.14
version: 0.0.14
otpauth:
specifier: 9.2.4
version: 9.2.4
@ -309,9 +306,6 @@ importers:
syslog-pro:
specifier: 1.0.0
version: 1.0.0
systeminformation:
specifier: 5.22.8
version: 5.22.8
tar-stream:
specifier: 3.1.7
version: 3.1.7
@ -6210,9 +6204,6 @@ packages:
resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==}
engines: {node: '>=4'}
os-utils@0.0.14:
resolution: {integrity: sha512-ajB8csaHLBvJOYsHJkp8YdO2FvlBbf/ZxaYQwXXRDyQ84UoE+uTuLXxqd0shekXMX6Qr/pt/DDyLMRAMsgfWzg==}
otpauth@9.2.4:
resolution: {integrity: sha512-t0Nioq2Up2ZaT5AbpXZLTjrsNtLc/g/rVSaEThmKLErAuT9mrnAKJryiPOKc3rCH+3ycWBgKpRHYn+DHqfaPiQ==}
@ -7285,12 +7276,6 @@ packages:
resolution: {integrity: sha512-7SNMJKtQBJlwBUp1jxFT7bXya71cnINXPCYJ2AVhlQE4MKL7o2QiPdAXbMdWRiLeykQ2rx+7TNrnoGzvzhO+eA==}
engines: {node: '>=10.0.0'}
systeminformation@5.22.8:
resolution: {integrity: sha512-F1iWQ+PSfOzvLMGh2UXASaWLDq5o+1h1db13Kddl6ojcQ47rsJhpMtRrmBXfTA5QJgutC4KV67YRmXLuroIxrA==}
engines: {node: '>=8.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
syuilo-password-strength@0.0.1:
resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
@ -14397,8 +14382,6 @@ snapshots:
dependencies:
arch: 2.2.0
os-utils@0.0.14: {}
otpauth@9.2.4:
dependencies:
jssha: 3.3.1
@ -15537,8 +15520,6 @@ snapshots:
dependencies:
moment: 2.30.1
systeminformation@5.22.8: {}
syuilo-password-strength@0.0.1: {}
tabbable@6.2.0: {}