Compare commits

..

31 commits
v0.2.0 ... rust

Author SHA1 Message Date
c25f2032d7 migrated to new version of sync script 2025-04-02 23:18:53 +02:00
9ef64b5b82 added more request logging code 2024-06-30 00:34:55 +02:00
c20e276de2 Merge branch 'rust' of github.com:J-onasJones/jonas_jones-api into rust 2024-06-29 23:29:10 +02:00
2c8a9db949 added request logger 2024-06-29 23:27:05 +02:00
350c9f722a added IP in brackets 2024-06-28 05:12:47 +02:00
cdcf27b0fa added request logging 2024-06-28 05:00:38 +02:00
41fa898a67 Updated dependencies 2024-05-15 13:10:18 +02:00
d2a89d5067 roadmap draft 2024-02-14 03:41:25 +01:00
3b5378c6f2 Fixed version (0verdue!!!) 2024-02-12 04:41:23 +01:00
1cdb34503d Added docker-compose file 2024-02-12 04:34:34 +01:00
4ecbd7aa98 fixed docker compose 2024-02-12 04:32:37 +01:00
a63f933dd9 Fixed dependencies 2024-02-12 04:21:05 +01:00
48da0d7f95 Fixed docker compose
and python lib requirements
2024-02-12 04:05:44 +01:00
5e9ed3738c fixed logs 2024-02-12 03:49:41 +01:00
7bee1f5bae added update runner thread 2024-02-12 03:46:08 +01:00
bdfd5a74a6 Added docker compose instructions 2024-02-11 08:44:20 +01:00
4c62ba2845 added /status route 2024-02-11 06:28:44 +01:00
Jonas_Jones
852e184b89 removed console spam 2024-01-28 03:38:11 +01:00
Jonas_Jones
a851b18875 changed update to run path 2024-01-28 03:31:10 +01:00
Jonas_Jones
c18ab8a8da reverted changes 2024-01-28 03:27:38 +01:00
Jonas_Jones
462ee68763 Fixed warnings 2024-01-28 03:10:49 +01:00
Jonas_Jones
f201bc7fda Added new branch /v1/update 2024-01-28 03:10:41 +01:00
5f2026af0d version bump 2024-01-04 00:49:32 +01:00
5c6a2ac53f Added mcmod update branch 2024-01-04 00:48:38 +01:00
e8d1b934d7 code cleanup 2024-01-03 12:01:22 +01:00
8e61e81f06 bumped version 2024-01-03 12:00:54 +01:00
5eb6080694 Fixed getlangs & language filter
It was a placeholder in v0.2.0 as not implemented
2024-01-03 12:00:32 +01:00
83e1b85811 Updated dependencies 2024-01-03 11:29:35 +01:00
bc09ff5f3c version bump that I forgot about 2024-01-03 11:28:36 +01:00
2188f2acd5 code cleanup 2024-01-03 00:59:22 +01:00
169be592ab Removed dollar signs in front of bash commands 2024-01-03 00:56:18 +01:00
20 changed files with 1415 additions and 100 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
/.vscode
.env
/resources

718
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "jonas_jones-api"
version = "0.1.0"
version = "0.4.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -11,9 +11,13 @@ parking_lot = "0.12.1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["macros"] }
dotenv = "0.15.0"
lastfm = "0.6.1"
lastfm = "0.10.0"
log = "0.4.20"
chrono = "0.4.31"
toml = "0.8.8"
reqwest = { version = "0.11.22", features = ["json"] }
reqwest = { version = "0.12.5", features = ["json", "blocking"] }
serde_json = "1.0.108"
regex = "1"
git2 = "0.19.0"
ip2location = "0.5.0"
sha2 = "0.10.8"

View file

