Skip to content

Commit

Permalink
0.8.0 - Additional Providers
Browse files Browse the repository at this point in the history
  • Loading branch information
ShayBox committed Oct 2, 2024
1 parent 400fa07 commit 533b8ab
Show file tree
Hide file tree
Showing 14 changed files with 716 additions and 336 deletions.
528 changes: 318 additions & 210 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vrc-log"
version = "0.7.1"
version = "0.8.0"
authors = ["Shayne Hartford <[email protected]>"]
edition = "2021"
description = "VRChat Local Cache Avatar ID Logger"
Expand All @@ -15,6 +15,7 @@ categories = ["config", "database", "filesystem", "games", "parsing"]

[dependencies]
anyhow = "1"
cached = { version = "0.53", optional = true }
chrono = "0.4"
colored = "2"
crossbeam = "0.8"
Expand All @@ -35,11 +36,18 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] }

[features]
default = ["cache", "sqlite", "title", "vrcdb"]
cache = []
discord = ["dep:discord-presence"]
sqlite = ["dep:sqlite"]
default = ["cache", "title", "doughnut", "neko", "vrcdb"]
# default = ["cache", "title", "avtrdb", "doughnut", "jeff", "neko", "vrcdb"]

discord = ["dep:discord-presence", "dep:cached"]
title = ["dep:crossterm"]

# VRChat Avatar Database Providers
avtrdb = ["dep:reqwest", "discord"]
cache = ["dep:sqlite"]
doughnut = ["dep:reqwest", "discord"]
jeff = ["dep:reqwest", "discord"]
neko = ["dep:reqwest", "discord"]
vrcdb = ["dep:reqwest", "discord"]

# https://github.com/johnthagen/min-sized-rust
Expand Down
43 changes: 28 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,40 @@

VRChat Local Cache Avatar ID Logger

## Notice
This project does **NOT** rip or steal avatars, it just scans your local cache for avatar ids and sends them to avatar database providers
## Important Notice
This project does **NOT** rip or steal avatars and it is **NOT** against the VRChat Terms of Service.
This project just scans your local cache for avatar ids and sends them to avatar search worlds and websites.
If you don't want your avatar(s) to be searchable, you can request them to be blacklisted by the below providers.

### Steam Support
This program supports Steam Launch Options (headless)
Place the file in the VRChat directory or PATH and set your launch options
`vrc-log(.exe) %command%`
I **DO NOT** work with search providers that don't allow blacklisting, such as YAAS (part of the SAARs ripper project)

### VRCX Support
### VRCX Auto-Launch
This program prints [VRCX] avatar links when a new (to you) avatar is found
You can place a **shortcut** to this program within the [VRCX] Auto-Launch Folder (Settings > Advanced)

