#[macro_use]
extern crate tracing;
use std::{
collections::HashSet,
env::Args,
ffi::OsStr,
fs::{File, create_dir_all},
io::{BufRead, BufReader, Error},
path::{Path, PathBuf},
process::{Command, Stdio},
sync::{Arc, LazyLock},
time::Duration,
};
use anyhow::{Result, bail};
use chrono::Local;
use colored::{Color, Colorize};
use flume::{Receiver, Sender};
use lazy_regex::{Lazy, Regex, lazy_regex, regex_replace_all};
use notify::{Config, Event, PollWatcher, RecursiveMode, Watcher};
use parking_lot::RwLock;
use terminal_link::Link;
#[cfg(feature = "cache")]
use crate::process::process_with_cache;
#[cfg(not(feature = "cache"))]
use crate::process::process_without_cache;
use crate::provider::Provider;
#[cfg(feature = "cache")]
pub mod cache;
#[cfg(feature = "discord")]
pub mod discord;
mod process;
pub mod provider;
pub mod settings;
pub mod vrchat;
#[cfg(windows)]
pub mod windows;
pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const CARGO_PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
pub const USER_AGENT: &str = concat!(
"VRC-LOG/",
env!("CARGO_PKG_VERSION"),
" shaybox@shaybox.com"
);
#[must_use]
pub fn get_local_time() -> String {
Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
}
pub async fn check_for_updates() -> reqwest::Result<bool> {
let response = reqwest::get(CARGO_PKG_HOMEPAGE).await?;
if let Some(mut segments) = response.url().path_segments()
&& let Some(remote_version) = segments.next_back()
{
return Ok(remote_version > CARGO_PKG_VERSION);
}
Ok(false)
}
pub fn watch<P: AsRef<Path>>(
tx: Sender<PathBuf>,
path: P,
millis: u64,
) -> notify::Result<PollWatcher> {
let path = path.as_ref();
debug!("Watching {path:?}");
let tx_clone = tx.clone();
let mut watcher = PollWatcher::with_initial_scan(
move |watch_event: notify::Result<Event>| {
if let Ok(event) = watch_event {
for path in event.paths {
if let Some(extension) = path.extension().and_then(OsStr::to_str)
&& ["csv", "log", "txt"].contains(&extension)
{
let _ = tx.send(path.clone());
}
if let Some(filename) = path.file_name().and_then(OsStr::to_str)
&& filename == "amplitude.cache"
{
let _ = tx.send(path);
}
}
}
},
Config::default()
.with_compare_contents(true)
.with_poll_interval(Duration::from_millis(millis)),
move |scan_event: notify::Result<PathBuf>| {
if let Ok(path) = scan_event {
if let Some(extension) = path.extension().and_then(OsStr::to_str)
&& ["csv", "log", "txt"].contains(&extension)
{
let _ = tx_clone.send(path.clone());
}
if let Some(filename) = path.file_name().and_then(OsStr::to_str)
&& filename == "amplitude.cache"
{
let _ = tx_clone.send(path);
}
}
},
)?;
watcher.watch(path, RecursiveMode::NonRecursive)?;
Ok(watcher)
}
pub fn launch_game(args: Args) -> Result<()> {
let args = args.collect::<Vec<_>>();
if args.len() > 1 {
let mut child = Command::new(&args[1])
.args(args.iter().skip(2))
.stderr(Stdio::null())
.stdout(Stdio::null())
.spawn()?;
std::thread::spawn(move || {
child.wait().unwrap();
std::process::exit(0);
});
}
Ok(())
}
pub async fn process_avatars(
providers: Vec<Arc<Box<dyn Provider>>>,
clear_amplitude: bool,
(_tx, rx): (Sender<PathBuf>, Receiver<PathBuf>),
) -> Result<()> {
#[cfg(feature = "cache")]
let cache = cache::Cache::new().await?;
while let Ok(path) = rx.recv_async().await {
let avatar_ids = parse_avatar_ids(&path);
if clear_amplitude && path.file_name().and_then(|n| n.to_str()) == Some("amplitude.cache") {
match std::fs::write(&path, "") {
Ok(()) => debug!("Cleared amplitude file: {path:?}"),
Err(error) => warn!("Failed to clear amplitude file: {error}"),
}
}
#[cfg(feature = "cache")]
process_with_cache(providers.clone(), &cache, avatar_ids).await?;
#[cfg(not(feature = "cache"))]
process_without_cache(providers.clone(), avatar_ids).await?;
}
bail!("Channel Closed")
}
pub fn parse_path_env(path: &str) -> Result<PathBuf, Error> {
let path = regex_replace_all!(r"(?:\$|%)(\w+)%?", path, |_, env| {
std::env::var(env).unwrap_or_else(|_| panic!("Environment Variable not found: {env}"))
});
let path = Path::new(path.as_ref());
if !path.exists() {
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
File::create(path)?;
}
std::fs::canonicalize(path)
}
#[must_use]
pub fn parse_avatar_ids(path: &PathBuf) -> impl IntoIterator<Item = String> {
#[allow(clippy::non_std_lazy_statics)]
static RE: Lazy<Regex> = lazy_regex!(r"avtr_\w{8}-\w{4}-\w{4}-\w{4}-\w{12}");
let Ok(file) = File::open(path) else {
return HashSet::new(); };
let mut reader = BufReader::new(file);
let mut avatar_ids = HashSet::new(); let mut buf = Vec::new();
while reader.read_until(b'\n', &mut buf).unwrap_or(0) > 0 {
let line = String::from_utf8_lossy(&buf);
for mat in RE.find_iter(&line) {
avatar_ids.insert(mat.as_str().to_string());
}
buf.clear();
}
avatar_ids
}
pub fn print_colorized(avatar_id: &str) {
static INDEX: LazyLock<RwLock<usize>> = LazyLock::new(|| RwLock::new(0));
static COLORS: LazyLock<[Color; 12]> = LazyLock::new(|| {
[
Color::Red,
Color::BrightRed,
Color::Yellow,
Color::BrightYellow,
Color::Green,
Color::BrightGreen,
Color::Blue,
Color::BrightBlue,
Color::Cyan,
Color::BrightCyan,
Color::Magenta,
Color::BrightMagenta,
]
});
let index = *INDEX.read();
let color = COLORS[index];
*INDEX.write() = (index + 1) % COLORS.len();
let text = format!("vrcx://avatar/{avatar_id}");
let link = Link::new(&text, &text).to_string().color(color);
info!("{link}");
}