~fd/beetboard

29a5bb4e6297a14fe6a625bbd272123e3e2e279d — Ersei Saggi 8 months ago b5aae34
Everything else
M Cargo.lock => Cargo.lock +1 -0
@@ 224,6 224,7 @@ dependencies = [
 "argon2",
 "sea-orm",
 "sea-orm-migration",
 "time",
 "tokio",
]


M Cargo.toml => Cargo.toml +1 -0
@@ 10,3 10,4 @@ tokio = { version = "1", features = ["full"] }
sea-orm = { version = "0.12", features = [ "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros" ] }
sea-orm-migration = "0.12"
argon2 = "0.5"
time = "0.3"

M src/entities/mod.rs => src/entities/mod.rs +1 -2
@@ 1,5 1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2

pub mod prelude;

pub mod user;
pub mod post;

A src/entities/post.rs => src/entities/post.rs +45 -0
@@ 0,0 1,45 @@
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub user: i32,
    pub parent: Option<i32>,
    pub title: String,
    pub body: Option<String>,
    pub date: TimeDateTime,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(belongs_to = "Entity", from = "Column::Parent", to = "Column::Id")]
    Parent,
    #[sea_orm(
        belongs_to = "super::user::Entity",
        from = "Column::User",
        to = "super::user::Column::Id"
    )]
    User,
}

impl Related<super::user::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

pub struct ParentLink;

impl Linked for ParentLink {
    type FromEntity = Entity;

    type ToEntity = Entity;

    fn link(&self) -> Vec<RelationDef> {
        vec![Relation::Parent.def()]
    }
}

impl ActiveModelBehavior for ActiveModel {}

M src/entities/prelude.rs => src/entities/prelude.rs +1 -2
@@ 1,3 1,2 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2

pub use super::user::Entity as User;
pub use super::post::Entity as Post;

M src/entities/user.rs => src/entities/user.rs +11 -3
@@ 1,5 1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]


