From 805f9cb49d0b2842ad04ae8bfdd105d207889906 Mon Sep 17 00:00:00 2001 From: Stephen <stephen@stephendownward.ca> Date: Sun, 22 Nov 2020 19:43:56 -0400 Subject: [PATCH] Basic saving/loading --- .env | 1 + Cargo.lock | 75 ++++ Cargo.toml | 8 +- diesel.toml | 5 + migrations/.gitkeep | 0 .../down.sql | 6 + .../up.sql | 36 ++ .../down.sql | 1 + .../up.sql | 11 + src/framebuffer.rs | 43 +++ src/handler.rs | 291 ++++++++++++++++ src/main.rs | 320 ++---------------- src/models.rs | 18 + src/program.rs | 123 +++++++ src/schema.rs | 8 + 15 files changed, 643 insertions(+), 303 deletions(-) create mode 100644 .env create mode 100644 diesel.toml create mode 100644 migrations/.gitkeep create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2020-11-21-032604_create_user_programs/down.sql create mode 100644 migrations/2020-11-21-032604_create_user_programs/up.sql create mode 100644 src/framebuffer.rs create mode 100644 src/handler.rs create mode 100644 src/models.rs create mode 100644 src/program.rs create mode 100644 src/schema.rs diff --git a/.env b/.env new file mode 100644 index 0000000..f7487d1 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://stephen@localhost/cat_disruptor_6500 diff --git a/Cargo.lock b/Cargo.lock index aaa391a..81336a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bigdecimal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -94,6 +105,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" name = "cat-disruptor-6500" version = "0.1.0" dependencies = [ + "bigdecimal", + "diesel", + "dotenv", "phf", "png", "serde", @@ -160,6 +174,33 @@ dependencies = [ "byteorder", ] +[[package]] +name = "diesel" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c" +dependencies = [ + "bigdecimal", + "bitflags", + "byteorder", + "diesel_derives", + "num-bigint", + "num-integer", + "num-traits", + "pq-sys", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.48", +] + [[package]] name = "digest" version = "0.9.0" @@ -169,6 +210,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dtoa" version = "0.4.6" @@ -643,6 +690,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-derive" version = "0.2.5" @@ -815,6 +873,15 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" +[[package]] +name = "pq-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" +dependencies = [ + "vcpkg", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1384,6 +1451,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + [[package]] name = "version_check" version = "0.9.2" @@ -1574,6 +1647,8 @@ dependencies = [ [[package]] name = "xbasic" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1521d9f71cc07a276b915ea6b670b06178cf84caa82636a9f5f1c4a505d1fe90" dependencies = [ "num-derive", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index a49a643..e735bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ tokio = {version = "0.2", features = ["full", "time"] } phf = { version = "0.8", features = ["macros"] } toml = "0.5" serde = { version = "1.0", features = ["derive"] } -# xbasic = "0.2" -xbasic = { path = "../xbasic", version = "0.2.0" } -png = "0.16" \ No newline at end of file +xbasic = "0.2" +png = "0.16" +diesel = { version = "1.4", features = ["postgres", "numeric"] } +dotenv = "0.15.0" +bigdecimal = "0.1.2" \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2020-11-21-032604_create_user_programs/down.sql b/migrations/2020-11-21-032604_create_user_programs/down.sql new file mode 100644 index 0000000..7bc4037 --- /dev/null +++ b/migrations/2020-11-21-032604_create_user_programs/down.sql @@ -0,0 +1 @@ +DROP TABLE user_programs \ No newline at end of file diff --git a/migrations/2020-11-21-032604_create_user_programs/up.sql b/migrations/2020-11-21-032604_create_user_programs/up.sql new file mode 100644 index 0000000..8810af3 --- /dev/null +++ b/migrations/2020-11-21-032604_create_user_programs/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE user_programs ( +id SERIAL PRIMARY KEY, +discord_user_id NUMERIC NOT NULL, +name VARCHAR NOT NULL, +code TEXT NOT NULL +); + +CREATE UNIQUE INDEX user_programs_name_id_index ON user_programs ( +discord_user_id, +name +); \ No newline at end of file diff --git a/src/framebuffer.rs b/src/framebuffer.rs new file mode 100644 index 0000000..034555b --- /dev/null +++ b/src/framebuffer.rs @@ -0,0 +1,43 @@ +#[derive(Clone)] +pub(crate) struct FrameBuffer { + width: u32, + height: u32, + buffer: Vec<u8>, +} + +impl FrameBuffer { + pub fn new(width: u32, height: u32) -> Self { + Self { + width, + height, + buffer: vec![0; width as usize * height as usize * 4], + } + } + + pub fn set_pixel(&mut self, x: u32, y: u32, red: u8, green: u8, blue: u8, alpha: u8) { + if x >= self.width || y >= self.height { + return; + } + + let x = x as usize; + let y = y as usize; + self.buffer[(x + self.width as usize * y) * 4] = red; + self.buffer[(x + self.width as usize * y) * 4 + 1] = green; + self.buffer[(x + self.width as usize * y) * 4 + 2] = blue; + self.buffer[(x + self.width as usize * y) * 4 + 3] = alpha; + } + + pub fn as_png_vec(&self) -> Vec<u8> { + let mut buffer: Vec<u8> = Vec::new(); + + { + let mut encoder = png::Encoder::new(&mut buffer, self.width, self.height); + encoder.set_color(png::ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + writer.write_image_data(&self.buffer).unwrap(); + } + + buffer + } +} diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..ffd662e --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,291 @@ +use crate::framebuffer::FrameBuffer; +use crate::program::Program; +use diesel::PgConnection; +use phf::phf_map; +use serenity::async_trait; +use serenity::http::AttachmentType; +use serenity::model::channel::{Message, ReactionType}; +use serenity::model::id::UserId; +use serenity::model::prelude::Ready; +use serenity::prelude::*; +use std::borrow::Cow; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use xbasic::basic_io::BasicIO; +use xbasic::expr::ExprValue; +use xbasic::xbasic::XBasicBuilder; + +static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! { +"cat" => "ðŸˆ", +"chicken" => "ðŸ”", +"spaghetti" => "ðŸ", +"dog" => "ðŸ•", +"bot" => "🤖", +"mango" => "ðŸ¥", +"banana" => "ðŸŒ", +"bee" => "ðŸ" +}; + +struct DiscordIO { + s: String, + frame: Option<FrameBuffer>, +} + +impl DiscordIO { + fn new() -> Self { + Self { + s: String::new(), + frame: None, + } + } +} + +impl BasicIO for DiscordIO { + fn read_line(&mut self) -> String { + unimplemented!() + } + + fn write_line(&mut self, line: String) { + self.s += &*(line + "\r\n"); + } +} + +pub(crate) struct Handler { + programs: Arc<Mutex<HashMap<UserId, Program>>>, + conn: Arc<Mutex<PgConnection>>, +} + +impl Handler { + pub fn new(conn: PgConnection) -> Self { + Self { + programs: Arc::new(Mutex::new(HashMap::new())), + conn: Arc::new(Mutex::new(conn)), + } + } +} + +#[async_trait] +impl EventHandler for Handler { + async fn message(&self, ctx: Context, msg: Message) { + for (key, value) in EMOJI_MAP.entries() { + let msg_lower = format!(" {} ", msg.content.to_lowercase()); + if msg_lower.contains(&format!(" {} ", key)) + || msg_lower.contains(&format!(" {}s ", key)) + { + let reaction_type = match ReactionType::from_str(value) { + Ok(x) => x, + Err(x) => { + println!("Could not react: {}", x); + return; + } + }; + if let Err(e) = msg.react(&ctx, reaction_type).await { + println!("Error reacting: {}", e); + } + } + } + + for line in msg.content.split('\n') { + if self.programs.lock().unwrap().contains_key(&msg.author.id) { + match line { + "!STOP" => { + self.programs + .lock() + .unwrap() + .remove(&msg.author.id) + .unwrap(); + } + "RUN" => { + let code = self.programs.lock().unwrap()[&msg.author.id].stringify(); + let io = DiscordIO::new(); + + let (output, fb, errors) = { + let mut xbb = XBasicBuilder::new(io); + xbb.compute_limit(1000000000); + + xbb.define_function("setframe".to_owned(), 2, |args, io| { + let w = args[0].clone().into_decimal() as u32; + let h = args[1].clone().into_decimal() as u32; + + io.frame = Some(FrameBuffer::new(w, h)); + + ExprValue::Decimal(0.0) + }) + .unwrap(); + + xbb.define_function("setpixel".to_owned(), 5, |args, io| { + let x = args[0].clone().into_decimal() as u32; + let y = args[1].clone().into_decimal() as u32; + let red = args[2].clone().into_decimal() as u8; + let green = args[3].clone().into_decimal() as u8; + let blue = args[4].clone().into_decimal() as u8; + + match &mut io.frame { + Some(fb) => { + fb.set_pixel(x, y, red, green, blue, 255); + } + None => {} + } + + ExprValue::Decimal(0.0) + }) + .unwrap(); + + let mut xb = xbb.build(); + + let _ = xb.run(&format!("{}\n", code)); + + let errors = if xb.error_handler.had_errors + || xb.error_handler.had_runtime_error + { + Some(xb.error_handler.errors.join("\n")) + } else { + None + }; + + (xb.get_io().s.clone(), xb.get_io().frame.clone(), errors) + }; + + if !output.is_empty() { + msg.channel_id.say(&ctx, output).await.unwrap(); + } + + if let Some(fb) = &fb { + let buf = fb.as_png_vec(); + + msg.channel_id + .send_message(&ctx, |e| { + e.add_file(AttachmentType::Bytes { + data: Cow::Borrowed(&buf), + filename: "output.png".to_string(), + }); + + e + }) + .await + .unwrap(); + } + + if let Some(e) = errors { + msg.channel_id.say(&ctx, e).await.unwrap(); + } + } + "LIST" => { + msg.channel_id + .say( + &ctx, + format!( + "```\n{}\n```", + self.programs.lock().unwrap()[&msg.author.id] + .stringy_line_nums() + ), + ) + .await + .unwrap(); + } + _ => { + if let Some(name) = line.strip_prefix("SAVE ") { + let result = self + .programs + .lock() + .unwrap() + .get_mut(&msg.author.id) + .unwrap() + .save_program(&self.conn.lock().unwrap(), msg.author.id, name); + match result { + Some(_) => { + msg.channel_id + .say(&ctx, format!("Saved as {}", name)) + .await + .unwrap(); + } + None => { + msg.channel_id + .say(&ctx, "Could not save program.") + .await + .unwrap(); + } + } + return; + } + + if let Some(name) = line.strip_prefix("LOAD ") { + let result = self + .programs + .lock() + .unwrap() + .get_mut(&msg.author.id) + .unwrap() + .load_program(&self.conn.lock().unwrap(), msg.author.id, name); + match result { + Some(_) => { + msg.channel_id + .say(&ctx, format!("Loaded {} into memory.", name)) + .await + .unwrap(); + } + None => { + msg.channel_id + .say(&ctx, "Could not load program into memory.") + .await + .unwrap(); + } + } + return; + } + + let mut split = line.splitn(2, ' '); + let first = split.next().unwrap(); + if let Ok(num) = first.parse::<u32>() { + match split.next() { + Some(x) => { + if x.is_empty() { + let _ = self + .programs + .lock() + .unwrap() + .get_mut(&msg.author.id) + .unwrap() + .code + .remove(&num); + return; + } + + self.programs + .lock() + .unwrap() + .get_mut(&msg.author.id) + .unwrap() + .code + .insert(num, x.to_owned()); + } + None => { + let _ = self + .programs + .lock() + .unwrap() + .get_mut(&msg.author.id) + .unwrap() + .code + .remove(&num); + } + } + } + } + } + } + + if line == "!START" { + self.programs + .lock() + .unwrap() + .insert(msg.author.id, Program::new()); + } + } + } + + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected", ready.user.name); + } +} diff --git a/src/main.rs b/src/main.rs index 6f972db..9249404 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,304 +1,24 @@ -mod config; +#[macro_use] +extern crate diesel; -use phf::phf_map; -use serenity::async_trait; -use serenity::http::AttachmentType; -use serenity::model::channel::{Message, ReactionType}; -use serenity::model::id::UserId; -use serenity::model::prelude::Ready; -use serenity::prelude::*; +mod config; +mod framebuffer; +mod handler; +mod models; +mod program; +mod schema; + +use crate::handler::Handler; +use diesel::{Connection, PgConnection}; +use dotenv::dotenv; use serenity::Client; -use std::borrow::Cow; -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Arc; -use xbasic::basic_io::BasicIO; -use xbasic::expr::ExprValue; -use xbasic::xbasic::XBasicBuilder; - -#[derive(Clone)] -struct FrameBuffer { - width: u32, - height: u32, - buffer: Vec<u8>, -} - -impl FrameBuffer { - fn new(width: u32, height: u32) -> Self { - Self { - width, - height, - buffer: vec![0; width as usize * height as usize * 4], - } - } - - fn set_pixel(&mut self, x: u32, y: u32, red: u8, green: u8, blue: u8, alpha: u8) { - if x >= self.width || y >= self.height { - return; - } - - let x = x as usize; - let y = y as usize; - self.buffer[(x + self.width as usize * y) * 4] = red; - self.buffer[(x + self.width as usize * y) * 4 + 1] = green; - self.buffer[(x + self.width as usize * y) * 4 + 2] = blue; - self.buffer[(x + self.width as usize * y) * 4 + 3] = alpha; - } - - fn as_png_vec(&self) -> Vec<u8> { - let mut buffer: Vec<u8> = Vec::new(); - - { - let mut encoder = png::Encoder::new(&mut buffer, self.width, self.height); - encoder.set_color(png::ColorType::RGBA); - encoder.set_depth(png::BitDepth::Eight); - let mut writer = encoder.write_header().unwrap(); - writer.write_image_data(&self.buffer).unwrap(); - } - - buffer - } -} - -struct DiscordIO { - s: String, - frame: Option<FrameBuffer>, -} - -impl DiscordIO { - fn new() -> Self { - Self { - s: String::new(), - frame: None, - } - } -} - -impl BasicIO for DiscordIO { - fn read_line(&mut self) -> String { - unimplemented!() - } - - fn write_line(&mut self, line: String) { - self.s += &*(line + "\r\n"); - } -} - -struct Program { - code: HashMap<u32, String>, -} - -impl Program { - fn new() -> Self { - Self { - code: HashMap::new(), - } - } - - fn stringify(&self) -> String { - let mut code: Vec<(u32, String)> = - self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect(); - code.sort_by(|a, b| a.0.cmp(&b.0)); - - code.into_iter() - .map(|a| a.1) - .collect::<Vec<String>>() - .join("\n") - } - - fn stringy_line_nums(&self) -> String { - let mut code: Vec<(u32, String)> = - self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect(); - code.sort_by(|a, b| a.0.cmp(&b.0)); - - code.into_iter() - .map(|a| format!("{}\t{}", a.0, a.1)) - .collect::<Vec<String>>() - .join("\n") - } -} - -struct Handler { - programs: Arc<Mutex<HashMap<UserId, Program>>>, -} - -static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! { -"cat" => "ðŸˆ", -"chicken" => "ðŸ”", -"spaghetti" => "ðŸ", -"dog" => "ðŸ•", -"bot" => "🤖", -"mango" => "ðŸ¥", -"banana" => "ðŸŒ", -"bee" => "ðŸ" -}; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - for (key, value) in EMOJI_MAP.entries() { - let msg_lower = format!(" {} ", msg.content.to_lowercase()); - if msg_lower.contains(&format!(" {} ", key)) - || msg_lower.contains(&format!(" {}s ", key)) - { - let reaction_type = match ReactionType::from_str(value) { - Ok(x) => x, - Err(x) => { - println!("Could not react: {}", x); - return; - } - }; - if let Err(e) = msg.react(&ctx, reaction_type).await { - println!("Error reacting: {}", e); - } - } - } - - for line in msg.content.split('\n') { - if self.programs.lock().await.contains_key(&msg.author.id) { - match line { - "!STOP" => { - self.programs.lock().await.remove(&msg.author.id).unwrap(); - } - "RUN" => { - let code = self.programs.lock().await[&msg.author.id].stringify(); - let io = DiscordIO::new(); - - let (output, fb, errors) = { - let mut xbb = XBasicBuilder::new(io); - xbb.compute_limit(1000000000); - - xbb.define_function("setframe".to_owned(), 2, |args, io| { - let w = args[0].clone().into_decimal() as u32; - let h = args[1].clone().into_decimal() as u32; - - io.frame = Some(FrameBuffer::new(w, h)); - - ExprValue::Decimal(0.0) - }) - .unwrap(); - - xbb.define_function("setpixel".to_owned(), 5, |args, io| { - let x = args[0].clone().into_decimal() as u32; - let y = args[1].clone().into_decimal() as u32; - let red = args[2].clone().into_decimal() as u8; - let green = args[3].clone().into_decimal() as u8; - let blue = args[4].clone().into_decimal() as u8; +use std::env; - match &mut io.frame { - Some(fb) => { - fb.set_pixel(x, y, red, green, blue, 255); - } - None => {} - } +fn establish_connection() -> PgConnection { + dotenv().ok(); - ExprValue::Decimal(0.0) - }) - .unwrap(); - - let mut xb = xbb.build(); - - let _ = xb.run(&format!("{}\n", code)); - - let errors = if xb.error_handler.had_errors - || xb.error_handler.had_runtime_error - { - Some(xb.error_handler.errors.join("\n")) - } else { - None - }; - - (xb.get_io().s.clone(), xb.get_io().frame.clone(), errors) - }; - - msg.channel_id.say(&ctx, output).await.unwrap(); - - if let Some(fb) = &fb { - let buf = fb.as_png_vec(); - - msg.channel_id - .send_message(&ctx, |e| { - e.add_file(AttachmentType::Bytes { - data: Cow::Borrowed(&buf), - filename: "output.png".to_string(), - }); - - e - }) - .await - .unwrap(); - } - - if let Some(e) = errors { - msg.channel_id.say(&ctx, e).await.unwrap(); - } - } - "LIST" => { - msg.channel_id - .say( - &ctx, - format!( - "```\n{}\n```", - self.programs.lock().await[&msg.author.id].stringy_line_nums() - ), - ) - .await - .unwrap(); - } - _ => { - let mut split = line.splitn(2, ' '); - let first = split.next().unwrap(); - if let Ok(num) = first.parse::<u32>() { - match split.next() { - Some(x) => { - if x.is_empty() { - let _ = self - .programs - .lock() - .await - .get_mut(&msg.author.id) - .unwrap() - .code - .remove(&num); - return; - } - - self.programs - .lock() - .await - .get_mut(&msg.author.id) - .unwrap() - .code - .insert(num, x.to_owned()); - } - None => { - let _ = self - .programs - .lock() - .await - .get_mut(&msg.author.id) - .unwrap() - .code - .remove(&num); - } - } - } - } - } - } - - if line == "!START" { - self.programs - .lock() - .await - .insert(msg.author.id, Program::new()); - } - } - } - - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected", ready.user.name); - } + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url).expect("Error connecting to database") } #[tokio::main] @@ -306,10 +26,10 @@ async fn main() { let config = config::get_conf(); let token = config.token; + let conn = establish_connection(); + let mut client = Client::builder(&token) - .event_handler(Handler { - programs: Arc::new(Mutex::new(HashMap::new())), - }) + .event_handler(Handler::new(conn)) .await .expect("Error creating client"); if let Err(e) = client.start().await { diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..0b7f705 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,18 @@ +use crate::schema::user_programs; +use bigdecimal::BigDecimal; + +#[derive(Queryable)] +pub struct UserProgram { + pub id: i32, + pub discord_user_id: u64, + pub name: String, + pub code: String, +} + +#[derive(Insertable)] +#[table_name = "user_programs"] +pub struct NewUserProgram<'a> { + pub discord_user_id: BigDecimal, + pub name: &'a str, + pub code: &'a str, +} diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 0000000..dd4a795 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,123 @@ +use crate::models::NewUserProgram; +use crate::schema::user_programs::columns; +use crate::schema::user_programs::columns::discord_user_id; +use crate::schema::user_programs::dsl::user_programs; +use bigdecimal::{BigDecimal, FromPrimitive}; +use diesel::{BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; +use serenity::model::id::UserId; +use std::collections::HashMap; + +pub(crate) struct Program { + pub(crate) code: HashMap<u32, String>, +} + +impl Program { + pub fn new() -> Self { + Self { + code: HashMap::new(), + } + } + + pub fn stringify(&self) -> String { + let mut code: Vec<(u32, String)> = + self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect(); + code.sort_by(|a, b| a.0.cmp(&b.0)); + + code.into_iter() + .map(|a| a.1) + .collect::<Vec<String>>() + .join("\n") + } + + pub fn stringy_line_nums(&self) -> String { + let mut code: Vec<(u32, String)> = + self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect(); + code.sort_by(|a, b| a.0.cmp(&b.0)); + + code.into_iter() + .map(|a| format!("{}\t{}", a.0, a.1)) + .collect::<Vec<String>>() + .join("\n") + } + + pub fn save_program(&self, conn: &PgConnection, user_id: UserId, name: &str) -> Option<()> { + let code = self.stringy_line_nums(); + let new_program = NewUserProgram { + discord_user_id: BigDecimal::from_u64(*user_id.as_u64()).unwrap(), + name, + code: code.as_str(), + }; + + diesel::insert_into(user_programs) + .values(&new_program) + .on_conflict((discord_user_id, columns::name)) + .do_update() + .set(columns::code.eq(&code)) + .execute(conn) + .ok()?; + + Some(()) + } + + pub fn load_program(&mut self, conn: &PgConnection, user_id: UserId, name: &str) -> Option<()> { + let code: Vec<String> = user_programs + .filter( + columns::discord_user_id + .eq(BigDecimal::from_u64(*user_id.as_u64()).unwrap()) + .and(columns::name.eq(name)), + ) + .limit(1) + .select(columns::code) + .get_results(conn) + .ok()?; + + if code.is_empty() { + return None; + } + + let code = &code[0]; + + self.parse_string(code)?; + + Some(()) + } + + fn parse_string(&mut self, code: &str) -> Option<()> { + let mut valid = true; + let code: HashMap<u32, String> = code + .split('\n') + .map(|line| { + let mut iter = line.splitn(2, &[' ', '\t'][..]); + // This unwrap_or_else thing is pretty ugly + // Is there a better way? + let line_num: u32 = iter + .next() + .unwrap_or_else(|| { + valid = false; + "0" + }) + .parse() + .unwrap_or_else(|_| { + valid = false; + 0 + }); + let line_code = iter + .next() + .unwrap_or_else(|| { + valid = false; + "" + }) + .to_owned(); + + (line_num, line_code) + }) + .collect(); + + if valid { + self.code = code; + Some(()) + } else { + None + } + } +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..115cafc --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,8 @@ +table! { + user_programs (id) { + id -> Int4, + discord_user_id -> Numeric, + name -> Varchar, + code -> Text, + } +} -- GitLab