Skip to content

Commit

Permalink
Auth
Browse files Browse the repository at this point in the history
  • Loading branch information
aNaOH committed Jun 11, 2024
1 parent 9b4b8fc commit 35edd4c
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 21 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ modio = { git = "https://github.com/nickelc/modio-rs", branch = "master" }
tokio = { version = "1.32.0", features = ["full"] }
zip = "0.5"
reqwest = { version = "0.11", features = ["json"] }
image = "0.23"
image = "0.23"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ Adds mod.io integration for Godot using GDExtension.

Currently supports:

- Windows
- OSX
- Linux
- Windows
- OSX
- Linux

**TODO**
# How to initialize

- Upload mods

## How to use
First, you have to get an API key for your game at https://mod.io/g/your-game/admin/api-key (if you don't have a game page on mod.io, get it [here](https://mod.io/g/add/)) alongside with the game ID on the section API path from the same page: https://g-gameid.modapi.io/v1

var modio = ModIO.new()
modio.connect(api_key, game_id)
var mods = modio.get_mods(query, page, per_page)

# Functions

## Basic

### get_mods(query : String, page : int, per_page : int)

get_mods() returns an array of dictioraries, the dictionary is formed by:

Expand All @@ -37,3 +40,21 @@ get_mods() returns an array of dictioraries, the dictionary is formed by:
- tags

get_mods() needs a string argument for the search query, if you want to list all mods just use "" as the argument

### upload_mod(api_key: String, modfile_path: String, name: String, summary: String, thumbnail_path: String)

Needs an API key obtained using one of the auth functions
Compresses the file at modfile_path onto a zip file and uploads it to mod.io with the specified name and summary, also uploads the thumbnail at thumbnail_path (This image has to have an aspect ratio of 16:9, a min resolution of 512x288 and max size of 8MB)
Returns the mod dictionary.

## Auth

These functions returns a Dictionary with an api key located in dictionary["api_key"]

### login_with_email(email: String, password: String)

Uses email and password to login to mod.io

### login_with_steam(app_id: int, ticket: String)

Uses the Steam AppID and user auth ticket to login to mod.io
113 changes: 101 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use tokio::fs::read;

use image::GenericImageView;

use serde::Deserialize;

struct ModIOAddon;

#[gdextension]
Expand Down Expand Up @@ -136,6 +138,12 @@ impl ModIOMod {
}
}

#[derive(Deserialize)]
struct AuthResponse {
access_token: String,
expires_in: u64,
}

struct ModIOClient {
client: Modio,
id: u64
Expand Down Expand Up @@ -176,7 +184,7 @@ impl ModIOClient {
Ok(())
}

async fn upload_mod_via_api(&self, modfile_path: &str, name: &str, summary: &str, api_key: &str, thumbnail_path: Option<&str>) -> Result<ModIOMod, Box<dyn std::error::Error>> {
async fn upload_mod_via_api(&self, modfile_path: &str, name: &str, summary: &str, api_key: &str, thumbnail_path: &str) -> Result<ModIOMod, Box<dyn std::error::Error>> {
let zip_path = format!("{}.zip", modfile_path);
Self::compress_to_zip(modfile_path, &zip_path).await?;

Expand All @@ -188,9 +196,8 @@ impl ModIOClient {
.text("summary", summary.to_string())
.part("modfile", multipart::Part::bytes(modfile).file_name("mod.zip"));

if let Some(thumb_path) = thumbnail_path {
let thumbnail = read(thumb_path).await?;
let img = image::open(thumb_path)?;
let thumbnail = read(thumbnail_path).await?;
let img = image::open(thumbnail_path)?;

if img.width() * 9 != img.height() * 16 || img.width() < 512 || img.height() < 288 {
return Err("Thumbnail must be 16:9 and at least 512x288".into());
Expand All @@ -201,7 +208,6 @@ impl ModIOClient {
}

form = form.part("logo", multipart::Part::bytes(thumbnail).file_name("thumbnail.png"));
}

let response = client.post(format!("https://api.mod.io/v1/games/{}/mods", self.id))
.header("Authorization", format!("Bearer {}", api_key))
Expand All @@ -214,6 +220,44 @@ impl ModIOClient {
let mod_info = ModIOMod::from_mod(&response);
Ok(mod_info)
}

pub async fn login_with_email(&self, email: &str, password: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::new();
let response = client.post("https://api.mod.io/v1/oauth/email")
.form(&[
("email", email),
("password", password),
("grant_type", "password"),
])
.send()
.await?;

if response.status().is_success() {
let auth: AuthResponse = response.json().await?;
Ok(auth.access_token)
} else {
Err("Failed to login with email and password".into())
}
}

pub async fn login_with_steam(&self, app_id: &str, ticket: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::new();
let response = client.post("https://api.mod.io/v1/oauth/steam")
.form(&[
("appdata", app_id),
("ticket", ticket),
("grant_type", "steam"),
])
.send()
.await?;

if response.status().is_success() {
let auth: AuthResponse = response.json().await?;
Ok(auth.access_token)
} else {
Err("Failed to login with Steam".into())
}
}
}

#[derive(GodotClass)]
Expand Down Expand Up @@ -316,18 +360,13 @@ impl ModIO {
}

#[func]
fn upload_mod(&self, api_key: GString, modfile_path: GString, name: GString, summary: GString, thumbnail_path: Dictionary) -> Dictionary {
fn upload_mod(&self, api_key: GString, modfile_path: GString, name: GString, summary: GString, thumbnail_path: GString) -> Dictionary {
let empty_dict = Dictionary::new();
if let Some(ref client) = self.client {
// Create a new task and execute it
let result = async {
// Extract the thumbnail path from the dictionary
let thumb_path: Option<String> = thumbnail_path
.get("path")
.and_then(|v| v.try_to::<GString>().ok())
.map(|s| s.to_string());

match client.upload_mod_via_api(&modfile_path.to_string(), &name.to_string(), &summary.to_string(), &api_key.to_string(), thumb_path.as_deref()).await {
match client.upload_mod_via_api(&modfile_path.to_string(), &name.to_string(), &summary.to_string(), &api_key.to_string(), &thumbnail_path.to_string()).await {
Ok(mod_info) => {
// Print information about the uploaded mod
godot_print!("Mod uploaded successfully: {}", mod_info.name);
Expand All @@ -352,4 +391,54 @@ impl ModIO {
empty_dict
}
}

#[func]
fn login_with_email(&self, email: GString, password: GString) -> Dictionary {
let empty_dict = Dictionary::new();
if let Some(ref client) = self.client {
let result = async {
match client.login_with_email(&email.to_string(), &password.to_string()).await {
Ok(api_key) => {
let mut dict = Dictionary::new();
dict.insert("api_key", api_key);
dict
}
Err(err) => {
godot_print!("Error logging in with email: {:?}", err);
empty_dict
}
}
};

let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(result)
} else {
empty_dict
}
}

#[func]
fn login_with_steam(&self, app_id: u64, ticket: GString) -> Dictionary {
let empty_dict = Dictionary::new();
if let Some(ref client) = self.client {
let result = async {
match client.login_with_steam(&app_id.to_string(), &ticket.to_string()).await {
Ok(api_key) => {
let mut dict = Dictionary::new();
dict.insert("api_key", api_key);
dict
}
Err(err) => {
godot_print!("Error logging in with Steam: {:?}", err);
empty_dict
}
}
};

let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(result)
} else {
empty_dict
}
}
}

0 comments on commit 35edd4c

Please sign in to comment.