@@ 12,6 10,16 @@ pub struct Model {
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
pub enum Relation {
    #[sea_orm(has_many = "super::post::Entity")]
    Post,
}

// `Related` trait has to be implemented by hand
impl Related<super::post::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Post.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

M src/lib.rs => src/lib.rs +175 -2
@@ 2,6 2,8 @@ mod entities;
mod split_unquoted_char;
mod strings;

use time::{OffsetDateTime, PrimitiveDateTime};

use entities::{prelude::*, *};

use sea_orm::*;


@@ 14,6 16,7 @@ use argon2::{

pub struct BeetClient {
    user: Option<String>,
    user_id: Option<i32>,
    conn: DatabaseConnection,
    exit: bool,
}


@@ 22,13 25,13 @@ impl BeetClient {
    pub fn new(db: DatabaseConnection) -> Self {
        return BeetClient {
            user: None,
            user_id: None,
            conn: db,
            exit: false,
        };
    }

    pub fn hash_password(password: &str) -> String {
        // TODO: this is kinda slow, we should have a global osrng instance
        let salt = SaltString::generate(&mut OsRng);

        // Argon2 with default params (Argon2id v19)


@@ 69,9 72,13 @@ impl BeetClient {
                println!("User not found");
                return (strings::LOGIN_BAD.to_owned() + strings::PROMPT).to_string();
            }
            if !Self::verify_password(&command[2], &test_user.as_ref().unwrap().as_ref().unwrap().password) {
            if !Self::verify_password(
                &command[2],
                &test_user.as_ref().unwrap().as_ref().unwrap().password,
            ) {
                return (strings::LOGIN_BAD.to_owned() + strings::PROMPT).to_string();
            }
            self.user_id = Some(test_user.as_ref().unwrap().as_ref().unwrap().id);
            self.user = Some(test_user.unwrap().unwrap().username);
            return (strings::LOGIN_SUCCESS.to_owned() + strings::PROMPT).to_string();
        }


@@ 97,6 104,164 @@ impl BeetClient {
        }
    }

    pub async fn post(&mut self, command: Vec<&str>) -> String {
        if self.user.is_none() || self.user_id.is_none() {
            return strings::UNAUTHED.to_owned() + strings::PROMPT;
        }
        if command.len() == 1 || command.len() > 3 {
            return strings::POST_HELP.to_owned() + strings::PROMPT;
        }
        let body = if command.get(2).is_none() {
            None
        } else {
            Some(command[2].to_owned())
        };
        let now_odt = OffsetDateTime::now_utc();
        let new_post = post::ActiveModel {
            title: ActiveValue::Set(command[1].to_owned()),
            parent: ActiveValue::Set(None),
            body: ActiveValue::Set(body),
            user: ActiveValue::Set(self.user_id.unwrap()),
            date: ActiveValue::Set(PrimitiveDateTime::new(now_odt.date(), now_odt.time())),
            ..Default::default()
        };
        let res = Post::insert(new_post).exec(&self.conn).await;
        match res {
            Ok(r) => (strings::POST_RESULT.to_owned()
                + &r.last_insert_id.to_string()
                + "\n"
                + strings::OK
                + strings::PROMPT)
                .to_string(),
            Err(e) => e.to_string() + "\n" + strings::PROMPT,
        }
    }

    pub async fn comment(&mut self, command: Vec<&str>) -> String {
        if self.user.is_none() || self.user_id.is_none() {
            return strings::UNAUTHED.to_owned() + strings::PROMPT;
        }
        if command.len() != 3 {
            return strings::COMMENT_HELP.to_owned() + strings::PROMPT;
        }

        let id = command[1].parse::<i32>();
        if id.is_err() {
            return strings::COMMENT_HELP.to_owned() + strings::PROMPT;
        }

        let now_odt = OffsetDateTime::now_utc();
        let new_comment = post::ActiveModel {
            title: ActiveValue::Set(command[2].to_owned()),
            parent: ActiveValue::Set(Some(id.unwrap())),
            body: ActiveValue::Set(None),
            user: ActiveValue::Set(self.user_id.unwrap()),
            date: ActiveValue::Set(PrimitiveDateTime::new(now_odt.date(), now_odt.time())),
            ..Default::default()
        };
        let res = Post::insert(new_comment).exec(&self.conn).await;
        match res {
            Ok(r) => (strings::POST_RESULT.to_owned()
                + &r.last_insert_id.to_string()
                + "\n"
                + strings::OK
                + strings::PROMPT)
                .to_string(),
            Err(e) => e.to_string() + "\n" + strings::PROMPT,
        }
    }

    pub async fn get_home_posts(&mut self, command: Vec<&str>) -> String {
        if self.user.is_none() || self.user_id.is_none() {
            return strings::UNAUTHED.to_owned() + strings::PROMPT;
        }
        if command.len() == 2 && strings::HELP_COMMANDS.contains(&command[1]) {
            return strings::SHOW_HELP.to_owned();
        }
        let mut output: String = "".to_string();

        // Could be a lot better with joins, but I don't know how to do that :(
        let post_finder = post::Entity::find()
            .find_with_related(user::Entity)
            .filter(post::Column::Parent.is_null())
            .order_by_asc(post::Column::Id)
            .limit(10);

        for p in post_finder.all(&self.conn).await.unwrap() {
            output.push_str(
                &format!(
                    "#{}: {}\nBy {} at {}\n\n",
                    p.0.id, p.0.title, p.1[0].username, p.0.date
                )
                .to_owned(),
            );
        }
        return output + strings::PROMPT;
    }

    pub async fn read(&mut self, command: Vec<&str>) -> String {
        if self.user.is_none() || self.user_id.is_none() {
            return strings::UNAUTHED.to_owned() + strings::PROMPT;
        }
        if command.len() != 2 {
            return strings::READ_HELP.to_owned() + strings::PROMPT;
        }
        if command.len() == 2 && strings::HELP_COMMANDS.contains(&command[1]) {
            return strings::READ_HELP.to_owned() + strings::PROMPT;
        }
        let id = command[1].parse::<i32>();
        if id.is_err() {
            return strings::READ_HELP.to_owned() + strings::PROMPT;
        }
        let mut output: String = "".to_string();

        // Could be a lot better with joins, but I don't know how to do that :(
        let post_finder = post::Entity::find()
            .filter(post::Column::Id.eq(id.unwrap()))
            .find_with_related(user::Entity)
            .filter(post::Column::Parent.is_null())
            .order_by_asc(post::Column::Id)
            .all(&self.conn)
            .await
            .unwrap();

        if post_finder.len() != 1 {
            return strings::READ_NOT_FOUND.to_owned() + strings::PROMPT;
        }

        let p = &post_finder[0];

        output.push_str(
            &format!(
                "#{}: {}\nBy {} at {}\n{}\n",
                p.0.id,
                p.0.title,
                p.1[0].username,
                p.0.date,
                p.0.body.as_ref().unwrap_or(&"empty".to_owned())
            )
            .to_owned(),
        );

        let comment_finder = post::Entity::find()
            .find_with_related(user::Entity)
            .filter(post::Column::Parent.eq(p.0.id))
            .order_by_asc(post::Column::Id)
            .all(&self.conn)
            .await
            .unwrap();
        for p in comment_finder {
            output.push_str(
                &format!(
                    "Comment #{}: {}\nBy {} at {}\n\n",
                    p.0.id, p.0.title, p.1[0].username, p.0.date,
                )
                .to_owned(),
            );
        }
        return output + strings::PROMPT;
    }

    pub fn logout(&mut self) -> String {
        self.user = None;
        strings::GOODBYE.to_string()


@@ 135,6 300,14 @@ impl BeetClient {
            }
            Some(help) if (strings::HELP_COMMANDS.contains(help)) => self.help(),
            Some(logout) if (strings::LOGOUT_COMMANDS.contains(logout)) => self.logout(),
            Some(post) if (strings::POST_COMMANDS.contains(post)) => self.post(command).await,
            Some(read) if (strings::READ_COMMANDS.contains(read)) => self.read(command).await,
            Some(comment) if (strings::COMMENT_COMMANDS.contains(comment)) => {
                self.comment(command).await
            }
            Some(show) if (strings::SHOW_COMMANDS.contains(show)) => {
                self.get_home_posts(command).await
            }
            Some(exit) if (strings::EXIT_COMMANDS.contains(exit)) => self.exit(),
            _ => strings::UNKNOWN_COMMAND.to_owned() + &self.help(),
        }

M src/main.rs => src/main.rs +1 -0
@@ 69,6 69,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
                    .expect("failed to write data to socket");

                if b_client.should_exit() {
                    socket.flush().await.unwrap();
                    return;
                }
            }

A src/migration/m20220101_000002_create_post_table.rs => src/migration/m20220101_000002_create_post_table.rs +50 -0
@@ 0,0 1,50 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Replace the sample below with your own migration scripts
        manager
            .create_table(
                Table::create()
                    .table(Post::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Post::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Post::User).not_null().integer())
                    .col(ColumnDef::new(Post::Parent).integer())
                    .col(ColumnDef::new(Post::Title).string().not_null())
                    .col(ColumnDef::new(Post::Body).string())
                    .col(ColumnDef::new(Post::Date).timestamp().not_null())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Replace the sample below with your own migration scripts
        manager
            .drop_table(Table::drop().table(Post::Table).to_owned())
            .await
    }
}

