Merge branch 'develop' into iceshrimp_mastodon

This commit is contained in:
naskya 2024-06-09 18:30:27 +09:00
commit 870d625565
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
8 changed files with 157 additions and 123 deletions

View file

@ -378,6 +378,13 @@ export function isSilencedServer(host: string): Promise<boolean>
* ``` * ```
*/ */
export function isAllowedServer(host: string): Promise<boolean> export function isAllowedServer(host: string): Promise<boolean>
export interface PartialNoteToCheckWordMute {
fileIds: Array<string>
text: string | null
cw: string | null
renoteId: string | null
replyId: string | null
}
/** /**
* Returns whether `note` should be hard-muted. * Returns whether `note` should be hard-muted.
* *
@ -390,11 +397,11 @@ export function isAllowedServer(host: string): Promise<boolean>
* *
* # Arguments * # Arguments
* *
* * `note` : [PartialNoteToElaborate] object * * `note` : [PartialNoteToCheckWordMute] object
* * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition) * * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition)
* * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions * * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions
*/ */
export function checkWordMute(note: PartialNoteToElaborate, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean> export function checkWordMute(note: PartialNoteToCheckWordMute, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean>
export function getFullApAccount(username: string, host?: string | undefined | null): string export function getFullApAccount(username: string, host?: string | undefined | null): string
export function isSelfHost(host?: string | undefined | null): boolean export function isSelfHost(host?: string | undefined | null): boolean
export function isSameOrigin(uri: string): boolean export function isSameOrigin(uri: string): boolean
@ -413,14 +420,6 @@ export interface ImageSize {
height: number height: number
} }
export function getImageSizeFromUrl(url: string): Promise<ImageSize> export function getImageSizeFromUrl(url: string): Promise<ImageSize>
export interface PartialNoteToElaborate {
fileIds: Array<string>
userId: string
text: string | null
cw: string | null
renoteId: string | null
replyId: string | null
}
export interface PartialNoteToSummarize { export interface PartialNoteToSummarize {
fileIds: Array<string> fileIds: Array<string>
text: string | null text: string | null

View file

@ -1,8 +1,17 @@
use crate::misc::get_note_all_texts::{all_texts, PartialNoteToElaborate}; use crate::misc::get_note_all_texts::all_texts;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use sea_orm::DbErr; use sea_orm::DbErr;
#[crate::export(object)]
pub struct PartialNoteToCheckWordMute {
pub file_ids: Vec<String>,
pub text: Option<String>,
pub cw: Option<String>,
pub renote_id: Option<String>,
pub reply_id: Option<String>,
}
fn convert_regex(js_regex: &str) -> String { fn convert_regex(js_regex: &str) -> String {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap()); static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap());
RE.replace(js_regex, "(?$2)$1").to_string() RE.replace(js_regex, "(?$2)$1").to_string()
@ -37,12 +46,12 @@ fn check_word_mute_impl(
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `note` : [PartialNoteToElaborate] object /// * `note` : [PartialNoteToCheckWordMute] object
/// * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition) /// * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition)
/// * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions /// * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions
#[crate::export] #[crate::export]
pub async fn check_word_mute( pub async fn check_word_mute(
note: PartialNoteToElaborate, note: PartialNoteToCheckWordMute,
muted_words: &[String], muted_words: &[String],
muted_patterns: &[String], muted_patterns: &[String],
) -> Result<bool, DbErr> { ) -> Result<bool, DbErr> {
@ -50,7 +59,7 @@ pub async fn check_word_mute(
Ok(false) Ok(false)
} else { } else {
Ok(check_word_mute_impl( Ok(check_word_mute_impl(
&all_texts(note, true).await?, &all_texts!(note, true).await?,
muted_words, muted_words,
muted_patterns, muted_patterns,
)) ))

View file

@ -4,40 +4,33 @@ use crate::{
}; };
use sea_orm::{prelude::*, QuerySelect}; use sea_orm::{prelude::*, QuerySelect};
#[crate::export(object)]
pub struct PartialNoteToElaborate {
pub file_ids: Vec<String>,
pub user_id: String,
pub text: Option<String>,
pub cw: Option<String>,
pub renote_id: Option<String>,
pub reply_id: Option<String>,
}
/// Returns [`Vec<String>`] containing the post text, content warning, /// Returns [`Vec<String>`] containing the post text, content warning,
/// those of the "parent" (replied/quoted) posts, and alt texts of attached files. /// those of the "parent" (replied/quoted) posts, and alt texts of attached files.
/// Consider using [`all_texts`] macro instead
/// when dealing with a note ([`note::Model`])-like instance.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `note` : [PartialNoteToElaborate] object /// * `file_ids` : IDs of attached files ([`drive_file::Model`])
/// * `text`, `cw`, `renote_id`, `reply_id` : note ([`note::Model`]) fields
/// * `include_parent` : whether to take the reply-to post and quoted post into account /// * `include_parent` : whether to take the reply-to post and quoted post into account
pub async fn all_texts( pub async fn all_texts_impl(
note: PartialNoteToElaborate, file_ids: &[String],
text: Option<String>,
cw: Option<String>,
renote_id: &Option<String>,
reply_id: &Option<String>,
include_parent: bool, include_parent: bool,
) -> Result<Vec<String>, DbErr> { ) -> Result<Vec<String>, DbErr> {
let db = db_conn().await?; let db = db_conn().await?;
let mut texts: Vec<String> = vec![]; let mut texts: Vec<String> = vec![];
let is_renote: bool; let is_renote = text.is_none();
if let Some(text) = note.text { if let Some(text) = text {
is_renote = false;
texts.push(text); texts.push(text);
} else {
is_renote = true;
} }
if let Some(cw) = cw {
if let Some(cw) = note.cw {
texts.push(cw); texts.push(cw);
} }
@ -45,7 +38,7 @@ pub async fn all_texts(
drive_file::Entity::find() drive_file::Entity::find()
.select_only() .select_only()
.column(drive_file::Column::Comment) .column(drive_file::Column::Comment)
.filter(drive_file::Column::Id.is_in(note.file_ids)) .filter(drive_file::Column::Id.is_in(file_ids))
.into_tuple::<Option<String>>() .into_tuple::<Option<String>>()
.all(db) .all(db)
.await? .await?
@ -53,45 +46,87 @@ pub async fn all_texts(
.flatten(), .flatten(),
); );
if note.renote_id.is_some() && (include_parent || is_renote) { let mut query_note_ids = Vec::<&str>::with_capacity(2);
let renote_id = note.renote_id.unwrap(); if let Some(renote_id) = renote_id {
if include_parent || is_renote {
if let Some((text, cw)) = note::Entity::find_by_id(&renote_id) query_note_ids.push(renote_id);
.select_only()
.columns([note::Column::Text, note::Column::Cw])
.into_tuple::<(Option<String>, Option<String>)>()
.one(db)
.await?
{
if let Some(t) = text {
texts.push(t);
}
if let Some(c) = cw {
texts.push(c);
}
} else {
tracing::warn!("nonexistent renote id: {}", renote_id);
} }
} }
if let Some(reply_id) = reply_id {
if include_parent && note.reply_id.is_some() { if include_parent {
if let Some((text, cw)) = note::Entity::find_by_id(note.reply_id.as_ref().unwrap()) query_note_ids.push(reply_id);
.select_only()
.columns([note::Column::Text, note::Column::Cw])
.into_tuple::<(Option<String>, Option<String>)>()
.one(db)
.await?
{
if let Some(t) = text {
texts.push(t);
}
if let Some(c) = cw {
texts.push(c);
}
} else {
tracing::warn!("nonexistent reply id: {}", note.reply_id.unwrap());
} }
} }
if !query_note_ids.is_empty() {
texts.extend(
note::Entity::find()
.filter(note::Column::Id.is_in(query_note_ids))
.select_only()
.columns([note::Column::Text, note::Column::Cw])
.into_tuple::<(Option<String>, Option<String>)>()
.one(db)
.await?
.into_iter()
.flat_map(|(text, cw)| [text, cw])
.flatten(),
);
}
Ok(texts) Ok(texts)
} }
/// Returns [`Vec<String>`] containing the post text, content warning,
/// those of the "parent" (replied/quoted) posts, and alt texts of attached files.
///
/// # Arguments
///
/// * `note_like` : a note ([`note::Model`])-like instance containing
/// `file_ids`, `text`, `cw`, `renote_id`, `reply_id` fields
/// * `include_parent` ([bool]) : whether to take the reply-to post and quoted post into account
///
/// # Caveats
///
/// The `note_like` argument should not contain function calls
/// (e.g., `all_texts!(note.clone(), false)`)
/// since the function will be called multiple times after macro expansion.
///
/// # Examples
///
/// ```
/// # use backend_rs::misc::get_note_all_texts::all_texts;
/// // note-like struct
/// struct SomeNoteLikeStruct {
/// // required fields
/// file_ids: Vec<String>,
/// text: Option<String>,
/// cw: Option<String>,
/// renote_id: Option<String>,
/// reply_id: Option<String>,
/// // arbitrary extra fields
/// extra_field_1: u32,
/// extra_field_2: Vec<String>,
/// }
///
/// async fn all_texts_from_some_note_like_struct(
/// note_like: &SomeNoteLikeStruct,
/// include_parent: bool,
/// ) -> Result<Vec<String>, sea_orm::DbErr> {
/// all_texts!(note_like, include_parent).await
/// }
/// ```
#[doc(hidden)] // hide the macro in the top doc page
#[macro_export]
macro_rules! all_texts {
($note_like:expr, $include_parent:expr) => {
$crate::misc::get_note_all_texts::all_texts_impl(
&$note_like.file_ids,
$note_like.text.clone(),
$note_like.cw.clone(),
&$note_like.renote_id,
&$note_like.reply_id,
$include_parent,
)
};
}
#[doc(inline)] // show the macro in the module doc page
pub use all_texts;

View file

@ -2,24 +2,23 @@
use crate::{database::db_conn, model::entity::antenna}; use crate::{database::db_conn, model::entity::antenna};
use sea_orm::prelude::*; use sea_orm::prelude::*;
use std::sync::Mutex; use std::sync::{Arc, Mutex};
static CACHE: Mutex<Option<Vec<antenna::Model>>> = Mutex::new(None); static CACHE: Mutex<Option<Arc<[antenna::Model]>>> = Mutex::new(None);
fn set(antennas: &[antenna::Model]) { fn set(antennas: Arc<[antenna::Model]>) {
let _ = CACHE let _ = CACHE.lock().map(|mut cache| *cache = Some(antennas));
.lock()
.map(|mut cache| *cache = Some(antennas.to_owned()));
} }
pub(super) async fn update() -> Result<Vec<antenna::Model>, DbErr> { pub(super) async fn update() -> Result<Arc<[antenna::Model]>, DbErr> {
tracing::debug!("updating cache"); tracing::debug!("updating cache");
let antennas = antenna::Entity::find().all(db_conn().await?).await?; let antennas: Arc<[antenna::Model]> =
set(&antennas); antenna::Entity::find().all(db_conn().await?).await?.into();
set(antennas.clone());
Ok(antennas) Ok(antennas)
} }
pub(super) async fn get() -> Result<Vec<antenna::Model>, DbErr> { pub(super) async fn get() -> Result<Arc<[antenna::Model]>, DbErr> {
if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) {
return Ok(cache); return Ok(cache);
} }

View file

@ -12,8 +12,6 @@ pub enum AntennaCheckError {
Db(#[from] DbErr), Db(#[from] DbErr),
#[error("Cache error: {0}")] #[error("Cache error: {0}")]
Cache(#[from] cache::Error), Cache(#[from] cache::Error),
#[error("User profile not found: {0}")]
UserProfileNotFound(String),
} }
fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool { fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool {
@ -29,7 +27,7 @@ fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> b
} }
} }
pub async fn check_hit_antenna( pub(super) async fn check_hit_antenna(
antenna: &antenna::Model, antenna: &antenna::Model,
note: &note::Model, note: &note::Model,
note_all_texts: &[String], note_all_texts: &[String],

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
database::{cache, redis_conn, redis_key, RedisConnError}, database::{cache, redis_conn, redis_key, RedisConnError},
federation::acct::Acct, federation::acct::Acct,
misc::get_note_all_texts::{all_texts, PartialNoteToElaborate}, misc::get_note_all_texts::all_texts,
model::entity::note, model::entity::note,
service::{ service::{
antenna, antenna,
@ -37,31 +37,19 @@ type Note = note::Model;
#[crate::export] #[crate::export]
pub async fn update_antennas_on_new_note( pub async fn update_antennas_on_new_note(
note: Note, note: &Note,
note_author: &Acct, note_author: &Acct,
note_muted_users: &[String], note_muted_users: &[String],
) -> Result<(), Error> { ) -> Result<(), Error> {
let note_cloned = note.clone(); let note_all_texts = all_texts!(note, false).await?;
let note_all_texts = all_texts(
PartialNoteToElaborate {
file_ids: note.file_ids,
user_id: note.user_id,
text: note.text,
cw: note.cw,
renote_id: note.renote_id,
reply_id: note.reply_id,
},
false,
)
.await?;
// TODO: do this in parallel // TODO: do this in parallel
for antenna in antenna::cache::get().await?.iter() { for antenna in antenna::cache::get().await?.iter() {
if note_muted_users.contains(&antenna.user_id) { if note_muted_users.contains(&antenna.user_id) {
continue; continue;
} }
if check_hit_antenna(antenna, &note_cloned, &note_all_texts, note_author).await? { if check_hit_antenna(antenna, note, &note_all_texts, note_author).await? {
add_note_to_antenna(&antenna.id, &note_cloned).await?; add_note_to_antenna(&antenna.id, note).await?;
} }
} }

View file

@ -90,6 +90,6 @@
"vue-draggable-plus": "0.5.0", "vue-draggable-plus": "0.5.0",
"vue-plyr": "7.0.0", "vue-plyr": "7.0.0",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vue-tsc": "2.0.19" "vue-tsc": "2.0.21"
} }
} }

View file

@ -762,8 +762,8 @@ importers:
specifier: 2.0.0-alpha.2 specifier: 2.0.0-alpha.2
version: 2.0.0-alpha.2(vue@3.4.27(typescript@5.4.5)) version: 2.0.0-alpha.2(vue@3.4.27(typescript@5.4.5))
vue-tsc: vue-tsc:
specifier: 2.0.19 specifier: 2.0.21
version: 2.0.19(typescript@5.4.5) version: 2.0.21(typescript@5.4.5)
packages/firefish-js: packages/firefish-js:
dependencies: dependencies:
@ -2586,14 +2586,14 @@ packages:
vite: ^5.0.0 vite: ^5.0.0
vue: ^3.2.25 vue: ^3.2.25
'@volar/language-core@2.2.5': '@volar/language-core@2.3.0-alpha.15':
resolution: {integrity: sha512-2htyAuxRrAgETmFeUhT4XLELk3LiEcqoW/B8YUXMF6BrGWLMwIR09MFaZYvrA2UhbdAeSyeQ726HaWSWkexUcQ==} resolution: {integrity: sha512-uSfn1Dsl1w9o2aN9nnS6N/4FcjSbmpY6P/ypfW4kRhasEyICstu4swIz2joNR6532R02JwJY9Ta0pxRmXbBOqw==}
'@volar/source-map@2.2.5': '@volar/source-map@2.3.0-alpha.15':
resolution: {integrity: sha512-wrOEIiZNf4E+PWB0AxyM4tfhkfldPsb3bxg8N6FHrxJH2ohar7aGu48e98bp3pR9HUA7P/pR9VrLmkTrgCCnWQ==} resolution: {integrity: sha512-DQr3FwhRxtxX4W6BoJkwajWjj6BAF5H/SgtzFaUP9z8txn6Y5oFxZPPDG+3Xwu3pTV3gvVlE7AL5E/G1jUr5Yg==}
'@volar/typescript@2.2.5': '@volar/typescript@2.3.0-alpha.15':
resolution: {integrity: sha512-eSV/n75+ppfEVugMC/salZsI44nXDPAyL6+iTYCNLtiLHGJsnMv9GwiDMujrvAUj/aLQyqRJgYtXRoxop2clCw==} resolution: {integrity: sha512-sTzUyqGC1wkzVyY0XZBO5smCPDRvIqxlMTUw7bZebuD/7sGaVfyk9ryE29aG6CLpuYDev/ugpQsKoFVhFaQQ8A==}
'@vue/compiler-core@3.4.27': '@vue/compiler-core@3.4.27':
resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==}
@ -2610,8 +2610,8 @@ packages:
'@vue/compiler-ssr@3.4.27': '@vue/compiler-ssr@3.4.27':
resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==}
'@vue/language-core@2.0.19': '@vue/language-core@2.0.21':
resolution: {integrity: sha512-A9EGOnvb51jOvnCYoRLnMP+CcoPlbZVxI9gZXE/y2GksRWM6j/PrLEIC++pnosWTN08tFpJgxhSS//E9v/Sg+Q==} resolution: {integrity: sha512-vjs6KwnCK++kIXT+eI63BGpJHfHNVJcUCr3RnvJsccT3vbJnZV5IhHR2puEkoOkIbDdp0Gqi1wEnv3hEd3WsxQ==}
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
peerDependenciesMeta: peerDependenciesMeta:
@ -7482,6 +7482,9 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
vue-draggable-plus@0.5.0: vue-draggable-plus@0.5.0:
resolution: {integrity: sha512-A5TT5+M5JceROSjPO9aDZTsrSN1TetEs419czPlboomarSiGIBIxTp2WD7XH53EHMrbO7Qo+leRiHWV/rMlyjA==} resolution: {integrity: sha512-A5TT5+M5JceROSjPO9aDZTsrSN1TetEs419czPlboomarSiGIBIxTp2WD7XH53EHMrbO7Qo+leRiHWV/rMlyjA==}
peerDependencies: peerDependencies:
@ -7509,8 +7512,8 @@ packages:
vue-template-compiler@2.7.16: vue-template-compiler@2.7.16:
resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==}
vue-tsc@2.0.19: vue-tsc@2.0.21:
resolution: {integrity: sha512-JWay5Zt2/871iodGF72cELIbcAoPyhJxq56mPPh+M2K7IwI688FMrFKc/+DvB05wDWEuCPexQJ6L10zSwzzapg==} resolution: {integrity: sha512-E6x1p1HaHES6Doy8pqtm7kQern79zRtIewkf9fiv7Y43Zo4AFDS5hKi+iHi2RwEhqRmuiwliB1LCEFEGwvxQnw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '*' typescript: '*'
@ -9677,18 +9680,19 @@ snapshots:
vite: 5.2.13(@types/node@20.14.2)(sass@1.77.4)(stylus@0.57.0)(terser@5.31.0) vite: 5.2.13(@types/node@20.14.2)(sass@1.77.4)(stylus@0.57.0)(terser@5.31.0)
vue: 3.4.27(typescript@5.4.5) vue: 3.4.27(typescript@5.4.5)
'@volar/language-core@2.2.5': '@volar/language-core@2.3.0-alpha.15':
dependencies: dependencies:
'@volar/source-map': 2.2.5 '@volar/source-map': 2.3.0-alpha.15
'@volar/source-map@2.2.5': '@volar/source-map@2.3.0-alpha.15':
dependencies: dependencies:
muggle-string: 0.4.1 muggle-string: 0.4.1
'@volar/typescript@2.2.5': '@volar/typescript@2.3.0-alpha.15':
dependencies: dependencies:
'@volar/language-core': 2.2.5 '@volar/language-core': 2.3.0-alpha.15
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.0.8
'@vue/compiler-core@3.4.27': '@vue/compiler-core@3.4.27':
dependencies: dependencies:
@ -9728,9 +9732,9 @@ snapshots:
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/language-core@2.0.19(typescript@5.4.5)': '@vue/language-core@2.0.21(typescript@5.4.5)':
dependencies: dependencies:
'@volar/language-core': 2.2.5 '@volar/language-core': 2.3.0-alpha.15
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
computeds: 0.0.1 computeds: 0.0.1
@ -15167,6 +15171,8 @@ snapshots:
void-elements@3.1.0: {} void-elements@3.1.0: {}
vscode-uri@3.0.8: {}
vue-draggable-plus@0.5.0(@types/sortablejs@1.15.8): vue-draggable-plus@0.5.0(@types/sortablejs@1.15.8):
dependencies: dependencies:
'@types/sortablejs': 1.15.8 '@types/sortablejs': 1.15.8
@ -15198,10 +15204,10 @@ snapshots:
de-indent: 1.0.2 de-indent: 1.0.2
he: 1.2.0 he: 1.2.0
vue-tsc@2.0.19(typescript@5.4.5): vue-tsc@2.0.21(typescript@5.4.5):
dependencies: dependencies:
'@volar/typescript': 2.2.5 '@volar/typescript': 2.3.0-alpha.15
'@vue/language-core': 2.0.19(typescript@5.4.5) '@vue/language-core': 2.0.21(typescript@5.4.5)
semver: 7.6.2 semver: 7.6.2
typescript: 5.4.5 typescript: 5.4.5