@ -9,16 +9,16 @@ As of now, the project has no proper production build and unless the proper envi
Clone the repository and install the dependencies.
```bash
$ git clone git@github.com:J-onasJones/jonas_jones-api.git
$ cd jonas_jones-api
$ cargo build
git clone git@github.com:J-onasJones/jonas_jones-api.git
cd jonas_jones-api
cargo build
```
## Usage
To run the API, simply run the following command.
```bash
$ cargo run
cargo run
```
If you want to run the API in a production environment, you will need to set the following environment variables.
@ -29,8 +29,39 @@ If you want to run the API in a production environment, you will need to set the
- LASTFM_API_SECRET
```bash
$ export API_PORT={port}
$ export API_IP={ip_address}
$ export LASTFM_API_KEY={lastfm_api_key}
$ export LASTFM_API_SECRET={lastfm_api_secret}
export API_PORT={port}
export API_IP={ip_address}
export LASTFM_API_KEY={lastfm_api_key}
export LASTFM_API_SECRET={lastfm_api_secret}
```
## Docker Compose
`docker-compose.yaml` (folder paths need adjusting):
```yaml
version: '3.8'
services:
arch-linux:
image: archlinux:latest
container_name: jonas_jones-api
ports:
- "3030:3030"
volumes:
- /home/jonas_jones/jonas_jones-api:/home/jonas_jones/jonas_jones-api
- /home/jonas_jones/.config/rclone/:/root/.config/rclone/
command: ["sh", "-c", "pacman -Syu --noconfirm --needed pkg-config openssl python3 python-pip rclone cargo && python3 -m venv api-venv && source api-venv/bin/activate && cd /home/jonas_jones/jonas_jones-api && pip install -r requirements.txt && /usr/bin/cargo run"]
```
run container:
```sh
docker-compose up -d
```
## Roadmap
- analytics backend. track request origin through IP from header (store IP hash, region and time)
- rewrite all scripts in rust
- DB implementation for projects, kcomebacks, minecraft mod versions
- session backend, auth token system
- implementation for dashboard front-end with analytics/config
- complete minecraft mod implementation

11
docker-compose.yaml Normal file
View file

@ -0,0 +1,11 @@
version: '3.8'
services:
arch-linux:
image: archlinux:latest
container_name: jonas_jones-api
ports:
- "3030:3030"
volumes:
- /home/jonas_jones/jonas_jones-api:/home/jonas_jones/jonas_jones-api
- /home/jonas_jones/.config/rclone/:/root/.config/rclone/
command: ["sh", "-c", "pacman -Syu --noconfirm --needed pkg-config openssl python3 python-pip rclone cargo && python3 -m venv api-venv && source api-venv/bin/activate && cd /home/jonas_jones/jonas_jones-api && pip install -r requirements.txt && /usr/bin/cargo run"]

9
requirements.txt Normal file
View file

@ -0,0 +1,9 @@
requests
datetime
python-dotenv
spotipy
praw
spotipy
pylast
typing
markdown

14
src/iplookup.rs Normal file
View file

@ -0,0 +1,14 @@
use ip2location::{DB, Record};
pub fn ip_lookup(ip: &str) -> String {
let database_path = "resources/IP2LOCATION-LITE-DB5.IPV6.BIN/IP2LOCATION-LITE-DB5.IPV6.BIN";
let mut db = DB::from_file(database_path).unwrap();
let geo_info = db.ip_lookup(ip.parse().unwrap()).unwrap();
let record = if let Record::LocationDb(rec) = geo_info {
Some(rec)
} else { None };
return record.unwrap().country.unwrap().short_name.to_string();
}

View file