### Provider Support
- [Avatar Search] / [Discord](https://discord.gg/q427ecnUvj) / [Web](https://vrcdb.com) / [World](https://vrchat.com/home/world/wrld_1146f625-5d42-40f5-bfe7-06a7664e2796) / [VRCX](https://vrcx.vrcdb.com/avatars/Avatar/VRCX) - Uses VRCDB
- ~~Ravenwood (Web & VRCX)~~ - Used VRCDB - Shutdown
- ~~[Just H Party (Web & VRCX)]~~ - There's no way to submit avatars
- ~~[Prismic's Avatar Search (World)]~~ - There's no way to submit avatars
### Steam Launch Options (Headless)
Place the file in the VRChat directory or `PATH` and set your launch options
`vrc-log(.exe) %command%`

### Supported Avatar Database Providers
<!-- - [AVTRDB] - [Discord](https://discord.gg/ZxB6w2hGfU) / [Web](https://avtrdb.com) / [VRCX](https://api.avtrdb.com/v1/avatar/search/vrcx) -->
- [DOUGHNUT] - [Discord](https://discord.gg/4HxcPk9r) / [Web](https://avtr1.nekosunevr.co.uk/search.php) / [VRCX](https://avtr1.nekosunevr.co.uk/vrcx_search.php)
<!-- - [JEFF] - [Discord](https://discord.gg/4HxcPk9r) / [Web](https://avtr.frensmp.cc/search.php) / [VRCX](https://avtr.frensmp.cc/vrcx_search.php) -->
- [NEKO] - [Discord](https://discord.gg/4HxcPk9r) / [Web](https://avtr.nekosunevr.co.uk/search.php) / [VRCX](https://avtr.nekosunevr.co.uk/vrcx_search.php)
- [VRCDB] - [Discord](https://discord.gg/q427ecnUvj) / [Web](https://vrcdb.com) / [World](https://vrchat.com/home/world/wrld_1146f625-5d42-40f5-bfe7-06a7664e2796) / [VRCX](vrcx.vrcdb.com/avatars/Avatar/VRCX)

#### Unsupported Avatar Database Providers
- ~~Ravenwood~~ - Used VRCDB (Shutdown)
- ~~[Just H Party]~~ - Can't submit avatars
- ~~[Prismic's Avatar Search]~~ - Can't submit avatars

Additional providers are welcome, please open an issue, pull request, or join Discord

[Avatar Search]: https://sites.smokes-hub.de
[Just H Party (Web & VRCX)]: https://avtr.just-h.party
[Prismic's Avatar Search (World)]: https://vrchat.com/home/world/wrld_57514404-7f4e-4aee-a50a-57f55d3084bf
[AVTRDB]: https://avtrdb.com
[DOUGHNUT]: https://avtr.nekosunevr.co.uk
[JEFF]: https://avtr.frensmp.cc
[Just H Party]: https://avtr.just-h.party
[NEKO]: https://avtr.nekosunevr.co.uk
[Prismic's Avatar Search]: https://vrchat.com/home/world/wrld_57514404-7f4e-4aee-a50a-57f55d3084bf
[VRCDB]: https://sites.smokes-hub.de
[VRCX]: https://github.com/vrcx-team/VRCX?tab=readme-ov-file#--vrcx
84 changes: 63 additions & 21 deletions src/discord.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,79 @@
use std::{
sync::{Arc, LazyLock},
time::Duration,
};
use std::{sync::Arc, time::Duration};

use cached::proc_macro::once;
use discord_presence::{
models::{EventData, PartialUser},
Client,
};
use parking_lot::RwLock;
use parking_lot::Mutex;

pub const CLIENT_ID: u64 = 1_137_885_877_918_502_923;
pub const DEVELOPER_ID: &str = "358558305997684739";

pub static USER: LazyLock<Option<PartialUser>> = LazyLock::new(|| {
let user_event = Arc::new(RwLock::new(None));
let user_clone = user_event.clone();
let mut client = Client::new(CLIENT_ID);
client
.on_ready(move |ctx| {
if let EventData::Ready(event) = ctx.event {
*user_event.write() = event.user;
};
})
.persist();
pub struct Discord {
pub client: Client,
pub user: Arc<Mutex<Option<PartialUser>>>,
}

impl Discord {
fn start() -> Self {
let mut discord = Self {
client: Client::new(CLIENT_ID),
user: Arc::default(),
};

let user = discord.user.clone();
discord
.client
.on_ready(move |ctx| {
if let EventData::Ready(event) = ctx.event {
*user.lock() = event.user;
};
})
.persist();

discord.client.start();
discord
}
}

#[once(sync_writes = true)]
pub fn get_dev_id() -> String {
warn!("Error: Discord RPC Connection Failed\n");
warn!("This may be due to one of the following reasons:");
warn!("1. Discord is not running on your system.");
warn!("2. VRC-LOG was restarted too quickly.\n");
warn!("The User ID will default to the developer: ShayBox");

std::env::var("DISCORD").unwrap_or_else(|_| DEVELOPER_ID.to_owned())
}

client.start();
#[once(option = true, sync_writes = true)]
pub fn get_user_id() -> Option<String> {
let discord = Discord::start();
let user = discord.user.lock().clone();

// block_until_event will never timeout
std::thread::sleep(Duration::from_secs(5));
discord.client.shutdown().ok()?;

client.shutdown().expect("Failed to stop RPC thread");
Some(match user {
None => get_dev_id(),
Some(user) => {
let userid = user.id.unwrap_or_else(get_dev_id);
if userid == "1045800378228281345" {
warn!("Vesktop & arRPC doesn't support fetching user info");
warn!("You can supply the 'DISCORD' env variable manually");
warn!("The User ID will default to the developer: ShayBox");

let user = user_clone.read();
std::env::var("DISCORD").unwrap_or_else(|_| DEVELOPER_ID.to_owned())
} else {
if let Some(username) = user.username {
info!("[Discord] Authenticated as {username}");
}

user.clone()
});
userid
}
}
})
}
29 changes: 16 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub mod vrchat;

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"),
" [email protected]"
);

pub type WatchResponse = (Sender<PathBuf>, Receiver<PathBuf>, PollWatcher);

Expand All @@ -37,7 +42,6 @@ pub fn get_local_time() -> String {
}

/// # Errors
///
/// Will return `Err` if couldn't get the GitHub repository
pub fn check_for_updates() -> reqwest::Result<bool> {
let response = reqwest::blocking::get(CARGO_PKG_HOMEPAGE)?;
Expand All @@ -53,7 +57,6 @@ pub fn check_for_updates() -> reqwest::Result<bool> {
}

/// # Errors
///
/// Will return `Err` if `PollWatcher::watch` errors
pub fn watch<P: AsRef<Path>>(path: P) -> notify::Result<WatchResponse> {
let (tx_a, rx_a) = crossbeam::channel::unbounded();
Expand Down Expand Up @@ -83,11 +86,8 @@ pub fn watch<P: AsRef<Path>>(path: P) -> notify::Result<WatchResponse> {
/// Steam Game Launch Options: `.../vrc-log(.exe) %command%`
///
/// # Errors
///
/// Will return `Err` if `Command::spawn` errors
///
/// # Panics
///
/// Will panic if `Child::wait` panics
pub fn launch_game(args: Args) -> anyhow::Result<()> {
let args = args.collect::<Vec<_>>();
Expand All @@ -108,21 +108,26 @@ pub fn launch_game(args: Args) -> anyhow::Result<()> {
}

/// # Errors
///
/// Will return `Err` if `Sqlite::new` or `Provider::send_avatar_id` errors
pub fn process_avatars((_tx, rx, _): WatchResponse) -> anyhow::Result<()> {
#[cfg_attr(not(feature = "cache"), allow(unused_mut))]
let mut providers = Providers::from([
#[cfg(all(feature = "cache", feature = "sqlite"))]
(Type::Cache, box_db!(Sqlite::new()?)),
#[cfg(feature = "cache")]
(Type::CACHE, box_db!(Cache::new()?)),
#[cfg(feature = "avtrdb")]
(Type::AVTRDB, box_db!(AvtrDB::default())),
#[cfg(feature = "doughnut")]
(Type::DOUGHNUT, box_db!(Doughnut::default())),
#[cfg(feature = "jeff")]
(Type::JEFF, box_db!(Jeff::default())),
#[cfg(feature = "neko")]
(Type::NEKO, box_db!(Neko::default())),
#[cfg(feature = "vrcdb")]
(Type::VRCDB, box_db!(VRCDB::default())),
#[cfg(all(feature = "sqlite", not(feature = "cache")))]
(Type::Sqlite, box_db!(Sqlite::new()?)),
]);

#[cfg(feature = "cache")]
let cache = providers.shift_remove(&Type::Cache).context("None")?;
let cache = providers.shift_remove(&Type::CACHE).context("None")?;

while let Ok(path) = rx.recv() {
let avatar_ids = self::parse_avatar_ids(&path);
Expand Down Expand Up @@ -163,11 +168,9 @@ pub fn process_avatars((_tx, rx, _): WatchResponse) -> anyhow::Result<()> {
}

/// # Errors
///
/// Will return `Err` if `std::fs::canonicalize` errors
///
/// # Panics
///
/// Will panic if an environment variable doesn't exist
pub fn parse_path_env(path: &str) -> Result<PathBuf, Error> {
let path = regex_replace_all!(r"(?:\$|%)(\w+)%?", path, |_, env| {
Expand Down
60 changes: 60 additions & 0 deletions src/provider/avtrdb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::{collections::HashMap, time::Duration};

use anyhow::{bail, Result};
use reqwest::blocking::Client;

use crate::{
provider::{Provider, Type},
USER_AGENT,
};

pub struct AvtrDB {
client: Client,
userid: String,
}

impl Default for AvtrDB {
fn default() -> Self {
Self {
client: Client::default(),
userid: crate::discord::get_user_id().unwrap(),
}
}
}

impl Provider for AvtrDB {
fn check_avatar_id(&self, _avatar_id: &str) -> Result<bool> {
bail!("Cache Only")
}

fn send_avatar_id(&self, avatar_id: &str) -> Result<bool> {
let response = self
.client
.put("...")
.header("User-Agent", USER_AGENT)
.json(&HashMap::from([
("id", avatar_id),
("userid", &self.userid),
]))
.send()?;

let status = response.status();
debug!("[{}] {status} | {}", Type::AvtrDB, response.text()?);

let unique = match status.as_u16() {
200 | 404 => false,
201 => true,
429 => {
warn!("[{}] 429 Rate Limit, Please Wait 1 Minute...", Type::AvtrDB);
std::thread::sleep(Duration::from_secs(60));
self.send_avatar_id(avatar_id)?
}
_ => {
error!("[{}] {status}", Type::AvtrDB);
false
}
};

Ok(unique)
}
}
11 changes: 5 additions & 6 deletions src/provider/sqlite.rs → src/provider/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ use crate::{
vrchat::VRCHAT_PATH,
};

pub struct Sqlite {
pub struct Cache {
connection: Connection,
}

impl Sqlite {
impl Cache {
/// # Errors
///
/// Will return `Err` if `sqlite::open` errors
pub fn new() -> Result<Self> {
let path = VRCHAT_PATH.join("avatars.sqlite");
Expand Down Expand Up @@ -59,22 +58,22 @@ impl Sqlite {
#[rustfmt::skip] // Prevent a large burst after updating
connection.execute("
UPDATE avatars
SET updated_at = datetime('now', '-31 days')
SET updated_at = datetime('now', '-31 days')
WHERE updated_at IS NULL
")?;

// Print cache statistics
if let Ok(statement) = connection.prepare("SELECT * FROM avatars") {
let rows = statement.into_iter().filter_map(Result::ok);
info!("[{}] {} Cached Avatars", Type::Cache, rows.count());
info!("[{}] {} Cached Avatars", Type::CACHE, rows.count());
}
}

Ok(Self { connection })
}
}

impl Provider for Sqlite {
impl Provider for Cache {
fn check_avatar_id(&self, avatar_id: &str) -> Result<bool> {
let query = "
SELECT 1 FROM avatars
Expand Down
Loading

0 comments on commit 533b8ab

Please sign in to comment.