1#[macro_use]
2extern crate tracing;
3
4use std::{
5 env::Args,
6 fs::File,
7 io::{BufRead, BufReader, Error},
8 path::{Path, PathBuf},
9 process::{Command, Stdio},
10 sync::LazyLock,
11 time::Duration,
12};
13
14use anyhow::{bail, Context};
15use chrono::Local;
16use colored::{Color, Colorize};
17use crossbeam::channel::{Receiver, Sender};
18use lazy_regex::{lazy_regex, regex_replace_all, Lazy, Regex};
19use notify::{Config, Event, PollWatcher, RecursiveMode, Watcher};
20use parking_lot::RwLock;
21use terminal_link::Link;
22
23use crate::provider::{prelude::*, Providers, Type};
24
25#[cfg(feature = "discord")]
26pub mod discord;
27pub mod provider;
28pub mod vrchat;
29
30pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
31pub const CARGO_PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
32pub const USER_AGENT: &str = concat!(
33 "VRC-LOG/",
34 env!("CARGO_PKG_VERSION"),
35 " shaybox@shaybox.com"
36);
37
38#[must_use]
39pub fn get_local_time() -> String {
40 Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
41}
42
43pub fn check_for_updates() -> reqwest::Result<bool> {
46 let response = reqwest::blocking::get(CARGO_PKG_HOMEPAGE)?;
47 if let Some(mut segments) = response.url().path_segments() {
48 if let Some(remote_version) = segments.next_back() {
49 return Ok(remote_version > CARGO_PKG_VERSION);
50 }
51 }
52
53 Ok(false)
54}
55
56pub fn watch<P: AsRef<Path>>(tx: Sender<PathBuf>, path: P) -> notify::Result<PollWatcher> {
59 let tx_clone = tx.clone();
60 let mut watcher = PollWatcher::with_initial_scan(
61 move |watch_event: notify::Result<Event>| {
62 if let Ok(event) = watch_event {
63 for path in event.paths {
64 let _ = tx.send(path);
65 }
66 }
67 },
68 Config::default().with_poll_interval(Duration::from_secs(1)),
69 move |scan_event: notify::Result<PathBuf>| {
70 if let Ok(path) = scan_event {
71 let _ = tx_clone.send(path);
72 }
73 },
74 )?;
75
76 watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?;
77
78 Ok(watcher)
79}
80
81pub fn launch_game(args: Args) -> anyhow::Result<()> {
88 let args = args.collect::<Vec<_>>();
89 if args.len() > 1 {
90 let mut child = Command::new(&args[1])
91 .args(args.iter().skip(2))
92 .stderr(Stdio::null())
93 .stdout(Stdio::null())
94 .spawn()?;
95
96 std::thread::spawn(move || {
97 child.wait().unwrap();
98 std::process::exit(0);
99 });
100 }
101
102 Ok(())
103}
104
105pub fn process_avatars(rx: &Receiver<PathBuf>) -> anyhow::Result<()> {
108 #[cfg_attr(not(feature = "cache"), allow(unused_mut))]
109 let mut providers = Providers::from([
110 #[cfg(feature = "cache")]
111 (Type::CACHE, box_db!(Cache::new()?)),
112 #[cfg(feature = "avtrdb")]
113 (Type::AVTRDB, box_db!(AvtrDB::default())),
114 #[cfg(feature = "vrcwb")]
115 (Type::VRCWB, box_db!(VRCWB::default())),
116 #[cfg(feature = "vrcds")]
117 (Type::VRCDS, box_db!(VRCDS::default())),
118 #[cfg(feature = "vrcdb")]
119 (Type::VRCDB, box_db!(VRCDB::default())),
120 ]);
121
122 #[cfg(feature = "cache")]
123 let cache = providers.shift_remove(&Type::CACHE).context("None")?;
124
125 while let Ok(path) = rx.recv() {
126 let avatar_ids = parse_avatar_ids(&path);
127 for avatar_id in avatar_ids {
128 #[cfg(feature = "cache")] if !cache.check_avatar_id(&avatar_id).unwrap_or(true) {
130 continue;
131 }
132
133 #[cfg(feature = "cache")] let mut send_to_cache = true;
135
136 print_colorized(&avatar_id);
137
138 for (provider_type, provider) in &providers {
139 match provider.send_avatar_id(&avatar_id) {
140 Ok(unique) => {
141 if unique {
142 info!("^ Successfully Submitted to {provider_type}");
143 }
144 }
145 Err(error) => {
146 send_to_cache = false;
147 error!("^ Failed to submit to {provider_type}: {error}");
148 }
149 }
150 }
151
152 #[cfg(feature = "cache")]
153 if send_to_cache {
154 cache.send_avatar_id(&avatar_id)?;
155 }
156 }
157 }
158
159 bail!("Channel Closed")
160}
161
162pub fn parse_path_env(path: &str) -> Result<PathBuf, Error> {
168 let path = regex_replace_all!(r"(?:\$|%)(\w+)%?", path, |_, env| {
169 std::env::var(env).unwrap_or_else(|_| panic!("Environment Variable not found: {env}"))
170 });
171
172 std::fs::canonicalize(path.as_ref())
173}
174
175#[must_use]
176pub fn parse_avatar_ids(path: &PathBuf) -> Vec<String> {
177 static RE: Lazy<Regex> = lazy_regex!(r"avtr_\w{8}-\w{4}-\w{4}-\w{4}-\w{12}");
178
179 let Ok(file) = File::open(path) else {
180 return Vec::new(); };
182
183 let mut reader = BufReader::new(file);
184 let mut avatar_ids = Vec::new();
185 let mut buf = Vec::new();
186
187 while reader.read_until(b'\n', &mut buf).unwrap_or(0) > 0 {
188 let line = String::from_utf8_lossy(&buf);
189 for mat in RE.find_iter(&line) {
190 avatar_ids.push(mat.as_str().to_string());
191 }
192 buf.clear();
193 }
194
195 avatar_ids
196}
197
198pub fn print_colorized(avatar_id: &str) {
200 static INDEX: LazyLock<RwLock<usize>> = LazyLock::new(|| RwLock::new(0));
201 static COLORS: LazyLock<[Color; 12]> = LazyLock::new(|| {
202 [
203 Color::Red,
204 Color::BrightRed,
205 Color::Yellow,
206 Color::BrightYellow,
207 Color::Green,
208 Color::BrightGreen,
209 Color::Blue,
210 Color::BrightBlue,
211 Color::Cyan,
212 Color::BrightCyan,
213 Color::Magenta,
214 Color::BrightMagenta,
215 ]
216 });
217
218 let index = *INDEX.read();
219 let color = COLORS[index];
220 *INDEX.write() = (index + 1) % COLORS.len();
221
222 let text = format!("vrcx://avatar/{avatar_id}");
223 let link = Link::new(&text, &text).to_string().color(color);
224 info!("{link}");
225}