@ -1,19 +1,50 @@
use dotenv::dotenv;
use tokio::time::{sleep, Duration};
use v1::{run_sync_all_command};
use std::fs;
pub mod v1;
pub mod logger;
pub mod tools;
pub mod server;
pub mod error_responses;
pub mod iplookup;
pub mod request_logger;
pub use logger::Logger;
pub use tools::parse_ip;
async fn periodic_script_runner() {
loop {
Logger::info("Running periodic scripts...");
// Run all Functions
let _ = run_sync_all_command();
// Sleep for 6 hours
sleep(Duration::from_secs(6 * 60 * 60)).await;
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// load .env file
// Load .env file
dotenv().ok();
server::serve().await;
// Start the api
let server_task = tokio::spawn(async {
server::serve().await;
});
// periodic script runner
let second_task = tokio::spawn(async {
// check if the local repository exists, if not, clone it
if !fs::metadata("./resources/turbo_octo_potato").is_ok() {
v1::run_setup().unwrap();
};
periodic_script_runner().await;
});
// Wait for both tasks to complete
let _ = tokio::try_join!(server_task, second_task);
}

71
src/request_logger.rs Normal file
View file

@ -0,0 +1,71 @@
use chrono::Utc;
use reqwest::Client;
use reqwest::blocking::get;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::fs::OpenOptions;
use std::io::{BufReader, BufWriter};
use warp::http::Method;
use std::path::Path;
#[derive(Serialize, Deserialize)]
struct RequestLog {
timestamp: String,
method: String,
pathname: String,
ip_country_code: String,
ip_hash: String,
}
async fn get_ip_country_code(ip: &str) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("http://ip-api.com/json/{}", ip);
let response: Value = get(&url)?.json()?;
if let Some(country_code) = response["countryCode"].as_str() {
Ok(country_code.to_string())
} else {
Err("Could not fetch country code".into())
}
}
async fn hash_ip(ip: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(ip);
format!("{:x}", hasher.finalize())
}
pub async fn log_request(client: &Client, ip: &str, pathname: &str, method: &Method, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let method_str = method.as_str();
let timestamp = Utc::now().to_rfc3339();
let ip_country_code = get_ip_country_code(ip).await?;
let ip_hash = hash_ip(ip).await;
let log_entry = RequestLog {
timestamp,
method: method_str.to_string(),
pathname: pathname.to_string(),
ip_country_code,
ip_hash,
};
let path = Path::new(file_path);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path)?;
let mut logs: Vec<RequestLog> = if path.exists() {
let reader = BufReader::new(&file);
serde_json::from_reader(reader)?
} else {
Vec::new()
};
logs.push(log_entry);
let writer = BufWriter::new(&file);
serde_json::to_writer_pretty(writer, &logs)?;
Ok(())
}

View file

@ -1,13 +1,19 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use std::env;
use reqwest::StatusCode;
use lastfm::reqwest::StatusCode;
use warp::filters::path::FullPath;
use warp::http::Method;
use warp::Filter;
use warp::reply::Reply;
use reqwest::Client;
use crate::error_responses::{ErrorMessage, InternalServerError, BadRequestError, NotFoundError, NotImplementedError};
use crate::v1::get_v1_routes;
use crate::{Logger, parse_ip};
use crate::{parse_ip, request_logger, Logger};
use crate::iplookup::ip_lookup;
pub async fn serve() {
@ -20,9 +26,38 @@ pub async fn serve() {
let favicon = warp::path("favicon.ico").and(warp::fs::file("./src/favicon.png"));
// /status => 200 OK
let status = warp::path("status")
.map(|| warp::reply());
// Middleware filter to log request details
let log_request = warp::any()
.and(warp::method())
.and(warp::path::full())
.and(warp::addr::remote())
.and(warp::header::optional::<String>("x-forwarded-for"))
.map(|method: Method, path: FullPath, addr: Option<SocketAddr>, fwd_for: Option<String>| {
let client_ip = fwd_for.unwrap_or_else(|| addr.map(|a| a.ip().to_string()).unwrap_or_else(|| String::from("unknown")));
let path_str = path.as_str().to_string(); // Convert to owned String
let method_clone = method.clone();
let method_str = method_clone.clone().as_str().to_string();
let client_ip_clone = client_ip.clone(); // Clone for use outside the async block
let path_str_clone = path_str.clone();
/*tokio::spawn(async move {
let client = Client::new();
if let Err(e) = request_logger::log_request(&client, &client_ip, &path_str, &method_clone, "requests.json").await {
eprintln!("Failed to log request: {:?}", e);
}
});*/
Logger::info(&format!("{} {} from {} ({})", method_str, path_str_clone, ip_lookup(&client_ip_clone), client_ip_clone));
});
// GET (any) => reply with return from handle_path
let routes = favicon.or(get_v1_routes())
.recover(handle_rejection);
let routes = log_request
.clone().untuple_one().and(favicon.or(status.or(get_v1_routes())
.recover(handle_rejection)));
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
@ -43,7 +78,7 @@ pub async fn serve() {
message: message.into(),
});
Ok(warp::reply::with_status(json, code))
Ok(warp::reply::with_status(json, StatusCode::from_u16(code.as_u16()).unwrap()))
}

