Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

14 changed files with 119 additions and 368 deletions

1
.gitignore vendored
View file

@ -14,4 +14,3 @@ Cargo.lock
/target
/.idea/
/quotes.txt

17
Cargo.lock generated
View file

@ -451,9 +451,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.18"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
"bytes",
"fnv",
@ -800,9 +800,9 @@ checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "openssl"
version = "0.10.55"
version = "0.10.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
dependencies = [
"bitflags",
"cfg-if",
@ -832,10 +832,11 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.90"
version = "0.9.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
@ -1404,9 +1405,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.24.2"
version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb"
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
dependencies = [
"autocfg",
"bytes",

View file

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
poise = "0.5.2"
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
rand = "0.8.5"
chrono = "0.4.23"
md5 = "0.7.0"

View file

@ -1,6 +1,6 @@
# echbot
A helper bot for Smite fun.
A trash bot for Smite fun.
## Environment
@ -13,3 +13,8 @@ environment:
- DEV_ID=your_hi-rez_dev_id
- AUTH_KEY=your_hi-rez_auth_key
```
## Quotes
A `quotes.txt` file must be present in the root directory for the ping command to function. The format is one quote per
line.

View file

@ -1,118 +0,0 @@
use md5::Digest;
use rand::seq::SliceRandom;
use reqwest::{Error, Response};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Session {
ret_msg: String,
session_id: String,
timestamp: String,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct God {
#[serde(rename = "Name")]
name: String,
ret_msg: Option<String>,
}
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct Profile {
#[serde(rename = "Name")]
pub name: Option<String>,
#[serde(rename = "Personal_Status_Message")]
pub personal_status_message: Option<String>,
pub hz_player_name: Option<String>,
#[serde(rename = "HoursPlayed")]
pub hours_played: i32,
#[serde(rename = "Losses")]
pub losses: i32,
#[serde(rename = "Wins")]
pub wins: i32,
#[serde(rename = "Team_Name")]
pub clan: Option<String>,
#[serde(rename = "Level")]
pub level: i32,
#[serde(rename = "Platform")]
pub platform: Option<String>,
#[serde(rename = "Leaves")]
pub leaves: i32,
pub ret_msg: Option<String>,
}
async fn get_utc_timestamp() -> String {
chrono::Utc::now().format("%Y%m%d%H%M%S").to_string()
}
async fn get_signature(
dev_id: &String,
method: &str,
auth_key: &String,
timestamp: &String,
) -> String {
let hash: Digest = md5::compute(format!("{dev_id}{method}{auth_key}{timestamp}"));
format!("{:x}", hash)
}
async fn create_session() -> Result<Session, Error> {
let dev_id: String = std::env::var("DEV_ID").expect("Missing DEV_ID");
let auth_key: String = std::env::var("AUTH_KEY").expect("Missing AUTH_KEY");
let timestamp: String = get_utc_timestamp().await;
let signature: String = get_signature(&dev_id, "createsession", &auth_key, &timestamp).await;
let request: String = format!(
"https://api.smitegame.com/smiteapi.svc/createsessionJson/{dev_id}/{signature}/{timestamp}"
);
let response: Response = reqwest::get(&request).await?;
let session: Session = response.json().await?;
Ok(session)
}
async fn get_gods() -> Result<Vec<God>, Error> {
let dev_id: String = std::env::var("DEV_ID").expect("Missing DEV_ID");
let auth_key: String = std::env::var("AUTH_KEY").expect("Missing AUTH_KEY");
let session_id: String = create_session().await?.session_id;
let timestamp: String = get_utc_timestamp().await;
let signature: String = get_signature(&dev_id, "getgods", &auth_key, &timestamp).await;
let request: String = format!(
"https://api.smitegame.com/smiteapi.svc/getgodsJson/{dev_id}/{signature}/{session_id}/{timestamp}/1"
);
let response: Response = reqwest::get(&request).await?;
let gods: Vec<God> = response.json().await?;
Ok(gods)
}
pub async fn get_random_god() -> Result<String, Error> {
let gods: Vec<God> = get_gods().await?;
let god: &God = gods.choose(&mut rand::thread_rng()).unwrap();
let name: String = god.name.clone();
Ok(name)
}
pub async fn get_player(player: String) -> Result<Vec<Profile>, Error> {
let dev_id: String = std::env::var("DEV_ID").expect("Missing DEV_ID");
let auth_key: String = std::env::var("AUTH_KEY").expect("Missing AUTH_KEY");
let session_id: String = create_session().await?.session_id;
let timestamp: String = get_utc_timestamp().await;
let signature: String = get_signature(&dev_id, "getplayer", &auth_key, &timestamp).await;
let request: String = format!(
"https://api.smitegame.com/smiteapi.svc/getplayerJson/{dev_id}/{signature}/{session_id}/{timestamp}/{player}"
);
let response: Response = reqwest::get(&request).await?;
let profiles: Vec<Profile> = response.json().await?;
Ok(profiles)
}

View file

@ -1,8 +1,3 @@
// Group all commands for registration
mod api;
pub mod ping;
pub mod pog;
pub mod profile;
pub mod random;
pub mod register;
pub mod team;
pub(crate) mod slur;
pub(crate) mod team;

View file

@ -1,8 +0,0 @@
use crate::{Context, Error};
/// Replies with a pong
#[poise::command(slash_command, prefix_command)]
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
ctx.say("Pong! 🏓").await?;
Ok(())
}

View file

@ -1,35 +0,0 @@
use crate::serenity;
use crate::{Context, Error};
#[poise::command(slash_command, subcommands("up", "down"))]
pub async fn pog(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Increases the pogs
#[poise::command(slash_command)]
pub async fn up(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(|f| {
f.embed(|f| {
f.title("Pog Status")
.description("The pog level has been increased.")
.color(serenity::Colour::DARK_GREEN)
})
})
.await?;
Ok(())
}
/// Decreases the pogs
#[poise::command(slash_command)]
pub async fn down(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(|f| {
f.embed(|f| {
f.title("Pog Status")
.description("The pog level has been decreased.")
.color(serenity::Colour::RED)
})
})
.await?;
Ok(())
}

View file

@ -1,59 +0,0 @@
use crate::commands::api::get_player;
use crate::serenity;
use crate::{Context, Error};
/// Looks up a player's profile
#[poise::command(slash_command)]
pub async fn profile(
ctx: Context<'_>,
#[rename = "player"] player_name: String,
) -> Result<(), Error> {
let profiles = get_player(player_name).await?;
let profile = profiles.first().unwrap();
if profile.name.is_none() {
ctx.send(|f| {
f.embed(|f| {
f.title("Hidden")
.description("This profile is hidden.")
.color(serenity::Colour::RED)
})
})
.await?;
return Ok(());
}
let winrate = (profile.wins as f32 / (profile.wins as f32 + profile.losses as f32)) * 100f32;
ctx.send(|f| {
f.embed(|f| {
f.title(format!("{}", profile.name.as_ref().unwrap()))
.description(format!(
"{}'s statistics.",
profile.hz_player_name.as_ref().unwrap()
))
.field(
"Clan Name",
format!("{}", profile.clan.as_ref().unwrap_or(&String::from(""))),
true,
)
.field(
"Status Message",
format!("{}", profile.personal_status_message.as_ref().unwrap()),
false,
)
.field("Level", format!("{}", profile.level), true)
.field("Hours Played", format!("{}", profile.hours_played), true)
.field("Leaves", format!("{}", profile.leaves), true)
.field(
"Platform",
format!("{}", profile.platform.as_ref().unwrap()),
false,
)
.field("Wins", format!("{}", profile.wins), true)
.field("Losses", format!("{}", profile.losses), true)
.field("Winrate", format!("{:.2}%", winrate), true)
.color(serenity::Colour::BLURPLE)
})
})
.await?;
Ok(())
}

View file

@ -1,24 +0,0 @@
use crate::commands::api::get_random_god;
use crate::serenity;
use crate::{Context, Error};
/// Picks a random something
#[poise::command(slash_command, subcommands("god"))]
pub async fn random(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Picks a random god
#[poise::command(slash_command)]
pub async fn god(ctx: Context<'_>) -> Result<(), Error> {
let god: String = get_random_god().await?;
ctx.send(|f| {
f.embed(|f| {
f.title("Random God")
.description(format!("Your random god is **{god}**!"))
.color(serenity::Colour::BLUE)
})
})
.await?;
Ok(())
}

View file

@ -1,7 +0,0 @@
use crate::{Context, Error};
#[poise::command(prefix_command, owners_only)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}

29
src/commands/slur.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{Context, Error};
use crate::serenity;
use rand::seq::IteratorRandom;
use std::{
fs::File,
io::{BufRead, BufReader},
};
/// Basically a ping command
#[poise::command(slash_command)]
pub(crate) async fn slur(
ctx: Context<'_>,
) -> Result<(), Error> {
let file = File::open("quotes.txt")
.unwrap_or_else(|_e| panic!("Quote file missing.")); // Open the quotes file
let file = BufReader::new(file); // Read the quotes file
let quotes = file.lines()
.map(|res| res.expect("Failed to read line."));
let quote = quotes.choose(&mut rand::thread_rng())
.expect("No lines in file."); // Pick a random quote
ctx.send(|f| f
.embed(|f| f
.title("DMBrandon Sez:")
.description(format!("\"{}\"", quote))
.color(serenity::Colour::GOLD)
)).await?; // Send embed with team picks
Ok(())
}

View file

@ -1,120 +1,91 @@
use std::collections::HashMap;
use std::string::String;
use crate::{Context, Error};
use crate::serenity;
use poise::serenity_prelude::{Guild, Member, UserId, VoiceState};
use rand::seq::SliceRandom;
use std::string::String;
use crate::serenity;
use crate::{Context, Error};
/// Return a string of pingable IDs from a slice of string UserIds
fn team_to_ping(team: &[&String]) -> String {
team.iter()
.map(|o| format!("<@{o}>"))
.collect::<Vec<String>>()
.join(", ")
/// Return a string of pingable IDs from a slice of UserIds
fn team_to_ping(team: &[&&UserId]) -> String {
return team.iter().map(|o| format!("<@{}>", o.to_string())).collect::<Vec<String>>().join(", ");
}
/// Splits up players for custom matches
#[poise::command(slash_command)]
pub async fn team(
pub(crate) async fn team(
ctx: Context<'_>,
#[description = "Your order voice channel"]
#[rename = "order"]
#[channel_types("Voice")]
order_channel: serenity::Channel, // Channel to pick all members from
#[channel_types("Voice")] order_channel: serenity::Channel, // Channel to pick all members from
#[description = "Your chaos voice channel"]
#[rename = "chaos"]
#[channel_types("Voice")]
chaos_channel: serenity::Channel, // Channel to move chaos team members into
#[channel_types("Voice")] chaos_channel: serenity::Channel, // Channel to move chaos team members into
#[description = "Team size"]
#[min = 1]
size: u8, // Number of members on each team
#[min = 1] size: u8, // Number of members on each team
) -> Result<(), Error> {
let mut voice_states: HashMap<UserId, VoiceState> = ctx.guild().unwrap().voice_states; // Get hashmap of users' voice states within the guild
voice_states.retain(|_, state: &mut VoiceState| state.channel_id == Some(order_channel.id())); // Drop users not active in requested voice channel from hashmap
if voice_states.keys().len() < size as usize * 2 {
// Make sure there are enough members in the voice channel
ctx.send(|f| {
f.embed(|f| {
f.title(format!("Custom {size}v{size} Teams"))
.description("There are not enough members in the call!")
if voice_states.keys().len() < size as usize * 2 { // Make sure there are enough members in the voice channel
ctx.send(|f| f
.embed(|f| f
.title(format!("Custom {}v{} Teams", size, size))
.description("You don't have enough friends for that, idiot.")
.color(serenity::Colour::RED)
})
})
.await?;
)).await?; // Insult the user for not having enough friends
return Ok(()); // Break out early if there are not enough members
}
let uuid_team: u64 = ctx.id(); // Grab context ID for action row
let users: Vec<String> = Vec::from_iter(voice_states.keys().map(|u| u.to_string())); // Get vec of PIDs
let players: Vec<&String> = users
.choose_multiple(&mut rand::thread_rng(), size as usize * 2)
.collect(); // Pick players randomly into slice
let users: Vec<&UserId> = Vec::from_iter(voice_states.keys()); // Get vec of PIDs
let players: Vec<&&UserId> = users.choose_multiple(
&mut rand::thread_rng(), size as usize * 2).collect(); // Pick players randomly into slice
let (order, chaos) = players.split_at(players.len() / 2); // Split slice into two teams
ctx.send(|f| {
f.embed(|f| {
f.title(format!("Custom {size}v{size} Teams"))
.description("Click the button below to move the Chaos players.")
ctx.send(|f| f
.embed(|f| f
.title(format!("Custom {}v{} Teams", size, size))
.description("VER")
.field("Order", team_to_ping(order), false)
.field("Chaos", team_to_ping(chaos), false)
.color(serenity::Colour::DARK_GREEN)
})
.components(|c| {
c.create_action_row(|a| {
// Create an action row with button
a.create_button(
|b| {
b.style(serenity::ButtonStyle::Primary)
).components(|c| c // Create an action row with button
.create_action_row(|a| a
.create_button(|b| b
.style(serenity::ButtonStyle::Primary)
.label("Swap Channels")
.custom_id(uuid_team)
}, // Use the context ID as button ID
.custom_id(uuid_team) // Use the context ID as button ID
)
})
})
})
.await?; // Send embed with team picks
)
)).await?; // Send embed with team picks
while let Some(mci) = serenity::CollectComponentInteraction::new(ctx) // Handle the interaction
.await
{
let guild: Guild = ctx.guild().unwrap(); // Grab guild from context
.await {
let guild: Guild = ctx.guild().unwrap();
for user in chaos {
let member: Member = guild.member(ctx, UserId(user.parse()?)).await?; // Get the member in the correct guild
member
.move_to_voice_channel(ctx, chaos_channel.id())
.await?; // Move the member to the correct voice channel
let member: Member = guild.member(ctx, UserId(*user.as_u64())).await?; // Get the member in the correct guild
member.move_to_voice_channel(ctx, chaos_channel.id()).await?; // Move the member to the correct voice channel
}
mci.create_interaction_response(ctx, |ir| {
// Edit embed
ir.kind(serenity::InteractionResponseType::UpdateMessage)
.interaction_response_data(|f| {
f.embed(|f| {
f.title(format!("Custom {size}v{size} Teams"))
.description("Good luck! Have fun!")
mci.create_interaction_response(ctx, |ir| { // Update embed
ir.kind(serenity::InteractionResponseType::UpdateMessage).interaction_response_data(|f| f
.embed(|f| f
.title(format!("Custom {}v{} Teams", size, size))
.description("VVGO VVW VVX")
.field("Order", team_to_ping(order), false)
.field("Chaos", team_to_ping(chaos), false)
.color(serenity::Colour::DARK_GREEN)
})
.components(|c| {
c.create_action_row(|a| {
// Create an action row with button
a.create_button(
|b| {
b
).components(|c| c // Create an action row with button
.create_action_row(|a| a
.create_button(|b| b
.disabled(true) // with disabled button
.style(serenity::ButtonStyle::Primary)
.label("Quit Sibelius") // and new text
}, // Use the context ID as button ID
.custom_id(uuid_team) // Use the context ID as button ID
)
})
})
})
})
.await?;
)
))
}).await?;
}
Ok(())
}

View file

@ -1,8 +1,8 @@
use poise::serenity_prelude as serenity;
mod commands;
pub struct Data {}
use poise::serenity_prelude as serenity;
struct Data {}
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
@ -12,23 +12,25 @@ async fn main() {
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![
commands::ping::ping(),
commands::slur::slur(),
commands::team::team(),
commands::random::random(),
commands::register::register(),
commands::profile::profile(),
commands::pog::pog(),
], // IntelliJ doesn't like this, but it's fine.
..Default::default()
})
.token(std::env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN"))
.token(std::env::var("DISCORD_TOKEN")
.expect("Missing DISCORD_TOKEN"))
.intents(serenity::GatewayIntents::non_privileged()) // Set intents for Discord dev portal
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?; // Update slash commands
ctx.set_activity(serenity::Activity::playing("SMITE")).await;
poise::builtins::register_in_guild(
ctx,
&framework.options().commands,
serenity::GuildId(std::env::var("GUILD_ID")
.expect("Missing GUILD_ID") // Get GID from env and parse
.parse::<u64>().unwrap())).await?; // Update slash commands in GID
Ok(Data {})
})
});
framework.run().await.unwrap();
}