Initial Deployment

This commit is contained in:
saulteafarmer 2025-05-18 16:29:10 +00:00
parent d9dcb19db0
commit 2e56f77d6c
3 changed files with 617 additions and 0 deletions

204
src/battlemetrics.rs Normal file
View File

@ -0,0 +1,204 @@
use serde::{Deserialize, Serialize};
use crate::commands::{Context, Error};
#[derive(Serialize, Deserialize, Debug)]
struct CommandOptions {
raw: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct CommandAttributes {
command: String,
options: CommandOptions,
}
#[derive(Serialize, Deserialize, Debug)]
struct RconCommand {
#[serde(rename = "type")]
type_field: String,
attributes: CommandAttributes,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct RconData {
data: RconCommand,
}
impl RconData {
pub fn new(type_field: &str, command: &str, raw: &str) -> Self {
RconData {
data: RconCommand {
type_field: type_field.to_string(),
attributes: CommandAttributes {
command: command.to_string(),
options: CommandOptions {
raw: raw.to_string(),
},
},
},
}
}
}
pub async fn mint_blood(
name: Option<String>,
amount: String,
ctx: Context<'_>,
api_client: &reqwest::Client,
) -> Result<(), Error> {
if let Some(name) = name.clone() {
let player_name = name;
let short_name = "blood";
let command_name = format!(
r#"inventory.giveto "{}" "{}" {}"#,
player_name, short_name, amount
);
println!("{:?}: Running Command: {}", player_name, command_name);
let rcon_data = RconData::new("rconCommand", "raw", &command_name);
let serialized_data = if let Ok(data) = serde_json::to_string(&rcon_data) {
data
} else {
println!("error serializing data");
return Ok(());
};
let server_id = ctx.data().server_id.clone();
let url = format!(
"https://api.battlemetrics.com/servers/{}/command",
server_id
);
let bm_token = ctx.data().bm_token.clone();
let res = api_client
.post(&url)
.header("Authorization", format!("Bearer {}", bm_token))
.header("Content-Type", "application/json")
.body(serialized_data)
.send()
.await?;
if res.status() == 200 {
let reply = ctx
.channel_id()
.say(
ctx.http(),
format!("{} has been payed {} blood", player_name, amount),
)
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
println!("{:?} blood minted.", player_name);
Ok(())
} else {
println!("{:?} blood failed to mint.", player_name);
let reply = ctx
.channel_id()
.say(
ctx.http(),
format!("Failed to pay {} blood to {}.", amount, player_name),
)
.await;
if let Err(e) = reply {
println!("errror: {}", e);
}
Ok(())
}
} else {
println!("error minting blood");
let reply = ctx
.channel_id()
.say(ctx.http(), "Failed to parse amount")
.await;
if let Err(e) = reply {
println!("errror: {}", e);
}
Ok(())
}
}
pub async fn unmute_player(
name: Option<String>,
ctx: Context<'_>,
api_client: &reqwest::Client,
) -> Result<(), Error> {
if let Some(name) = name.clone() {
let player_name = name;
let command_name = format!(r#"unmute "{}""#, player_name);
println!("{:?}: Running Command: {}", player_name, command_name);
let rcon_data = RconData::new("rconCommand", "raw", &command_name);
let serialized_data = if let Ok(data) = serde_json::to_string(&rcon_data) {
data
} else {
println!("error serializing data");
return Ok(());
};
let server_id = ctx.data().server_id.clone();
let url = format!(
"https://api.battlemetrics.com/servers/{}/command",
server_id
);
let bm_token = ctx.data().bm_token.clone();
let res = api_client
.post(&url)
.header("Authorization", format!("Bearer {}", bm_token))
.header("Content-Type", "application/json")
.body(serialized_data)
.send()
.await?;
if res.status() == 200 {
let reply = ctx
.channel_id()
.say(ctx.http(), format!("{} has been unmuted", player_name))
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
println!("{:?} unmuted.", player_name);
Ok(())
} else {
println!("Failed to unmute {:?}", player_name);
let reply = ctx
.channel_id()
.say(
ctx.http(),
format!("Failed to unmute player {}.", player_name),
)
.await;
if let Err(e) = reply {
println!("errror: {}", e);
}
Ok(())
}
} else {
println!("error unmuting player");
let reply = ctx
.channel_id()
.say(ctx.http(), "Failed to unmute player")
.await;
if let Err(e) = reply {
println!("errror: {}", e);
}
Ok(())
}
}

350
src/commands.rs Normal file
View File

@ -0,0 +1,350 @@
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use qrcode_generator::QrCodeEcc;
use std::{thread::sleep, time::Duration};
use zebedee_rust::charges::*;
use crate::{
battlemetrics::{mint_blood, unmute_player},
Data,
};
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Context<'a> = poise::Context<'a, Data, Error>;
/// Responds with ln invoice
#[poise::command(prefix_command, track_edits, aliases("amount, name"), slash_command)]
pub async fn giveblood(
ctx: Context<'_>,
#[description = "blood amount to buy"] amount: Option<String>,
#[description = "In game name"] name: Option<String>,
) -> Result<(), Error> {
let zebedee_client = &ctx.data().zbd;
let api_client = &ctx.data().api_client;
let name_str = name.as_deref().unwrap_or("User");
if let Some(amount) = amount {
if let Ok(mut num) = amount.parse::<i32>() {
num *= 1000;
let new_amount = num.to_string();
let charge = Charge {
amount: new_amount,
description: "Buy Blood".to_string(),
expires_in: 40,
..Default::default()
};
match zebedee_client.create_charge(&charge).await {
Ok(invoice) => {
if let Some(request_data) = invoice.data {
let requested_invoice =
if let Some(requested_invoice) = request_data.invoice {
requested_invoice
} else {
let reply = ctx
.channel_id()
.say(ctx.http(), "Failed to get invoice data.")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
return Ok(());
};
match serde_json::to_string(&requested_invoice.request) {
Ok(serialized_request_data) => {
let data = serialized_request_data.trim_matches('"').to_string();
let qr_invoice: Vec<u8> = qrcode_generator::to_png_to_vec(
data.clone(),
QrCodeEcc::Low,
1024,
)
.unwrap();
let description = format!("{:?}, please pay {} sats to the following invoice to give {} blood.", name_str, amount, amount);
let embed = CreateEmbed::new()
.title("Blood Invoice")
.description(description)
.fields(vec![
("Amount", amount.clone(), false),
("Expires in", "40 seconds".to_string(), false),
("Invoice: ", data.clone(), false),
]);
let attachment =
CreateAttachment::bytes(qr_invoice.as_slice(), "qr.png");
let builder =
CreateMessage::new().embed(embed).add_file(attachment);
let invoice_message =
ctx.channel_id().send_message(&ctx.http(), builder).await;
match invoice_message {
Ok(_) => {
println!("{:?}: invoice sent...", name_str);
}
Err(e) => {
println!("{:?}: Failed to send invoice: {}", name_str, e);
}
};
}
Err(e) => {
let reply = ctx
.channel_id()
.say(
ctx.http(),
format!("Failed to serialize request data: {}", e),
)
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
}
}
loop {
sleep(Duration::from_millis(1000));
match zebedee_client.get_charge(request_data.id.clone()).await {
Ok(charge) => {
if let Some(data) = charge.data {
match data.status.as_str() {
"completed" => {
println!(
"{:?}: payment completed...sending blood...",
name_str
);
let give_blood = mint_blood(
name.clone(),
amount.clone(),
ctx,
api_client,
)
.await;
if let Err(e) = give_blood {
println!("sending blood error: {}", e);
}
break;
}
"expired" => {
let reply = ctx
.channel_id()
.say(ctx.http(), "payment expired")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
println!("{:?}: payment expired.", name_str);
break;
}
"error" => {
let reply = ctx
.channel_id()
.say(ctx.http(), "payment error")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
break;
}
_ => {
println!("{:?}: Waiting for payment...", name_str);
}
}
}
}
Err(_) => {
println!("error...");
break;
}
}
}
} else {
println!("invoice data error...");
}
}
Err(e) => {
let reply = ctx
.channel_id()
.say(ctx.http(), format!("Failed to create charge: {}", e))
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
}
}
} else {
let reply = ctx
.channel_id()
.say(ctx.http(), "Please enter a valid number for the amount.")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
}
}
Ok(())
}
#[poise::command(prefix_command, track_edits, aliases("name"), slash_command)]
pub async fn unmute(
ctx: Context<'_>,
#[description = "In game name to unmute"] name: Option<String>,
) -> Result<(), Error> {
let zebedee_client = &ctx.data().zbd;
let api_client = &ctx.data().api_client;
let name_str = name.as_deref().unwrap_or("User");
let amount = 1000;
let new_amount = amount * 1000;
let charge = Charge {
amount: new_amount.to_string(),
description: "unmute player".to_string(),
expires_in: 40,
..Default::default()
};
match zebedee_client.create_charge(&charge).await {
Ok(invoice) => {
if let Some(request_data) = invoice.data {
let requested_invoice = if let Some(requested_invoice) = request_data.invoice {
requested_invoice
} else {
let reply = ctx
.channel_id()
.say(ctx.http(), "Failed to get invoice data.")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
return Ok(());
};
match serde_json::to_string(&requested_invoice.request) {
Ok(serialized_request_data) => {
let data = serialized_request_data.trim_matches('"').to_string();
let qr_invoice: Vec<u8> =
qrcode_generator::to_png_to_vec(data.clone(), QrCodeEcc::Low, 1024)
.unwrap();
let description = format!(
"{:?}, please pay {} sats to the following invoice to unmute {}.",
name_str, amount, name_str
);
let embed = CreateEmbed::new()
.title("Blood Invoice")
.description(description)
.fields(vec![
("Amount", amount.to_string().clone(), false),
("Expires in", "40 seconds".to_string(), false),
("Invoice: ", data.clone(), false),
]);
let attachment = CreateAttachment::bytes(qr_invoice.as_slice(), "qr.png");
let builder = CreateMessage::new().embed(embed).add_file(attachment);
let invoice_message =
ctx.channel_id().send_message(&ctx.http(), builder).await;
match invoice_message {
Ok(_) => {
println!("{:?}: invoice sent...", name_str);
}
Err(e) => {
println!("{:?}: Failed to send invoice: {}", name_str, e);
}
};
}
Err(e) => {
let reply = ctx
.channel_id()
.say(
ctx.http(),
format!("Failed to serialize request data: {}", e),
)
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
}
}
loop {
sleep(Duration::from_millis(1000));
match zebedee_client.get_charge(request_data.id.clone()).await {
Ok(charge) => {
if let Some(data) = charge.data {
match data.status.as_str() {
"completed" => {
println!(
"{:?}: payment completed...unmuting player...",
name_str
);
let unmute_player =
unmute_player(name.clone(), ctx, api_client).await;
if let Err(e) = unmute_player {
println!("unmuting player error: {}", e);
}
break;
}
"expired" => {
let reply = ctx
.channel_id()
.say(ctx.http(), "payment expired")
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
println!("{:?}: payment expired.", name_str);
break;
}
"error" => {
let reply =
ctx.channel_id().say(ctx.http(), "payment error").await;
if let Err(e) = reply {
println!("error: {}", e);
}
break;
}
_ => {
println!("{:?}: Waiting for payment...", name_str);
}
}
}
}
Err(_) => {
println!("error...");
break;
}
}
}
} else {
println!("invoice data error...");
}
}
Err(e) => {
let reply = ctx
.channel_id()
.say(ctx.http(), format!("Failed to create charge: {}", e))
.await;
if let Err(e) = reply {
println!("error: {}", e);
}
}
}
Ok(())
}

63
src/main.rs Normal file
View File

@ -0,0 +1,63 @@
use anyhow::Context as _;
use poise::serenity_prelude::{ClientBuilder, GatewayIntents};
use shuttle_runtime::SecretStore;
use shuttle_serenity::ShuttleSerenity;
use zebedee_rust::ZebedeeClient;
mod commands;
use commands::*;
mod battlemetrics;
pub struct Data {
zbd: ZebedeeClient,
api_client: reqwest::Client,
bm_token: String,
server_id: String,
}
#[shuttle_runtime::main]
async fn poise(#[shuttle_runtime::Secrets] secret_store: SecretStore) -> ShuttleSerenity {
let zbd_token = secret_store
.get("ZBD_TOKEN")
.context("'ZBD_TOKEN' was not found")?;
let zebedee_client = ZebedeeClient::new(zbd_token);
let api_client = reqwest::Client::new();
let discord_token = secret_store
.get("DISCORD_TOKEN")
.context("'DISCORD_TOKEN' was not found")?;
let server_id = secret_store
.get("SERVER_ID")
.context("'SERVER_ID' was not found")?;
let bm_token = secret_store
.get("BM_TOKEN")
.context("'BM_TOKEN' was not found")?;
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![giveblood(), unmute()],
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {
zbd: zebedee_client,
api_client,
bm_token,
server_id,
})
})
})
.build();
let client = ClientBuilder::new(discord_token, GatewayIntents::non_privileged())
.framework(framework)
.await
.map_err(shuttle_runtime::CustomError::new)?;
Ok(client.into())
}