8
src/v1/analytics/mod.rs Normal file
View file

@ -0,0 +1,8 @@
use warp::Filter;
pub fn get_analytics_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("v1").and(warp::path("analytics"))
.and((warp::path("logcdnrequest").map(|| "Please refer to the wiki at https://wiki.jonasjones.dev/Api/"))
.or(warp::path("ping").map(|| "pong"))
.or(warp::path("version").map(|| warp::reply::json(&[option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")]))))
}

7
src/v1/auth/mod.rs Normal file
View file

@ -0,0 +1,7 @@
use warp::Filter;
pub fn get_auth_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("v1").and(warp::path("auth"))
.and((warp::path("requestsession").and(warp))
.or(warp::path("login")))
}

View file

@ -1,5 +1,3 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use warp::Filter;
@ -10,7 +8,7 @@ mod upcoming;
use filter::get_kcomebacks_filter_routes;
use upcoming::get_kcomebacks_upcoming_routes;
use crate::error_responses::{InternalServerError, BadRequestError, NotFoundError};
use crate::error_responses::InternalServerError;
pub fn get_kcomebacks_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("v1").and(warp::path("kcomebacks"))

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, ops::Add};
use warp::Filter;
use crate::{v1::kcomebacks::filter::filter_daterange_handler, error_responses::BadRequestError};
use crate::v1::kcomebacks::filter::filter_daterange_handler;
pub fn get_kcomebacks_upcoming_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("upcoming")

View file

@ -2,13 +2,16 @@ mod builtin;
mod debug;
mod kcomebacks;
mod projects;
mod updates;
mod run;
pub use run::setup as run_setup;
pub use run::run_sync_all_command;
pub use builtin::get_builtin_routes as get_v1_builtin_routes;
pub use debug::get_debug_routes as get_v1_debug_routes;
pub use kcomebacks::get_kcomebacks_routes as get_v1_kcomebacks_routes;
pub use projects::get_project_routes as get_v1_project_routes;
pub use updates::get_updates_routes as get_v1_updates_routes;
pub use run::get_run_routes as get_v1_updates_routes;
use warp::Filter;

View file

@ -1,10 +1,9 @@
use std::{collections::{HashMap, HashSet}, vec};
use std::collections::{HashMap, HashSet};
use reqwest::StatusCode;
use serde_json::{Value, json};
use warp::Filter;
use crate::{error_responses::{BadRequestError, InternalServerError, NotImplementedError}, v1::projects::{fetch_data, create_json_response, Project as EntryProject}};
use crate::{error_responses::BadRequestError, v1::projects::{fetch_data, create_json_response, Project as EntryProject}};
pub fn get_project_filter_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("filter")
@ -399,8 +398,6 @@ async fn filter_category_handler(params: HashMap<String, String>) -> Result<imp
}
async fn filter_language_handler(params: HashMap<String, String>) -> Result<impl warp::Reply, warp::Rejection> {
return Err(warp::reject::custom(NotImplementedError));
return Ok(warp::reply::html("placeholder for compiler to be happy"));
// Access the parameres from the HashMap
let language = params.get("language").unwrap_or(&"".to_string()).to_string();
let limit = params.get("limit").unwrap_or(&"".to_string()).to_string();
@ -429,7 +426,7 @@ async fn filter_language_handler(params: HashMap<String, String>) -> Result<impl
.iter()
.filter_map(|item| serde_json::from_value::<EntryProject>(item.clone()).ok())
.filter(|project| project.visible)
//.filter(|project| project.languages.iter().any(|lang| lang.contains_key(language)))
.filter(|project| project.languages.contains_key(&language))
.collect()
}
_ => Vec::new(),
@ -442,16 +439,14 @@ async fn filter_language_handler(params: HashMap<String, String>) -> Result<impl
let filtered_data = filtered_data.iter().skip(offset.parse::<usize>().unwrap()).take(limit.parse::<usize>().unwrap()).collect::<Vec<_>>();
// return the data
//Ok(warp::reply::json(&create_json_response(filtered_data, total_results)))
Ok(warp::reply::json(&create_json_response(filtered_data, total_results)))
}
async fn filter_getlangs_handler() -> Result<impl warp::Reply, warp::Rejection> {
return Err(warp::reject::custom(NotImplementedError));
return Ok(warp::reply::html("placeholder for compiler to be happy"));
// fetch the data
let data = fetch_data().await.unwrap();
// filter the data
//filter the data
let filtered_data: Vec<EntryProject> = match data {
Value::Array(items) => {
items
@ -464,16 +459,25 @@ async fn filter_getlangs_handler() -> Result<impl warp::Reply, warp::Rejection>
};
// filter the data
/*let all_language_keys: Vec<&String> = filtered_data
.iter()
.flat_map(|project| project.languages.iter().flat_map(|lang_map| lang_map.keys()))
.collect();*/
let mut languages_set: HashSet<String> = HashSet::new();
for project in filtered_data {
for language in project.languages.keys() {
languages_set.insert(language.clone());
}
}
// get the total number of results
//let total_results = all_language_keys.len();
let total_results = languages_set.len();
// json response
let json_response = json!({
"results": languages_set,
"total_results": total_results,
});
// return the data
//Ok(warp::reply::json(&create_json_response(all_language_keys, total_results)))
Ok(warp::reply::json(&json_response))
}
async fn filter_getstatuses_handler() -> Result<impl warp::Reply, warp::Rejection> {

View file

@ -15,7 +15,8 @@ pub fn get_project_routes() -> impl warp::Filter<Extract = impl warp::Reply, Err
.and(warp::path("last_update").and(warp::get()).and_then(last_update)
.or(warp::path("start_update").map(|| "Not implemented yet"))
.or(get_project_filter_routes()))
.or(get_project_filter_routes())
)
}
// get json data from https://https://cdn.jonasjones.dev/api/projects/projects.json
@ -73,11 +74,3 @@ pub fn create_json_response(items: Vec<&Project>, total_results: usize) -> Value
json_response
}
pub fn parse_item(item: &Value) -> Project {
// Parse the item into a struct
let item: Project = serde_json::from_value(item.clone()).unwrap();
// Return the parsed item
item
}