/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum Post {
    Table,
    Id,
    User,
    Parent,
    Title,
    Body,
    Date,
}

M src/migration/mod.rs => src/migration/mod.rs +5 -1
@@ 1,12 1,16 @@
pub use sea_orm_migration::prelude::*;

mod m20220101_000001_create_user_table;
mod m20220101_000002_create_post_table;

pub struct Migrator;

//#[tokio::async_trait]
impl MigratorTrait for Migrator {
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
        vec![Box::new(m20220101_000001_create_user_table::Migration)]
        vec![
            Box::new(m20220101_000001_create_user_table::Migration),
            Box::new(m20220101_000002_create_post_table::Migration),
        ]
    }
}

M src/strings.rs => src/strings.rs +30 -3
@@ 22,8 22,10 @@ o888bood8P'  `Y8bod8P' `Y888\"\"8o d888b    `Y8bod88P

";

pub const HELP_UNAUTHED: &str = "Type (login) to log in, (register) to register, (exit) to exit, and (help) for help.\n";
pub const HELP_UNAUTHED: &str =
    "Type (login) to log in, (register) to register, (exit) to exit, and (help) for help.\n";

// TODO: Need Moar Help
pub const HELP: &str = "Type (logout) to log out, (exit) to exit, and (help) for help.\n";

pub const HELP_COMMANDS: [&str; 3] = ["help", "h", "?"];


@@ 34,13 36,15 @@ pub const UNKNOWN_COMMAND: &str = "Unknown command\n";

pub const REGISTER_COMMANDS: [&str; 1] = ["register"];

pub const REGISTER_WRONG_NUMBER_OF_ARGS: &str = "Wrong number of arguments! Use \"register help\" for help!\n";
pub const REGISTER_WRONG_NUMBER_OF_ARGS: &str =
    "Wrong number of arguments! Use \"register help\" for help!\n";

pub const REGISTER_HELP: &str = "Usage: register username password\n";

pub const LOGIN_COMMANDS: [&str; 2] = ["login", "l"];

pub const LOGIN_WRONG_NUMBER_OF_ARGS: &str = "Wrong number of arguments! Use \"login help\" for help!\n";
pub const LOGIN_WRONG_NUMBER_OF_ARGS: &str =
    "Wrong number of arguments! Use \"login help\" for help!\n";

pub const LOGIN_HELP: &str = "Usage: login username password\n";



@@ 52,6 56,29 @@ pub const EXIT_COMMANDS: [&str; 3] = ["exit", "quit", "q"];

pub const LOGOUT_COMMANDS: [&str; 1] = ["logout"];

pub const POST_COMMANDS: [&str; 2] = ["post", "p"];

pub const POST_HELP: &str = "Usage: post title [body (optional)]\n";

pub const POST_RESULT: &str = "Posted as #";

pub const SHOW_COMMANDS: [&str; 2] = ["show", "s"];

pub const SHOW_HELP: &str =
    "Usage: show [before ID (optional)]. Note that this only shows the 10 most recent top posts.\n";

pub const READ_COMMANDS: [&str; 2] = ["read", "r"];

pub const READ_HELP: &str = "Usage: read id\n";

pub const READ_NOT_FOUND: &str = "Post not found\n";

pub const COMMENT_COMMANDS: [&str; 2] = ["comment", "c"];

pub const COMMENT_HELP: &str = "Usage: comment id message\n";

pub const UNAUTHED: &str = "You can't do that right now. Try logging in\n";

pub const OK: &str = "Ok\n";

pub const INTERNAL_ERR: &str = "Internal error! Please report this issue\n";