Merge branch 'develop' into refactor/push-notification
This commit is contained in:
commit
854030db3b
|
@ -167,7 +167,6 @@ dependencies = [
|
|||
"argon2",
|
||||
"basen",
|
||||
"bcrypt",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"cuid2",
|
||||
"emojis",
|
||||
|
|
|
@ -12,7 +12,6 @@ napi-build = "2.1.3"
|
|||
argon2 = "0.5.3"
|
||||
basen = "0.1.0"
|
||||
bcrypt = "0.15.1"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4.37"
|
||||
convert_case = "0.6.0"
|
||||
cuid2 = "0.1.2"
|
||||
|
|
|
@ -20,7 +20,6 @@ napi-derive = { workspace = true, optional = true }
|
|||
argon2 = { workspace = true, features = ["std"] }
|
||||
basen = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
cfg-if = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cuid2 = { workspace = true }
|
||||
emojis = { workspace = true }
|
||||
|
|
|
@ -264,6 +264,8 @@ export interface DecodedReaction {
|
|||
export function decodeReaction(reaction: string): DecodedReaction
|
||||
export function countReactions(reactions: Record<string, number>): Record<string, number>
|
||||
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 AbuseUserReport {
|
||||
id: string
|
||||
createdAt: Date
|
||||
|
@ -1120,6 +1122,8 @@ export interface Webhook {
|
|||
latestSentAt: Date | null
|
||||
latestStatus: number | null
|
||||
}
|
||||
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
|
||||
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
||||
export enum PushNotificationKind {
|
||||
Generic = 'generic',
|
||||
Chat = 'chat',
|
||||
|
@ -1136,8 +1140,6 @@ export enum ChatEvent {
|
|||
Typing = 'typing'
|
||||
}
|
||||
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
|
||||
/** Initializes Cuid2 generator. Must be called before any [create_id]. */
|
||||
export function initIdGenerator(length: number, fingerprint: string): void
|
||||
export function getTimestamp(id: string): number
|
||||
/**
|
||||
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||
|
@ -1147,5 +1149,7 @@ export function getTimestamp(id: string): number
|
|||
*
|
||||
* Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
||||
*/
|
||||
export function genId(date?: Date | undefined | null): string
|
||||
export function genId(): string
|
||||
/** Generate an ID using a specific datetime */
|
||||
export function genIdAt(date: Date): string
|
||||
export function secureRndstr(length?: number | undefined | null): string
|
||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, PushNotificationKind, sendPushNotification, ChatEvent, publishToChatStream, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
|
||||
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, ChatEvent, publishToChatStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
|
||||
module.exports.loadEnv = loadEnv
|
||||
module.exports.loadConfig = loadConfig
|
||||
|
@ -342,6 +342,7 @@ module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
|
|||
module.exports.decodeReaction = decodeReaction
|
||||
module.exports.countReactions = countReactions
|
||||
module.exports.toDbReaction = toDbReaction
|
||||
module.exports.removeOldAttestationChallenges = removeOldAttestationChallenges
|
||||
module.exports.AntennaSrcEnum = AntennaSrcEnum
|
||||
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
|
||||
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
|
||||
|
@ -353,11 +354,13 @@ module.exports.RelayStatusEnum = RelayStatusEnum
|
|||
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
|
||||
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||
module.exports.watchNote = watchNote
|
||||
module.exports.unwatchNote = unwatchNote
|
||||
module.exports.PushNotificationKind = PushNotificationKind
|
||||
module.exports.sendPushNotification = sendPushNotification
|
||||
module.exports.ChatEvent = ChatEvent
|
||||
module.exports.publishToChatStream = publishToChatStream
|
||||
module.exports.initIdGenerator = initIdGenerator
|
||||
module.exports.getTimestamp = getTimestamp
|
||||
module.exports.genId = genId
|
||||
module.exports.genIdAt = genIdAt
|
||||
module.exports.secureRndstr = secureRndstr
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
use crate::database::{redis_conn, redis_key};
|
||||
use crate::model::entity::note;
|
||||
use crate::service::stream;
|
||||
use crate::util::id::get_timestamp;
|
||||
use redis::{streams::StreamMaxlen, Commands};
|
||||
use crate::util::id::{get_timestamp, InvalidIdErr};
|
||||
use redis::{streams::StreamMaxlen, Commands, RedisError};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisErr(#[from] RedisError),
|
||||
#[error("Invalid ID: {0}")]
|
||||
InvalidIdErr(#[from] InvalidIdErr),
|
||||
#[error("Stream error: {0}")]
|
||||
StreamErr(#[from] stream::Error),
|
||||
}
|
||||
|
||||
type Note = note::Model;
|
||||
|
||||
#[crate::export]
|
||||
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), stream::Error> {
|
||||
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> {
|
||||
// for timeline API
|
||||
redis_conn()?.xadd_maxlen(
|
||||
redis_key(format!("antennaTimeline:{}", antenna_id)),
|
||||
StreamMaxlen::Approx(200),
|
||||
format!("{}-*", get_timestamp(¬e.id)),
|
||||
format!("{}-*", get_timestamp(¬e.id)?),
|
||||
&[("note", ¬e.id)],
|
||||
)?;
|
||||
|
||||
// for streaming API
|
||||
stream::antenna::publish(antenna_id, note)
|
||||
Ok(stream::antenna::publish(antenna_id, note)?)
|
||||
}
|
||||
|
|
|
@ -13,3 +13,4 @@ pub mod nyaify;
|
|||
pub mod password;
|
||||
pub mod reaction;
|
||||
pub mod redis_cache;
|
||||
pub mod remove_old_attestation_challenges;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// TODO: We want to get rid of this
|
||||
|
||||
use crate::database::db_conn;
|
||||
use crate::model::entity::attestation_challenge;
|
||||
use chrono::{Duration, Utc};
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter};
|
||||
|
||||
/// Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago
|
||||
#[crate::export]
|
||||
pub async fn remove_old_attestation_challenges() -> Result<(), DbErr> {
|
||||
attestation_challenge::Entity::delete_many()
|
||||
.filter(attestation_challenge::Column::CreatedAt.lt(Utc::now() - Duration::minutes(5)))
|
||||
.exec(db_conn().await?)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod note;
|
||||
pub mod push_notification;
|
||||
pub mod stream;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod watch;
|
|
@ -0,0 +1,42 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::model::entity::note_watching;
|
||||
use crate::util::id::gen_id;
|
||||
use sea_orm::{ActiveValue, ColumnTrait, DbErr, EntityTrait, ModelTrait, QueryFilter};
|
||||
|
||||
#[crate::export]
|
||||
pub async fn watch_note(
|
||||
watcher_id: &str,
|
||||
note_author_id: &str,
|
||||
note_id: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
if watcher_id != note_author_id {
|
||||
note_watching::Entity::insert(note_watching::ActiveModel {
|
||||
id: ActiveValue::set(gen_id()),
|
||||
created_at: ActiveValue::set(chrono::Local::now().naive_local()),
|
||||
user_id: ActiveValue::Set(watcher_id.to_string()),
|
||||
note_user_id: ActiveValue::Set(note_author_id.to_string()),
|
||||
note_id: ActiveValue::Set(note_id.to_string()),
|
||||
})
|
||||
.exec(db_conn().await?)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn unwatch_note(watcher_id: &str, note_id: &str) -> Result<(), DbErr> {
|
||||
let db = db_conn().await?;
|
||||
|
||||
let entry = note_watching::Entity::find()
|
||||
.filter(note_watching::Column::UserId.eq(watcher_id))
|
||||
.filter(note_watching::Column::NoteId.eq(note_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if let Some(entry) = entry {
|
||||
entry.delete(db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,95 +1,109 @@
|
|||
//! ID generation utility based on [cuid2]
|
||||
|
||||
use crate::config::CONFIG;
|
||||
use basen::BASE36;
|
||||
use cfg_if::cfg_if;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::cmp;
|
||||
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
#[error("ID generator has not been initialized yet")]
|
||||
pub struct ErrorUninitialized;
|
||||
|
||||
static FINGERPRINT: OnceCell<String> = OnceCell::new();
|
||||
static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
|
||||
|
||||
const TIME_2000: i64 = 946_684_800_000;
|
||||
const TIMESTAMP_LENGTH: u16 = 8;
|
||||
const TIMESTAMP_LENGTH: u8 = 8;
|
||||
|
||||
/// Initializes Cuid2 generator. Must be called before any [create_id].
|
||||
#[crate::export]
|
||||
pub fn init_id_generator(length: u16, fingerprint: &str) {
|
||||
/// Initializes Cuid2 generator.
|
||||
fn init_id_generator(length: u8, fingerprint: &str) {
|
||||
FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id()));
|
||||
GENERATOR.get_or_init(move || {
|
||||
cuid2::CuidConstructor::new()
|
||||
// length to pass shoule be greater than or equal to 8.
|
||||
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8))
|
||||
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8).into())
|
||||
.with_fingerprinter(|| FINGERPRINT.get().unwrap().clone())
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns Cuid2 with the length specified by [init_id]. Must be called after
|
||||
/// [init_id], otherwise returns [ErrorUninitialized].
|
||||
pub fn create_id(datetime: &NaiveDateTime) -> Result<String, ErrorUninitialized> {
|
||||
match GENERATOR.get() {
|
||||
None => Err(ErrorUninitialized),
|
||||
Some(gen) => {
|
||||
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
|
||||
Ok(format!(
|
||||
"{:0>8}{}",
|
||||
BASE36.encode_var_len(&date_num),
|
||||
gen.create_id()
|
||||
))
|
||||
}
|
||||
/// Returns Cuid2 with the length specified by [init_id_generator].
|
||||
/// It automatically calls [init_id_generator], if the generator has not been initialized.
|
||||
fn create_id(datetime: &NaiveDateTime) -> String {
|
||||
if GENERATOR.get().is_none() {
|
||||
let length = match &CONFIG.cuid {
|
||||
Some(cuid) => cmp::min(cmp::max(cuid.length.unwrap_or(16), 16), 24),
|
||||
None => 16,
|
||||
};
|
||||
let fingerprint = match &CONFIG.cuid {
|
||||
Some(cuid) => cuid.fingerprint.as_deref().unwrap_or_default(),
|
||||
None => "",
|
||||
};
|
||||
init_id_generator(length, fingerprint);
|
||||
}
|
||||
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
|
||||
format!(
|
||||
"{:0>8}{}",
|
||||
BASE36.encode_var_len(&date_num),
|
||||
GENERATOR.get().unwrap().create_id()
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("Invalid ID: {id}")]
|
||||
pub struct InvalidIdErr {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn get_timestamp(id: &str) -> i64 {
|
||||
pub fn get_timestamp(id: &str) -> Result<i64, InvalidIdErr> {
|
||||
let n: Option<u64> = BASE36.decode_var_len(&id[0..8]);
|
||||
match n {
|
||||
None => -1,
|
||||
Some(n) => n as i64 + TIME_2000,
|
||||
if let Some(n) = n {
|
||||
Ok(n as i64 + TIME_2000)
|
||||
} else {
|
||||
Err(InvalidIdErr { id: id.to_string() })
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "napi")] {
|
||||
use chrono::{DateTime, Utc};
|
||||
/// The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||
/// The minimum and maximum lengths are 16 and 24, respectively.
|
||||
/// With the length of 16, namely 8 for cuid2, roughly 1427399 IDs are needed
|
||||
/// in the same millisecond to reach 50% chance of collision.
|
||||
///
|
||||
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
||||
#[crate::export]
|
||||
pub fn gen_id() -> String {
|
||||
create_id(&Utc::now().naive_utc())
|
||||
}
|
||||
|
||||
/// The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||
/// The minimum and maximum lengths are 16 and 24, respectively.
|
||||
/// With the length of 16, namely 8 for cuid2, roughly 1427399 IDs are needed
|
||||
/// in the same millisecond to reach 50% chance of collision.
|
||||
///
|
||||
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
||||
#[napi_derive::napi]
|
||||
pub fn gen_id(date: Option<DateTime<Utc>>) -> String {
|
||||
create_id(&date.unwrap_or_else(Utc::now).naive_utc()).unwrap()
|
||||
}
|
||||
}
|
||||
/// Generate an ID using a specific datetime
|
||||
#[crate::export]
|
||||
pub fn gen_id_at(date: DateTime<Utc>) -> String {
|
||||
create_id(&date.naive_utc())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use crate::util::id;
|
||||
use chrono::Utc;
|
||||
use super::{gen_id, gen_id_at, get_timestamp};
|
||||
use chrono::{Duration, Utc};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn can_create_and_decode_id() {
|
||||
let now = Utc::now().naive_utc();
|
||||
assert_eq!(id::create_id(&now), Err(id::ErrorUninitialized));
|
||||
id::init_id_generator(16, "");
|
||||
assert_eq!(id::create_id(&now).unwrap().len(), 16);
|
||||
assert_ne!(id::create_id(&now).unwrap(), id::create_id(&now).unwrap());
|
||||
let id1 = thread::spawn(move || id::create_id(&now).unwrap());
|
||||
let id2 = thread::spawn(move || id::create_id(&now).unwrap());
|
||||
let now = Utc::now();
|
||||
assert_eq!(gen_id().len(), 16);
|
||||
assert_ne!(gen_id_at(now), gen_id_at(now));
|
||||
assert_ne!(gen_id(), gen_id());
|
||||
|
||||
let id1 = thread::spawn(move || gen_id_at(now));
|
||||
let id2 = thread::spawn(move || gen_id_at(now));
|
||||
assert_ne!(id1.join().unwrap(), id2.join().unwrap());
|
||||
|
||||
let test_id = id::create_id(&now).unwrap();
|
||||
let timestamp = id::get_timestamp(&test_id);
|
||||
assert_eq!(now.and_utc().timestamp_millis(), timestamp);
|
||||
let test_id = gen_id_at(now);
|
||||
let timestamp = get_timestamp(&test_id).unwrap();
|
||||
assert_eq!(now.timestamp_millis(), timestamp);
|
||||
|
||||
let now_id = gen_id_at(now);
|
||||
let old_id = gen_id_at(now - Duration::milliseconds(1));
|
||||
let future_id = gen_id_at(now + Duration::milliseconds(1));
|
||||
assert!(old_id < now_id);
|
||||
assert!(now_id < future_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import semver from "semver";
|
|||
|
||||
import Logger from "@/services/logger.js";
|
||||
import type { Config } from "backend-rs";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import { fetchMeta, removeOldAttestationChallenges } from "backend-rs";
|
||||
import { config, envOption } from "@/config.js";
|
||||
import { showMachineInfo } from "@/misc/show-machine-info.js";
|
||||
import { db, initDb } from "@/db/postgre.js";
|
||||
|
@ -115,18 +115,14 @@ export async function masterMain() {
|
|||
true,
|
||||
);
|
||||
|
||||
if (
|
||||
!envOption.noDaemons &&
|
||||
config.clusterLimits?.web &&
|
||||
config.clusterLimits?.web >= 1
|
||||
) {
|
||||
if (!envOption.noDaemons) {
|
||||
import("../daemons/server-stats.js").then((x) => x.default());
|
||||
import("../daemons/queue-stats.js").then((x) => x.default());
|
||||
import("../daemons/janitor.js").then((x) => x.default());
|
||||
// Update meta cache every 5 minitues
|
||||
setInterval(() => fetchMeta(false), 1000 * 60 * 5);
|
||||
// Remove old attestation challenges
|
||||
setInterval(() => removeOldAttestationChallenges(), 1000 * 60 * 30);
|
||||
}
|
||||
|
||||
// Update meta cache every 5 minitues
|
||||
setInterval(() => fetchMeta(false), 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
function showEnvironment(): void {
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import cluster from "node:cluster";
|
||||
import { config } from "@/config.js";
|
||||
import { initDb } from "@/db/postgre.js";
|
||||
import { initIdGenerator } from "backend-rs";
|
||||
import os from "node:os";
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
*/
|
||||
export async function workerMain() {
|
||||
const length = Math.min(Math.max(config.cuid?.length ?? 16, 16), 24);
|
||||
const fingerprint = config.cuid?.fingerprint ?? "";
|
||||
initIdGenerator(length, fingerprint);
|
||||
|
||||
await initDb();
|
||||
|
||||
if (!process.env.mode || process.env.mode === "web") {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// TODO: 消したい
|
||||
|
||||
const interval = 30 * 60 * 1000;
|
||||
import { AttestationChallenges } from "@/models/index.js";
|
||||
import { LessThan } from "typeorm";
|
||||
|
||||
/**
|
||||
* Clean up database occasionally
|
||||
*/
|
||||
export default function () {
|
||||
async function tick() {
|
||||
await AttestationChallenges.delete({
|
||||
createdAt: LessThan(new Date(Date.now() - 5 * 60 * 1000)),
|
||||
});
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as util from "node:util";
|
||||
import * as fs from "node:fs/promises";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { createTemp } from "./create-temp.js";
|
||||
import { downloadUrl } from "./download-url.js";
|
||||
|
@ -16,7 +15,7 @@ export async function downloadTextFile(url: string): Promise<string> {
|
|||
// write content at URL to temp file
|
||||
await downloadUrl(url, path);
|
||||
|
||||
const text = await util.promisify(fs.readFile)(path, "utf8");
|
||||
const text = await fs.readFile(path, "utf-8");
|
||||
|
||||
return text;
|
||||
} finally {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as stream from "node:stream";
|
||||
import * as util from "node:util";
|
||||
import * as stream from "node:stream/promises";
|
||||
import got, * as Got from "got";
|
||||
import { config } from "@/config.js";
|
||||
import { getAgentByHostname, StatusError } from "./fetch.js";
|
||||
|
@ -10,8 +9,6 @@ import IPCIDR from "ip-cidr";
|
|||
import PrivateIp from "private-ip";
|
||||
import { isValidUrl } from "./is-valid-url.js";
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
if (!isValidUrl(url)) {
|
||||
throw new StatusError("Invalid URL", 400);
|
||||
|
@ -84,7 +81,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
});
|
||||
|
||||
try {
|
||||
await pipeline(req, fs.createWriteStream(path));
|
||||
await stream.pipeline(req, fs.createWriteStream(path));
|
||||
} catch (e) {
|
||||
if (e instanceof Got.HTTPError) {
|
||||
throw new StatusError(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import { createReadStream } from "node:fs";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as stream from "node:stream";
|
||||
import * as util from "node:util";
|
||||
import * as stream from "node:stream/promises";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import probeImageSize from "probe-image-size";
|
||||
import isSvg from "is-svg";
|
||||
|
@ -9,8 +9,6 @@ import sharp from "sharp";
|
|||
import { encode } from "blurhash";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
export type FileInfo = {
|
||||
size: number;
|
||||
md5: string;
|
||||
|
@ -163,7 +161,7 @@ export async function checkSvg(path: string) {
|
|||
try {
|
||||
const size = await getFileSize(path);
|
||||
if (size > 1 * 1024 * 1024) return false;
|
||||
return isSvg(fs.readFileSync(path));
|
||||
return isSvg(await fs.readFile(path, "utf-8"));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
@ -173,8 +171,7 @@ export async function checkSvg(path: string) {
|
|||
* Get file size
|
||||
*/
|
||||
export async function getFileSize(path: string): Promise<number> {
|
||||
const getStat = util.promisify(fs.stat);
|
||||
return (await getStat(path)).size;
|
||||
return (await fs.stat(path)).size;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -182,7 +179,7 @@ export async function getFileSize(path: string): Promise<number> {
|
|||
*/
|
||||
async function calcHash(path: string): Promise<string> {
|
||||
const hash = crypto.createHash("md5").setEncoding("hex");
|
||||
await pipeline(fs.createReadStream(path), hash);
|
||||
await stream.pipeline(createReadStream(path), hash);
|
||||
return hash.read();
|
||||
}
|
||||
|
||||
|
@ -196,7 +193,7 @@ async function detectImageSize(path: string): Promise<{
|
|||
hUnits: string;
|
||||
orientation?: number;
|
||||
}> {
|
||||
const readable = fs.createReadStream(path);
|
||||
const readable = createReadStream(path);
|
||||
const imageSize = await probeImageSize(readable);
|
||||
readable.destroy();
|
||||
return imageSize;
|
||||
|
@ -214,7 +211,7 @@ function getBlurhash(path: string): Promise<string> {
|
|||
.toBuffer((err, buffer, { width, height }) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
let hash;
|
||||
let hash: string;
|
||||
|
||||
try {
|
||||
hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7);
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import type { Note } from "@/models/entities/note.js";
|
||||
|
||||
export default function (note: Note): boolean {
|
||||
return (
|
||||
note.renoteId != null &&
|
||||
(note.text != null ||
|
||||
note.hasPoll ||
|
||||
(note.fileIds != null && note.fileIds.length > 0))
|
||||
);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import watch from "@/services/note/watch.js";
|
||||
import { watchNote } from "backend-rs";
|
||||
import define from "@/server/api/define.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
@ -34,5 +34,5 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
await watch(user.id, note);
|
||||
await watchNote(user.id, note.userId, note.id);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import unwatch from "@/services/note/unwatch.js";
|
||||
import { unwatchNote } from "backend-rs";
|
||||
import define from "@/server/api/define.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
@ -34,5 +34,5 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
await unwatch(user.id, note);
|
||||
await unwatchNote(user.id, note.id);
|
||||
});
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
addNoteToAntenna,
|
||||
checkWordMute,
|
||||
genId,
|
||||
genIdAt,
|
||||
isSilencedServer,
|
||||
} from "backend-rs";
|
||||
import { countSameRenotes } from "@/misc/count-same-renotes.js";
|
||||
|
@ -711,7 +712,7 @@ async function insertNote(
|
|||
data.createdAt = new Date();
|
||||
}
|
||||
const insert = new Note({
|
||||
id: genId(data.createdAt),
|
||||
id: genIdAt(data.createdAt),
|
||||
createdAt: data.createdAt,
|
||||
fileIds: data.files ? data.files.map((file) => file.id) : [],
|
||||
replyId: data.reply ? data.reply.id : null,
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import { NoteWatchings } from "@/models/index.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
|
||||
export default async (me: User["id"], note: Note) => {
|
||||
await NoteWatchings.delete({
|
||||
noteId: note.id,
|
||||
userId: me,
|
||||
});
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import { NoteWatchings } from "@/models/index.js";
|
||||
import { genId } from "backend-rs";
|
||||
import type { NoteWatching } from "@/models/entities/note-watching.js";
|
||||
|
||||
export default async (me: User["id"], note: Note) => {
|
||||
// 自分の投稿はwatchできない
|
||||
if (me === note.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await NoteWatchings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: me,
|
||||
noteUserId: note.userId,
|
||||
} as NoteWatching);
|
||||
};
|
Loading…
Reference in New Issue