284
src/v1/run/mod.rs Normal file
View file

@ -0,0 +1,284 @@
use std::fs;
// use std::io::BufRead;
use std::process::{Stdio, Command};
use serde::{Deserialize, Serialize};
use serde_json::json;
// use tokio::sync::mpsc;
use tokio::task;
use warp::Filter;
use crate::error_responses::InternalServerError;
use crate::Logger;
// DiscographyQuery
#[derive(Debug, Deserialize, Serialize)]
pub struct DiscographyQuery {
artists: String,
}
pub fn get_run_routes() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path("v1").and(warp::path("run"))
// update/kcomebacks
// update/projects
// update/makediscography?artists=artist1,artist2,artist3
// update/synclikedsongs
.and((warp::path("kcomebacks").and_then(update_kcomebacks))
.or(warp::path("projects").and_then(update_projects))
.or(warp::path("makediscography").map(||"Not implemented yet"))
.or(warp::path("synclikedsongs").and_then(sync_liked_songs))
.or(warp::path("sync_all").and_then(sync_all))
)
}
async fn update_kcomebacks() -> Result<impl warp::Reply, warp::Rejection> {
// check if the local repository exists, if not, clone it
if !fs::metadata("./resources/turbo_octo_potato").is_ok() {
setup().unwrap();
};
if let Err(err) = run_kcomebacks_command() {
// Handle the error here
eprintln!("Error: {}", err);
// Return an appropriate response or error
return Err(warp::reject::custom(InternalServerError));
}
Ok(warp::reply::json(&json!({"status": "updating..."})))
}
async fn update_projects() -> Result<impl warp::Reply, warp::Rejection> {
// check if the local repository exists, if not, clone it
if !fs::metadata("./resources/turbo_octo_potato").is_ok() {
setup().unwrap();
};
if let Err(err) = run_projects_command() {
// Handle the error here
eprintln!("Error: {}", err);
// Return an appropriate response or error
return Err(warp::reject::custom(InternalServerError));
}
Ok(warp::reply::json(&json!({"status": "updating..."})))
}
async fn sync_liked_songs() -> Result<impl warp::Reply, warp::Rejection> {
// check if the local repository exists, if not, clone it
if !fs::metadata("./resources/turbo_octo_potato").is_ok() {
setup().unwrap();
};
if let Err(err) = run_likedsongs_command() {
// Handle the error here
eprintln!("Error: {}", err);
// Return an appropriate response or error
return Err(warp::reject::custom(InternalServerError));
}
Ok(warp::reply::json(&json!({"status": "updating..."})))
}
async fn sync_all() -> Result<impl warp::Reply, warp::Rejection> {
// check if the local repository exists, if not, clone it
if !fs::metadata("./resources/turbo_octo_potato").is_ok() {
setup().unwrap();
};
if let Err(err) = run_sync_all_command() {
// Handle the error here
eprintln!("Error: {}", err);
// Return an appropriate response or error
return Err(warp::reject::custom(InternalServerError));
}
Ok(warp::reply::json(&json!({"status": "syncing..."})))
}
pub fn setup() -> Result<(), git2::Error> {
let repository_url = "https://github.com/JonasunderscoreJones/turbo-octo-potato.git";
let local_directory = "resources/turbo_octo_potato";
git2::Repository::clone(repository_url, local_directory)?;
Ok(())
}
// fn run_command() -> Result<(), std::io::Error> {
// let (tx, mut rx) = mpsc::channel(1);
// task::spawn_blocking(move || {
// let mut child = Command::new("python3")
// .arg(&py_file)
// .arg(&args)
// .current_dir("resources/turbo_octo_potato")
// .stdout(Stdio::piped())
// .spawn()
// .expect("failed to execute child");
// let stdout = child.stdout.as_mut().unwrap();
// let mut reader = std::io::BufReader::new(stdout);
// let mut line = String::new();
// loop {
// let len = reader.read_line(&mut line).unwrap();
// if len == 0 {
// break;
// }
// tx.blocking_send(line.clone()).unwrap();
// line.clear();
// }
// child.wait().unwrap();
// });
// task::spawn(async move {
// while let Some(line) = rx.recv().await {
// println!("{}", line);
// }
// });
// Ok(())
// }
// run_command with python file and args as parameters
pub fn run_kcomebacks_command() -> Result<(), std::io::Error> {
// let (tx, mut rx) = mpsc::channel(1);
task::spawn_blocking(move || {
let mut child = Command::new("python3")
.arg("rpopfetch.py")
.arg("--cdn")
.current_dir("resources/turbo_octo_potato")
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");
// let stdout = child.stdout.as_mut().unwrap();
// let mut reader = std::io::BufReader::new(stdout);
// let mut line = String::new();
// loop {
// let len = reader.read_line(&mut line).unwrap();
// if len == 0 {
// break;
// }
// tx.blocking_send(line.clone()).unwrap();
// line.clear();
// }
child.wait().unwrap();
});
// task::spawn(async move {
// while let Some(line) = rx.recv().await {
// Logger::info(&format!("[/v1/kcomebacks/update]: {}", line));
// }
// });
Logger::info("Updating kcomebacks...");
Ok(())
}
pub fn run_projects_command() -> Result<(), std::io::Error> {
// let (tx, mut rx) = mpsc::channel(1);
task::spawn_blocking(move || {
let mut child = Command::new("python3")
.arg("update_projects.py")
.arg("--cdn")
.current_dir("resources/turbo_octo_potato")
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");
// let stdout = child.stdout.as_mut().unwrap();
// let mut reader = std::io::BufReader::new(stdout);
// let mut line = String::new();
// loop {
// let len = reader.read_line(&mut line).unwrap();
// if len == 0 {
// break;
// }
// tx.blocking_send(line.clone()).unwrap();
// line.clear();
// }
child.wait().unwrap();
});
// task::spawn(async move {
// while let Some(line) = rx.recv().await {
// Logger::info(&format!("[/v1/projects/update]: {}", line));
// }
// });
Logger::info("Updating projects...");
Ok(())
}
pub fn run_likedsongs_command() -> Result<(), std::io::Error> {
// let (tx, mut rx) = mpsc::channel(1);
task::spawn_blocking(move || {
let mut child = Command::new("python3")
.arg("likedsongsync2.py")
.current_dir("resources/turbo_octo_potato")
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");
// let stdout = child.stdout.as_mut().unwrap();
// let mut reader = std::io::BufReader::new(stdout);
// let mut line = String::new();
// loop {
// let len = reader.read_line(&mut line).unwrap();
// if len == 0 {
// break;
// }
// tx.blocking_send(line.clone()).unwrap();
// line.clear();
// }
child.wait().unwrap();
});
// task::spawn(async move {
// while let Some(line) = rx.recv().await {
// Logger::info(&format!("[/v1/synclikedsongs]: {}", line));
// }
// });
Logger::info("Syncing liked songs...");
Ok(())
}
pub fn run_sync_all_command() -> Result<(), std::io::Error> {
task::spawn_blocking(move || {
let mut child = Command::new("python3")
.arg("script_interval_runner.py")
.current_dir("resources/turbo_octo_potato")
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");
child.wait().unwrap();
});
Logger::info("Running all Sync Scripts...");
Ok(())
}

