Added random commands and story command
+ Clippy + Fmt
This commit is contained in:
parent
10b471f25a
commit
35321fc3c2
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/target
|
||||
/.idea
|
||||
config.toml
|
||||
stories/
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -438,6 +438,7 @@ dependencies = [
|
||||
"config",
|
||||
"ndm",
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serenity",
|
||||
|
||||
@ -14,6 +14,7 @@ toml = "0.5.9"
|
||||
rand = "0.8.5"
|
||||
tera = "1.17.1"
|
||||
ndm = "0.9.9"
|
||||
regex = "1.7.0"
|
||||
|
||||
[dependencies.serenity]
|
||||
version = "0.11.5"
|
||||
|
||||
135914
src/assets/bad_apple.txt
Normal file
135914
src/assets/bad_apple.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use structopt::StructOpt;
|
||||
use tera::Tera;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "fren", about = "Friend Bot")]
|
||||
@ -23,14 +24,23 @@ pub struct AlbumConfig {
|
||||
pub album_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct RandomConfig {
|
||||
pub name: String,
|
||||
pub responses: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct BotConfig {
|
||||
pub bot_token: String,
|
||||
pub imgur_client_id: String,
|
||||
pub fortunes: Vec<String>,
|
||||
pub story_path: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
pub albums: Vec<AlbumConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub randoms: Vec<RandomConfig>,
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
@ -51,23 +61,29 @@ impl BotConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct BotState {
|
||||
pub accepted_nsfw: Option<UserId>,
|
||||
pub fortune_templates: Tera,
|
||||
pub templates: HashMap<String, Tera>,
|
||||
pub albums: HashMap<String, Vec<Image>>,
|
||||
pub bad_apple_running: bool,
|
||||
pub story_in_progress: Mutex<()>,
|
||||
}
|
||||
|
||||
impl BotState {
|
||||
pub async fn new(cfg: &BotConfig) -> Result<Self, Error> {
|
||||
let mut fortune_templates = Tera::default();
|
||||
let mut albums: HashMap<String, Vec<Image>> = HashMap::new();
|
||||
let mut templates = HashMap::new();
|
||||
|
||||
for (idx, fortune) in cfg.fortunes.iter().enumerate() {
|
||||
fortune_templates
|
||||
.add_raw_template(&idx.to_string(), fortune)
|
||||
.unwrap();
|
||||
for random_config in cfg.randoms.iter() {
|
||||
let mut templates_set = Tera::default();
|
||||
for (idx, response) in random_config.responses.iter().enumerate() {
|
||||
templates_set
|
||||
.add_raw_template(&idx.to_string(), response)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
templates.insert(random_config.name.clone(), templates_set);
|
||||
}
|
||||
|
||||
for album in &cfg.albums {
|
||||
@ -79,9 +95,10 @@ impl BotState {
|
||||
|
||||
Ok(Self {
|
||||
accepted_nsfw: None,
|
||||
fortune_templates,
|
||||
templates,
|
||||
albums,
|
||||
bad_apple_running: false,
|
||||
story_in_progress: Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
@ -146,3 +163,30 @@ impl GlobalData {
|
||||
impl TypeMapKey for GlobalData {
|
||||
type Value = GlobalData;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StoryRecv {
|
||||
pub recv: tokio::sync::mpsc::Receiver<String>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for StoryRecv {
|
||||
type Value = Mutex<StoryRecv>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StorySend {
|
||||
pub send: tokio::sync::mpsc::Sender<String>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for StorySend {
|
||||
type Value = Mutex<StorySend>;
|
||||
}
|
||||
|
||||
pub fn create_story_channel() -> (Mutex<StorySend>, Mutex<StoryRecv>) {
|
||||
let (send, recv) = tokio::sync::mpsc::channel::<String>(10);
|
||||
|
||||
(
|
||||
Mutex::new(StorySend { send }),
|
||||
Mutex::new(StoryRecv { recv }),
|
||||
)
|
||||
}
|
||||
|
||||
@ -127,18 +127,15 @@ pub async fn parse_album(
|
||||
msg: &Message,
|
||||
album_name: &str,
|
||||
tags: Vec<&str>,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<bool, Error> {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
||||
match global_data.bot_state.get_image(album_name, tags) {
|
||||
Ok(match global_data.bot_state.get_image(album_name, tags) {
|
||||
Some(image) => {
|
||||
msg.reply(&ctx.http, &image.link).await?;
|
||||
true
|
||||
}
|
||||
None => {
|
||||
msg.reply(&ctx.http, "No image ;(").await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
None => false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[group]
|
||||
#[commands(dad_joke, fortune, roll, bad_apple)]
|
||||
#[commands(dad_joke, roll, bad_apple)]
|
||||
pub struct Joke;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@ -43,12 +43,12 @@ async fn dad_joke(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FortuneCtx {
|
||||
user: serenity::model::guild::Member,
|
||||
struct RandomCtx {
|
||||
user: String,
|
||||
random_image: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl FortuneCtx {
|
||||
impl RandomCtx {
|
||||
pub async fn new(
|
||||
user: serenity::model::guild::Member,
|
||||
global_data: &GlobalData,
|
||||
@ -63,25 +63,29 @@ impl FortuneCtx {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { user, random_image })
|
||||
Ok(Self {
|
||||
user: (*user.display_name()).clone(),
|
||||
random_image,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[aliases("8ball")]
|
||||
#[description("Use as your own risk")]
|
||||
async fn fortune(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
pub async fn random(ctx: &Context, msg: &Message, random_name: &str) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
||||
let fortune_template = {
|
||||
let templates = global_data.bot_state.templates.get(random_name);
|
||||
|
||||
let templates = if let Some(templates) = templates {
|
||||
templates
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let template = {
|
||||
let mut rng = thread_rng();
|
||||
global_data
|
||||
.bot_state
|
||||
.fortune_templates
|
||||
.get_template_names()
|
||||
.choose(&mut rng)
|
||||
|
||||
templates.get_template_names().choose(&mut rng)
|
||||
};
|
||||
|
||||
let guild_member = msg
|
||||
@ -90,15 +94,12 @@ async fn fortune(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
.member(&ctx.http, msg.author.id)
|
||||
.await?;
|
||||
|
||||
let fortune_ctx = FortuneCtx::new(guild_member, global_data).await?;
|
||||
let random_ctx = RandomCtx::new(guild_member, global_data).await?;
|
||||
|
||||
let reply = if let Some(fortune_template) = fortune_template {
|
||||
global_data.bot_state.fortune_templates.render(
|
||||
fortune_template,
|
||||
&tera::Context::from_serialize(&fortune_ctx)?,
|
||||
)?
|
||||
let reply = if let Some(template) = template {
|
||||
templates.render(template, &tera::Context::from_serialize(&random_ctx)?)?
|
||||
} else {
|
||||
"Sorry kid, all out of fortunes.".to_string()
|
||||
"Sorry kid, all out of messages.".to_string()
|
||||
};
|
||||
|
||||
msg.reply(&ctx.http, reply).await?;
|
||||
|
||||
@ -3,7 +3,9 @@ pub mod album;
|
||||
pub mod celeryman;
|
||||
pub mod color;
|
||||
pub mod joke;
|
||||
pub mod story;
|
||||
|
||||
use crate::discord::joke::random;
|
||||
use crate::{help, hook, GlobalData};
|
||||
use serenity::async_trait;
|
||||
use serenity::client::Context;
|
||||
@ -100,8 +102,19 @@ pub async fn unrecognised_command_hook(
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Err(e) = album::parse_album(ctx, msg, unrecognised_command_name, tags).await {
|
||||
println!("Error processing album command: {}", e)
|
||||
let parsed_album = match album::parse_album(ctx, msg, unrecognised_command_name, tags).await {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => {
|
||||
println!("Error processing album command: {}", e);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if !parsed_album {
|
||||
match random(ctx, msg, unrecognised_command_name).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => println!("Error processing random command: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
216
src/discord/story.rs
Normal file
216
src/discord/story.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use crate::config::{BotConfig, StoryRecv, StorySend};
|
||||
use crate::{command, group, GlobalData};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use regex::Regex;
|
||||
use serenity::client::Context;
|
||||
use serenity::framework::standard::{Args, CommandResult};
|
||||
use serenity::model::channel::{Attachment, Message};
|
||||
use serenity::utils::MessageBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[group]
|
||||
#[commands(story, word, list_stories, upload_story)]
|
||||
pub struct Story;
|
||||
|
||||
async fn get_all_stories(cfg: &BotConfig) -> Vec<PathBuf> {
|
||||
let mut dir = tokio::fs::read_dir(&cfg.story_path).await.unwrap();
|
||||
|
||||
let mut stories = Vec::new();
|
||||
|
||||
while let Some(file) = &dir.next_entry().await.unwrap() {
|
||||
stories.push(file.path());
|
||||
}
|
||||
|
||||
stories
|
||||
}
|
||||
|
||||
fn get_all_blanks(story: &str) -> Vec<String> {
|
||||
let re = Regex::new(r"\{%(?P<global>.*?)%}").unwrap();
|
||||
let mut globals: Vec<String> = Vec::new();
|
||||
|
||||
for cap in re.captures_iter(story) {
|
||||
if let Some(global) = cap.name("global") {
|
||||
if !globals.contains(&global.as_str().to_string()) {
|
||||
globals.push(global.as_str().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globals
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description("List Stories")]
|
||||
async fn list_stories(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
||||
let stories = get_all_stories(&global_data.cfg).await;
|
||||
|
||||
let mut resp = MessageBuilder::default();
|
||||
|
||||
resp.push_line("**Stories I know:**");
|
||||
|
||||
for story in stories {
|
||||
resp.push_line(
|
||||
story
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('_', " "),
|
||||
);
|
||||
}
|
||||
|
||||
msg.reply(&ctx.http, resp.build()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description("Let me tell you a tail")]
|
||||
async fn story(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
||||
let _story_lock = match global_data.bot_state.story_in_progress.try_lock() {
|
||||
Ok(lock) => lock,
|
||||
Err(_) => {
|
||||
msg.reply(&ctx.http, "Let me finish telling this story bub.")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let story_recv = data.get::<StoryRecv>().unwrap();
|
||||
|
||||
let mut story_recv = story_recv.lock().await;
|
||||
|
||||
let stories = get_all_stories(&global_data.cfg).await;
|
||||
|
||||
let story_path = if args.is_empty() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
stories.choose(&mut rng)
|
||||
} else {
|
||||
let story_name = args.rest().to_lowercase().replace(' ', "_");
|
||||
|
||||
stories.iter().find(|story_p| {
|
||||
let story_p_name = story_p.file_stem().unwrap().to_str().unwrap();
|
||||
story_p_name == story_name
|
||||
})
|
||||
};
|
||||
|
||||
let story_path = match story_path {
|
||||
None => {
|
||||
msg.reply(&ctx.http, "No story found :(").await?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(story) => story,
|
||||
};
|
||||
|
||||
let mut story_contents = tokio::fs::read_to_string(story_path).await?;
|
||||
|
||||
let mut story_globals: HashMap<String, String> = HashMap::new();
|
||||
|
||||
let msg_channel = msg.channel(&ctx.http).await?;
|
||||
let guild = msg.guild(&ctx.cache).unwrap();
|
||||
|
||||
let msg_channel = guild
|
||||
.channels(&ctx.http)
|
||||
.await?
|
||||
.get(&msg_channel.id())
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
for global in get_all_blanks(&story_contents) {
|
||||
msg_channel
|
||||
.say(&ctx.http, format!("Give me {}", global))
|
||||
.await?;
|
||||
|
||||
story_globals.insert(global, story_recv.recv.recv().await.unwrap());
|
||||
}
|
||||
|
||||
for (prompt, response) in story_globals {
|
||||
story_contents = story_contents.replace(&format!("{{%{}%}}", prompt), &response)
|
||||
}
|
||||
|
||||
let mut msg_builder = MessageBuilder::default();
|
||||
for part in story_contents.split('\n') {
|
||||
if (msg_builder.0.len() + part.len()) > serenity::constants::MESSAGE_CODE_LIMIT {
|
||||
msg_channel.say(&ctx.http, msg_builder.build()).await?;
|
||||
msg_builder = MessageBuilder::default();
|
||||
}
|
||||
|
||||
msg_builder.push_line(part);
|
||||
}
|
||||
|
||||
if !msg_builder.0.is_empty() {
|
||||
msg_channel.say(&ctx.http, msg_builder.build()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description("Give me a word")]
|
||||
async fn word(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global = data.get::<GlobalData>().unwrap();
|
||||
|
||||
if global.bot_state.story_in_progress.try_lock().is_ok() {
|
||||
msg.reply(&ctx.http, "No story in progress!").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let story_send = data.get::<StorySend>().unwrap();
|
||||
|
||||
let story_send = story_send.lock().await;
|
||||
|
||||
let resp = MessageBuilder::default().push_safe(args.rest()).build();
|
||||
|
||||
story_send.send.send(resp).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[description("Attach stories as .txt files")]
|
||||
async fn upload_story(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global = data.get::<GlobalData>().unwrap();
|
||||
|
||||
let stories: Vec<&Attachment> = msg
|
||||
.attachments
|
||||
.iter()
|
||||
.filter(|f| f.filename.ends_with(".txt"))
|
||||
.collect();
|
||||
|
||||
for story in stories {
|
||||
let story_txt = String::from_utf8(story.download().await?)?;
|
||||
|
||||
let globals = get_all_blanks(&story_txt);
|
||||
|
||||
let mut resp = MessageBuilder::new();
|
||||
|
||||
resp.push_line(format!("{} contains the following blanks:", story.filename));
|
||||
|
||||
for blank in globals {
|
||||
resp.push_line(format!("* {}", blank));
|
||||
}
|
||||
|
||||
msg.reply(&ctx.http, resp.build()).await?;
|
||||
|
||||
let story_path = global.cfg.story_path.join(&story.filename);
|
||||
|
||||
tokio::fs::write(&story_path, story_txt).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -3,7 +3,7 @@ mod discord;
|
||||
mod error;
|
||||
mod imgur;
|
||||
|
||||
use crate::config::{Args, BotConfig, GlobalData};
|
||||
use crate::config::{create_story_channel, Args, BotConfig, GlobalData, StoryRecv, StorySend};
|
||||
use crate::discord::unrecognised_command_hook;
|
||||
use serenity::framework::standard::macros::{command, group, help, hook};
|
||||
use serenity::framework::standard::StandardFramework;
|
||||
@ -39,16 +39,21 @@ async fn main() {
|
||||
.group(&discord::celeryman::CELERYMAN_GROUP)
|
||||
.group(&discord::joke::JOKE_GROUP)
|
||||
.group(&discord::admin::ADMIN_GROUP)
|
||||
.group(&discord::story::STORY_GROUP)
|
||||
.unrecognised_command(unrecognised_command_hook)
|
||||
.bucket("bad_apple", |b| b.delay(60*10))
|
||||
.bucket("bad_apple", |b| b.delay(60 * 10))
|
||||
.await
|
||||
.help(&discord::MY_HELP)
|
||||
.after(discord::after);
|
||||
|
||||
let (send, recv) = create_story_channel();
|
||||
|
||||
let intents = GatewayIntents::all();
|
||||
let mut client = Client::builder(&global_data.cfg.bot_token, intents)
|
||||
.framework(framework)
|
||||
.type_map_insert::<GlobalData>(global_data)
|
||||
.type_map_insert::<StorySend>(send)
|
||||
.type_map_insert::<StoryRecv>(recv)
|
||||
.event_handler(discord::Handler)
|
||||
.await
|
||||
.expect("Unable to create client.");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user