inital commit
3
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
5313
src-tauri/Cargo.lock
generated
Normal file
31
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.7.0", features = [ "os-all", "notification-all", "dialog-confirm", "clipboard-all", "dialog-message", "dialog-ask"] }
|
||||
dirs = "5.0.1"
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
lazy_static = "1.5.0"
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
tokio-macros = "2.3.0"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 1011 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 957 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
43
src-tauri/src/commands/config/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use crate::handlers::config::{get_config_path, Config};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn init_config() -> Result<(), String> {
|
||||
println!("[commands::config::init_config] Initializing config...");
|
||||
let config_path = get_config_path().map_err(|e| e.to_string())?;
|
||||
|
||||
println!(
|
||||
"[commands::config::init_config] Config path: {}",
|
||||
config_path.display()
|
||||
);
|
||||
|
||||
Config::load_or_create(&config_path).map_err(|e| e.to_string())?;
|
||||
println!("[commands::config::init_config] Config initialized successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_config() -> Result<Config, String> {
|
||||
let config_path = get_config_path().map_err(|e| e.to_string())?;
|
||||
let config = Config::load_or_create(&config_path).map_err(|e| e.to_string())?;
|
||||
|
||||
println!(
|
||||
"[commands::config::get_config] Config loaded successfully: {:?} from path: {}",
|
||||
config,
|
||||
config_path.display()
|
||||
);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_config(config: Config) -> Result<(), String> {
|
||||
let config_path = get_config_path().map_err(|e| e.to_string())?;
|
||||
println!(
|
||||
"[commands::config::save_config] Saving config: {:?} to path: {}",
|
||||
config,
|
||||
config_path.display()
|
||||
);
|
||||
|
||||
config.save(&config_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
37
src-tauri/src/commands/fansly/mod.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use crate::{
|
||||
handlers::fansly::Fansly,
|
||||
structs::{FanslyAccountResponse, FanslyBaseResponse, SyncDataResponse},
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
lazy_static! {
|
||||
static ref FANSLY: Mutex<Fansly> = Mutex::new(Fansly::new(None));
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fansly_set_token(token: Option<String>) {
|
||||
FANSLY.lock().await.set_token(token);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fansly_get_me() -> Result<FanslyBaseResponse<FanslyAccountResponse>, String> {
|
||||
let fansly = FANSLY.lock().await;
|
||||
let response = fansly.get_profile().await;
|
||||
|
||||
match response {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fansly_sync() -> Result<SyncDataResponse, String> {
|
||||
let fansly = FANSLY.lock().await;
|
||||
let response = fansly.sync().await;
|
||||
|
||||
match response {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
3
src-tauri/src/commands/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod fansly;
|
||||
pub mod utils;
|
4
src-tauri/src/commands/utils/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[tauri::command]
|
||||
pub fn quit(code: i32) {
|
||||
std::process::exit(code);
|
||||
}
|
105
src-tauri/src/handlers/config/mod.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::structs::{FanslyFollowersResponse, Subscription};
|
||||
|
||||
const CURRENT_VERSION: i32 = 1; // Set the current version of the config
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SyncData {
|
||||
pub followers: Vec<FanslyFollowersResponse>,
|
||||
pub subscribers: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub version: i32, // Add a version field to the config (1, 2, 3, etc.)
|
||||
pub is_first_run: bool,
|
||||
pub fansly_token: String,
|
||||
pub sync_interval: u64,
|
||||
pub last_sync: u64,
|
||||
pub last_sync_data: SyncData,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
version: CURRENT_VERSION, // Version is set to CURRENT_VERSION by default
|
||||
is_first_run: true, // First run is set to true by default
|
||||
fansly_token: String::new(), // Fansly token is stored as a string
|
||||
sync_interval: 1, // Every hour - sync interval is interpreted as hours
|
||||
last_sync: 0, // Last sync time is stored as a UNIX timestamp
|
||||
last_sync_data: SyncData {
|
||||
followers: Vec::new(),
|
||||
subscribers: Vec::new(),
|
||||
}, // Last sync data is stored as a list of followers and subscribers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load_or_create(path: &Path) -> io::Result<Self> {
|
||||
if path.exists() {
|
||||
let mut config: Self = serde_json::from_str(std::fs::read_to_string(path)?.as_str())
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Could not parse config file: {}", e),
|
||||
)
|
||||
})?;
|
||||
if config.version != CURRENT_VERSION {
|
||||
config = config.migrate()?;
|
||||
config.save(path)?;
|
||||
}
|
||||
Ok(config)
|
||||
} else {
|
||||
let saved_config = Config::default().save(path);
|
||||
saved_config
|
||||
.and_then(|_| Config::load_or_create(path))
|
||||
.or_else(|e| Err(e))
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate(mut self) -> io::Result<Self> {
|
||||
while self.version < CURRENT_VERSION {
|
||||
self = match self.version {
|
||||
1 => {
|
||||
// If we're on version 1, migrate to version 2 (not implemented)
|
||||
self.version += 1;
|
||||
self
|
||||
}
|
||||
_ => {
|
||||
// If we don't have a migration path, return an error
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("No migration path for version {}", self.version),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn save(&self, path: &Path) -> io::Result<()> {
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(serde_json::to_string_pretty(self).unwrap().as_bytes())?;
|
||||
|
||||
// Return the saved config
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_path() -> io::Result<PathBuf> {
|
||||
let mut config_dir = dirs::config_dir().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"Could not determine user's config directory",
|
||||
)
|
||||
})?;
|
||||
config_dir.push("FanslySync");
|
||||
fs::create_dir_all(&config_dir)?;
|
||||
config_dir.push("config.json");
|
||||
Ok(config_dir)
|
||||
}
|
254
src-tauri/src/handlers/fansly/mod.rs
Normal file
@ -0,0 +1,254 @@
|
||||
// Create a simple module for handling the Fansly API, using reqwest to make requests to the API.
|
||||
// This module will contain a struct Fansly, which will have a method to get the user's profile information.
|
||||
use crate::structs::{
|
||||
FanslyAccountResponse, FanslyBaseResponse, FanslyBaseResponseList, FanslyFollowersResponse,
|
||||
FanslySubscriptionsResponse, Subscription, SyncDataResponse,
|
||||
};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||
|
||||
pub struct Fansly {
|
||||
client: reqwest::Client,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
impl Fansly {
|
||||
pub fn new(token: Option<String>) -> Self {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// Set the user agent to the FanslySync/0.1.0 tanner@fanslycreatorbot.com
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_static("FanslySync/0.1.0 tanner@fanslycreatorbot.com"),
|
||||
);
|
||||
|
||||
// If we have a token, add it to the headers\
|
||||
if let Some(token) = &token {
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("{}", token)).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Set our default base url to https://apiv3.fansly.com/api/v1/
|
||||
let client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Self { client, token }
|
||||
}
|
||||
|
||||
// Helper function to set our token on the fly
|
||||
pub fn set_token(&mut self, token: Option<String>) {
|
||||
self.token = token;
|
||||
|
||||
// Re-create the client with the new token (if it exists)
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_static("FanslySync/0.1.0 tanner@fanslycreatorbot.com"),
|
||||
);
|
||||
|
||||
// If we have a token, add it to the headers
|
||||
if let Some(token) = &self.token {
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("{}", token)).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
self.client = reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn get_profile(
|
||||
&self,
|
||||
) -> Result<FanslyBaseResponse<FanslyAccountResponse>, reqwest::Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get("https://apiv3.fansly.com/api/v1/account/me")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
eprintln!("[sync::process::get_profile] No successful response from API. Setting error state.");
|
||||
return Err(response.error_for_status().unwrap_err());
|
||||
} else {
|
||||
println!("[sync::process::get_profile] Got successful response from API.");
|
||||
}
|
||||
|
||||
let profile: FanslyBaseResponse<FanslyAccountResponse> = response.json().await?;
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
async fn fetch_followers(
|
||||
&self,
|
||||
account_id: &str,
|
||||
auth_token: &str,
|
||||
offset: u32,
|
||||
) -> Result<FanslyBaseResponseList<FanslyFollowersResponse>, reqwest::Error> {
|
||||
let url = format!("https://apiv3.fansly.com/api/v1/account/{}/followers?ngsw-bypass=true&limit=100&offset={}", account_id, offset);
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("{}", auth_token).parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
"FanslySync/1.0.0 (tanner@teamhydra.dev)".parse().unwrap(),
|
||||
);
|
||||
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
"application/json".parse().unwrap(),
|
||||
);
|
||||
|
||||
let response = self.client.get(url).headers(headers).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
eprintln!("[sync::process::fetch_followers] No successful response from API. Setting error state.");
|
||||
return Err(response.error_for_status().unwrap_err());
|
||||
}
|
||||
|
||||
let followers: FanslyBaseResponseList<FanslyFollowersResponse> = response.json().await?;
|
||||
println!(
|
||||
"[sync::process::fetch_followers] Got {} followers from API.",
|
||||
followers.response.len()
|
||||
);
|
||||
|
||||
Ok(followers)
|
||||
}
|
||||
|
||||
async fn fetch_subscribers(
|
||||
&self,
|
||||
auth_token: &str,
|
||||
offset: u32,
|
||||
) -> Result<Vec<Subscription>, reqwest::Error> {
|
||||
let url = format!("https://apiv3.fansly.com/api/v1/subscribers?status=3,4&limit=100&offset={}&ngsw-bypass=true", offset);
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("{}", auth_token).parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
"FanslySync/1.0.0 (sticks@teamhydra.dev)".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
"application/json".parse().unwrap(),
|
||||
);
|
||||
|
||||
let response = self.client.get(url).headers(headers).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
eprintln!("[fanslySyncExt] No successful response from API. Setting error state.");
|
||||
let error = response.error_for_status().unwrap_err();
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let subscriptions: FanslyBaseResponse<FanslySubscriptionsResponse> =
|
||||
response.json().await?;
|
||||
println!(
|
||||
"[fanslySyncExt] Got {} subscriptions from API.",
|
||||
subscriptions.response.subscriptions.len()
|
||||
);
|
||||
|
||||
Ok(subscriptions.response.subscriptions)
|
||||
}
|
||||
|
||||
pub async fn sync(&self) -> Result<SyncDataResponse, String> {
|
||||
// Fetch profile
|
||||
println!("[sync::process] Fetching profile...");
|
||||
let profile = self.get_profile().await.map_err(|e| e.to_string())?;
|
||||
|
||||
if !profile.success {
|
||||
return Err("Failed to fetch profile".to_string());
|
||||
}
|
||||
|
||||
println!("[sync::process] Profile retrieved successfully.");
|
||||
|
||||
let account = profile.response.account;
|
||||
let total_followers = account.follow_count;
|
||||
let total_subscribers = account.subscriber_count;
|
||||
|
||||
println!(
|
||||
"[sync::process] Account {} has {} followers and {} subscribers. Starting sync...",
|
||||
account.id, total_followers, total_subscribers
|
||||
);
|
||||
|
||||
let mut followers: Vec<FanslyFollowersResponse> = Vec::new();
|
||||
let mut subscribers: Vec<Subscription> = Vec::new();
|
||||
|
||||
println!("[sync::process] Fetching followers and subscribers...");
|
||||
|
||||
// Fetch followers until we have all of them
|
||||
let mut offset = 0;
|
||||
let mut total_requests = 0;
|
||||
while followers.len() < total_followers as usize {
|
||||
println!(
|
||||
"[sync::process] Fetching followers for account {} with offset {} (total: {})",
|
||||
account.id, offset, total_followers
|
||||
);
|
||||
let response = self
|
||||
.fetch_followers(&account.id, &self.token.as_ref().unwrap(), offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
println!(
|
||||
"[sync::process] Got {} followers from API.",
|
||||
response.response.len()
|
||||
);
|
||||
followers.extend(response.response);
|
||||
offset += 100;
|
||||
total_requests += 1;
|
||||
|
||||
// Every 10 requests, sleep for a bit to avoid rate limiting
|
||||
if total_requests % 10 == 0 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch subscribers until we have all of them
|
||||
offset = 0;
|
||||
while subscribers.len() < total_subscribers as usize {
|
||||
println!(
|
||||
"[sync::process] Fetching subscribers with offset {} for account {} (total: {})",
|
||||
offset, account.id, total_subscribers
|
||||
);
|
||||
|
||||
let response = self
|
||||
.fetch_subscribers(&self.token.as_ref().unwrap(), offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
subscribers.extend(response);
|
||||
offset += 100;
|
||||
total_requests += 1;
|
||||
|
||||
// Every 10 requests, sleep for a bit to avoid rate limiting
|
||||
if total_requests % 10 == 0 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"[sync::process] Got {} followers and {} subscribers from API.",
|
||||
followers.len(),
|
||||
subscribers.len()
|
||||
);
|
||||
|
||||
println!("[sync::process] Sync complete.");
|
||||
|
||||
// Return JSON of what we fetched
|
||||
Ok(SyncDataResponse {
|
||||
followers,
|
||||
subscribers,
|
||||
})
|
||||
}
|
||||
}
|
2
src-tauri/src/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod config;
|
||||
pub mod fansly;
|
26
src-tauri/src/main.rs
Normal file
@ -0,0 +1,26 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod commands;
|
||||
mod handlers;
|
||||
mod structs;
|
||||
|
||||
use commands::config::{get_config, init_config, save_config};
|
||||
use commands::fansly::{fansly_get_me, fansly_set_token, fansly_sync};
|
||||
use commands::utils::quit;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
init_config,
|
||||
get_config,
|
||||
save_config,
|
||||
quit,
|
||||
fansly_set_token,
|
||||
fansly_get_me,
|
||||
fansly_sync
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
196
src-tauri/src/structs/mod.rs
Normal file
@ -0,0 +1,196 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SyncDataResponse {
|
||||
pub followers: Vec<FanslyFollowersResponse>,
|
||||
pub subscribers: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FanslyBaseResponse<T> {
|
||||
pub success: bool,
|
||||
pub response: T,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FanslyBaseResponseList<T> {
|
||||
pub success: bool,
|
||||
pub response: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FanslyFollowersResponse {
|
||||
pub follower_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FanslySubscriptionsResponse {
|
||||
pub stats: SubscriptionsStats,
|
||||
pub subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubscriptionsStats {
|
||||
pub total_active: i64,
|
||||
pub total_expired: i64,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Subscription {
|
||||
pub id: String,
|
||||
pub history_id: String,
|
||||
pub subscriber_id: String,
|
||||
pub subscription_tier_id: String,
|
||||
pub subscription_tier_name: String,
|
||||
pub subscription_tier_color: String,
|
||||
pub plan_id: String,
|
||||
pub promo_id: Option<String>,
|
||||
pub gift_code_id: Value,
|
||||
pub payment_method_id: String,
|
||||
pub status: i64,
|
||||
pub price: i64,
|
||||
pub renew_price: i64,
|
||||
pub renew_correlation_id: String,
|
||||
pub auto_renew: i64,
|
||||
pub billing_cycle: i64,
|
||||
pub duration: i64,
|
||||
pub renew_date: i64,
|
||||
pub version: i64,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub ends_at: i64,
|
||||
pub promo_price: Value,
|
||||
pub promo_duration: Value,
|
||||
pub promo_status: Value,
|
||||
pub promo_starts_at: Value,
|
||||
pub promo_ends_at: Value,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FanslyAccountResponse {
|
||||
pub account: Account,
|
||||
pub correlation_id: String,
|
||||
pub check_token: Value,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Account {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub flags: i64,
|
||||
pub version: i64,
|
||||
pub created_at: i64,
|
||||
pub follow_count: i64,
|
||||
pub subscriber_count: i64,
|
||||
pub permissions: Permissions,
|
||||
pub timeline_stats: TimelineStats,
|
||||
pub profile_access_flags: i64,
|
||||
pub profile_flags: i64,
|
||||
pub about: String,
|
||||
pub location: String,
|
||||
pub profile_socials: Vec<Value>,
|
||||
pub status_id: i64,
|
||||
pub last_seen_at: i64,
|
||||
pub post_likes: i64,
|
||||
pub main_wallet: MainWallet,
|
||||
pub streaming: Streaming,
|
||||
pub account_media_likes: i64,
|
||||
pub earnings_wallet: EarningsWallet,
|
||||
pub subscription_tiers: Vec<SubscriptionTier>,
|
||||
pub profile_access: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Permissions {
|
||||
pub account_permission_flags: AccountPermissionFlags,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountPermissionFlags {
|
||||
pub flags: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimelineStats {
|
||||
pub account_id: String,
|
||||
pub image_count: i64,
|
||||
pub video_count: i64,
|
||||
pub bundle_count: i64,
|
||||
pub bundle_image_count: i64,
|
||||
pub bundle_video_count: i64,
|
||||
pub fetched_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MainWallet {
|
||||
pub id: String,
|
||||
pub account_id: String,
|
||||
pub balance: i64,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: i64,
|
||||
pub wallet_version: i64,
|
||||
pub flags: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Streaming {
|
||||
pub account_id: String,
|
||||
pub channel: Value,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EarningsWallet {
|
||||
pub id: String,
|
||||
pub account_id: String,
|
||||
pub balance: i64,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: i64,
|
||||
pub wallet_version: i64,
|
||||
pub flags: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubscriptionTier {
|
||||
pub id: String,
|
||||
pub account_id: String,
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
pub pos: i64,
|
||||
pub price: i64,
|
||||
pub max_subscribers: i64,
|
||||
pub subscription_benefits: Vec<String>,
|
||||
pub included_tier_ids: Vec<Value>,
|
||||
pub plans: Vec<Plan>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Plan {
|
||||
pub id: String,
|
||||
pub status: i64,
|
||||
pub billing_cycle: i64,
|
||||
pub price: i64,
|
||||
pub use_amounts: i64,
|
||||
pub promos: Vec<Value>,
|
||||
pub uses: i64,
|
||||
}
|
89
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"build": {
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": {
|
||||
"productName": "fanslysync-desktop",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"clipboard": {
|
||||
"all": true,
|
||||
"readText": false,
|
||||
"writeText": false
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"ask": true,
|
||||
"confirm": true,
|
||||
"message": true,
|
||||
"open": false,
|
||||
"save": false
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "com.fanslycreatorbot.fanslysync",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://cdn.crabnebula.app/update/fansly-creator-bot/fansly-sync/{{target}}-{{arch}}/{{current_version}}"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJFODZGRDI4NjBFMDQ1RUMKUldUc1JlQmdLUDJHTGdRdSt6dWFISXE0MThsa0tvUDA2RWdMSStjQ0J6NVBhdmU4ajRMMms4a1cK"
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 650,
|
||||
"resizable": false,
|
||||
"title": "FanslySync",
|
||||
"width": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|