View file

@ -1,29 +1,144 @@
use std::collections::HashMap;
use std::net::IpAddr;
use reqwest::Error;
use serde_json::json;
use serde::{Deserialize, Serialize};
use warp::Filter;
use regex::Regex;
use crate::error_responses::BadRequestError;
pub fn get_mods_paths() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
// any path that starts with /v1/updates/minecraft/mods/{modname}/{loadername}/{version} calls handle_path
warp::path("v1").and(warp::path("updates")).and(warp::path("minecraft")).and(warp::path("mods"))
.and(warp::path::param())
.and(warp::path::param())
.and(warp::path::param())
.and(warp::path::end())
.and(warp::addr::remote())
.map(handle_path)
.and(warp::get().and(warp::path::param()).and(warp::path::param()).and(warp::path::param()).and(warp::path::param()).and(warp::path::end()).and(warp::filters::header::headers_cloned()).and(warp::query::<HashMap<String, String>>()).and_then(handle_path))
}
fn handle_path(modname: String, loadername: String, version: String, remote_ip: Option<std::net::SocketAddr>) -> String {
format!("modname: {}, loadername: {}, version: {}, IP: {}", modname, loadername, version, remote_ip.unwrap_or(std::net::SocketAddr::from(([0, 0, 0, 0], 0))).ip())
#[derive(Debug, Deserialize, Serialize)]
struct ModData {
package: String,
name: String,
versions: Vec<HashMap<String, HashMap<String, ModVersion>>>,
}
fn handle_with_headers(
headers: warp::http::HeaderMap,
) -> String {
// Iterate through the headers and print them
for (name, value) in headers.iter() {
println!("Header: {}: {}", name, value.to_str().unwrap_or("Invalid UTF-8"));
#[derive(Debug, Deserialize, Serialize, Clone)]
struct ModVersion {
recommended: String,
latest: String,
all: Vec<String>,
}
// get json data from https://https://cdn.jonasjones.dev/api/mcmods/mcmod_metadata.json
pub async fn fetch_data() -> Result<serde_json::Value, Error> {
let url = "https://cdn.jonasjones.dev/api/mcmods/mcmod_metadata.json";
let response = reqwest::get(url).await?;
if response.status().is_success() {
// Parse the JSON response
let json_data: serde_json::Value = response.json().await?;
return Ok(json_data);
} else {
// Handle non-successful status codes
Err(response.error_for_status().unwrap_err())
}
}
fn is_valid_ip(ip_str: &str) -> bool {
if let Ok(ip) = ip_str.parse::<IpAddr>() {
match ip {
IpAddr::V4(_) => true,
IpAddr::V6(_) => true,
}
} else {
false
}
}
fn is_valid_minecraft_version(version: &str) -> bool {
// Define the regex pattern for the Minecraft version
let pattern = Regex::new(r"^1\.\d{1,2}(\.\d)?$").unwrap();
// Check if the provided version matches the pattern
pattern.is_match(version)
}
fn get_header_forward_for_ip(headers: warp::http::HeaderMap) -> String {
// check if the header X-Forward-For exists and return the ip, if not, return an empty string
if let Some(forwarded_for) = headers.get("X-Forwarded-For") {
if let Ok(ip) = forwarded_for.to_str() {
// Extract the first IP address from the comma-separated list
if let Some(first_ip) = ip.split(',').next() {
return first_ip.trim().to_string();
}
}
}
String::new()
}
async fn handle_path(modpackage: String, loadername: String, mcversion: String, modversion: String, headers: warp::http::HeaderMap, params: HashMap<String, String>) -> Result<impl warp::Reply, warp::Rejection> {
// Retrieve the IP from the header and check if it's valid
let mut client_ip = get_header_forward_for_ip(headers);
if !is_valid_ip(&client_ip) {
client_ip = params.get("ip").unwrap_or(&"".to_string()).to_string();
if !is_valid_ip(&client_ip) {
client_ip = "Not valid".to_string();
}
}
// Respond with a message or perform other actions as needed
"Headers received".to_string()
}
// check if the minecraft version is valid
if !is_valid_minecraft_version(&mcversion) {
return Err(warp::reject::custom(BadRequestError));
}
// fetch the data
let data = fetch_data().await.unwrap();
// filter the data
// convert the raw list of data into a list of ModData and ModVersion
let mods_data: Vec<ModData> = serde_json::from_value(data).unwrap();
// get the mod data from the requested mod
let mod_data: ModData = mods_data.into_iter().find(|mod_data| mod_data.package == modpackage).unwrap();
// get the version data from the requested loader and remove the other loaders
let version_data: HashMap<std::string::String, ModVersion> = mod_data.versions.into_iter().find(|version_data| version_data.contains_key(&loadername)).unwrap().remove(&loadername).unwrap();
// turn version_data into an object of String: ModVersion key value pairs
let version_data: HashMap<std::string::String, ModVersion> = version_data.into_iter().map(|(key, value)| (key, value)).collect();
// get the version data for the current minecraft version
let version_data: ModVersion = version_data.get(&mcversion).unwrap().clone();
// get recommended and latest version
let recommended_version = version_data.recommended.clone();
let latest_version = version_data.latest.clone();
// determine whether the client is up to date
let mut up_to_date = false;
if modversion == recommended_version {
up_to_date = true;
}
// determine if telemetry is enabled by checking if the client_ip is valid
let mut telemetry = false;
if is_valid_ip(&client_ip) {
telemetry = true;
}
// create the response
let response = json!({
"promos": {
"latest": latest_version,
"recommended": recommended_version
},
"upToDate": up_to_date,
"telemetry_enabled": telemetry
});
//TODO: Add way to process telemetry data
// return the data
return Ok(warp::reply::json(&response));
}

View file

@ -0,0 +1,46 @@
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::Path;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct IpInfo {
region: String,
// Add other fields as needed
}
fn get_ip_hash(ip: &str) -> u64 {
let mut hasher = DefaultHasher::new();
ip.hash(&mut hasher);
hasher.finish()
}
pub fn log_ip_info(ip_address: &str, file_path: &str, mod_package: &str) -> io::Result<()> {
let ip_hash = get_ip_hash(ip_address);
let ip_info = match get_ip_info(ip_address) {
Ok(info) => info,
Err(err) => {
IpInfo { region: "Unknown".to_string() } // Default to "Unknown" in case of an error
}
};
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?;
writeln!(file, "{} {} {}", ip_hash, ip_info.region, mod_package)?;
Ok(())
}
fn main() {
let file_path = "ip_log.txt"; // Replace with your desired file path
// Example usage
match log_ip_info("8.8.8.8", file_path) {
Err(err) => eprintln!("Error: {}", err),
}
}