Browse Source

Basic saving/loading

pull/1/head
Stephen 1 month ago
parent
commit
805f9cb49d
15 changed files with 643 additions and 303 deletions
  1. +1
    -0
      .env
  2. +75
    -0
      Cargo.lock
  3. +5
    -3
      Cargo.toml
  4. +5
    -0
      diesel.toml
  5. +0
    -0
      migrations/.gitkeep
  6. +6
    -0
      migrations/00000000000000_diesel_initial_setup/down.sql
  7. +36
    -0
      migrations/00000000000000_diesel_initial_setup/up.sql
  8. +1
    -0
      migrations/2020-11-21-032604_create_user_programs/down.sql
  9. +11
    -0
      migrations/2020-11-21-032604_create_user_programs/up.sql
  10. +43
    -0
      src/framebuffer.rs
  11. +291
    -0
      src/handler.rs
  12. +20
    -300
      src/main.rs
  13. +18
    -0
      src/models.rs
  14. +123
    -0
      src/program.rs
  15. +8
    -0
      src/schema.rs

+ 1
- 0
.env View File

@ -0,0 +1 @@
DATABASE_URL=postgres://stephen@localhost/cat_disruptor_6500

+ 75
- 0
Cargo.lock View File

@ -58,6 +58,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -94,6 +105,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
name = "cat-disruptor-6500"
version = "0.1.0"
dependencies = [
"bigdecimal",
"diesel",
"dotenv",
"phf",
"png",
"serde",
@ -161,6 +175,33 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -170,6 +211,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -644,6 +691,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -816,6 +874,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -1385,6 +1452,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@ -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",


+ 5
- 3
Cargo.toml View File

@ -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"
xbasic = "0.2"
png = "0.16"
diesel = { version = "1.4", features = ["postgres", "numeric"] }
dotenv = "0.15.0"
bigdecimal = "0.1.2"

+ 5
- 0
diesel.toml View File

@ -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"

+ 0
- 0
migrations/.gitkeep View File


+ 6
- 0
migrations/00000000000000_diesel_initial_setup/down.sql View File

@ -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();

+ 36
- 0
migrations/00000000000000_diesel_initial_setup/up.sql View File

@ -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;

+ 1
- 0
migrations/2020-11-21-032604_create_user_programs/down.sql View File

@ -0,0 +1 @@
DROP TABLE user_programs

+ 11
- 0
migrations/2020-11-21-032604_create_user_programs/up.sql View File

@ -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
);

+ 43
- 0
src/framebuffer.rs View File

@ -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
}
}

+ 291
- 0
src/handler.rs View File

@ -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);
}
}

+ 20
- 300
src/main.rs View File

@ -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 {


+ 18
- 0
src/models.rs View File

@ -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,
}

+ 123
- 0
src/program.rs View File

@ -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
}
}
}

+ 8
- 0
src/schema.rs View File

@ -0,0 +1,8 @@
table! {
user_programs (id) {
id -> Int4,
discord_user_id -> Numeric,
name -> Varchar,
code -> Text,
}
}

Loading…
Cancel
Save