From 801e3f90a82ac5d3d6f046a8b4a8439ec6b7f5ac Mon Sep 17 00:00:00 2001 From: jbell Date: Sun, 13 Oct 2024 17:04:59 -0400 Subject: [PATCH] Initial commit --- src/api/comment.rs | 100 ++++++++++++++++++++++ src/api/issue.rs | 153 ++++++++++++++++++++++++++++++++++ src/api/mod.rs | 3 + src/api/session.rs | 45 ++++++++++ src/db/db.rs | 12 +++ src/db/mod.rs | 1 + src/models/mod.rs | 1 + src/models/models.rs | 149 +++++++++++++++++++++++++++++++++ src/repositories/comment.rs | 38 +++++++++ src/repositories/issue.rs | 161 ++++++++++++++++++++++++++++++++++++ src/repositories/mod.rs | 3 + src/repositories/session.rs | 66 +++++++++++++++ 12 files changed, 732 insertions(+) create mode 100644 src/api/comment.rs create mode 100644 src/api/issue.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/session.rs create mode 100644 src/db/db.rs create mode 100644 src/db/mod.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/models.rs create mode 100644 src/repositories/comment.rs create mode 100644 src/repositories/issue.rs create mode 100644 src/repositories/mod.rs create mode 100644 src/repositories/session.rs diff --git a/src/api/comment.rs b/src/api/comment.rs new file mode 100644 index 0000000..c9d150c --- /dev/null +++ b/src/api/comment.rs @@ -0,0 +1,100 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use chrono::NaiveDateTime; +use crate::models::models::Comment; +use crate::repositories::comment::{create_comment, get_issue_comments}; +use crate::repositories::session::get_username_from_session; + +#[derive(Deserialize)] +pub struct AddCommentRequest { + pub content: String, + pub parent: Option, + pub issue_id: i32, +} + +#[derive(Serialize)] +pub struct AddCommentResponse { + pub content: String, + pub parent: Option, + pub telegram_handle: String, + pub created_at: NaiveDateTime, +} + +pub async fn add_comment(req: HttpRequest, data: web::Json) -> impl Responder { + if let Some(session) = req.cookie("session") { + if let Some(username) = get_username_from_session(session.value().to_string()) { + let new_comment = create_comment(data.content.clone(), data.parent, username.clone(), data.issue_id); + HttpResponse::Ok().json( + AddCommentResponse { + content: new_comment.content, + parent: new_comment.parent, + telegram_handle: username.clone(), + created_at: new_comment.created_at.unwrap() + }) + } else { + HttpResponse::Unauthorized().into() + } + } else { + HttpResponse::Unauthorized().into() + } +} + +#[derive(Deserialize)] +pub struct GetCommentsRequest { + pub issue_id: i32, +} + +#[derive(Serialize, Clone)] +pub struct CommentWithChildren { + pub comment: Comment, + pub children: Vec, +} + +#[derive(Serialize)] +pub struct GetCommentsResponse { + pub comments: Vec, +} + +fn build_comment_tree(comments: Vec) -> Vec { + let mut comment_map: HashMap = HashMap::new(); + let mut child_map: HashMap> = HashMap::new(); + let mut root_ids: Vec = Vec::new(); + + for comment in comments { + let id = comment.id; + let comment_with_children = CommentWithChildren { + comment: comment.clone(), + children: Vec::new(), + }; + comment_map.insert(id, comment_with_children); + + if let Some(parent_id) = comment.parent { + child_map.entry(parent_id).or_default().push(id); + } else { + root_ids.push(id); + } + } + + fn build_tree(id: i64, comment_map: &mut HashMap, child_map: &HashMap>) -> CommentWithChildren { + let mut comment = comment_map.remove(&id).unwrap(); + if let Some(child_ids) = child_map.get(&id) { + comment.children = child_ids.iter() + .map(|&child_id| build_tree(child_id, comment_map, child_map)) + .collect(); + } + comment + } + + root_ids.into_iter() + .map(|id| build_tree(id, &mut comment_map, &child_map)) + .collect() +} + +pub async fn get_comments(data: web::Query) -> impl Responder { + let comments = build_comment_tree(get_issue_comments(data.issue_id)); + + HttpResponse::Ok().json(GetCommentsResponse { + comments + }) +} \ No newline at end of file diff --git a/src/api/issue.rs b/src/api/issue.rs new file mode 100644 index 0000000..c7780a0 --- /dev/null +++ b/src/api/issue.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize}; +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use crate::models::models::IssueWithSummaryAndVotes; +use crate::repositories; +use crate::repositories::issue::{get_issue_vote_equity, get_vote_for_user, record_vote_for_issue, FilterOptions}; +use crate::repositories::session::get_user_id; + +#[derive(Serialize, Deserialize, Clone)] +enum IssueVote { + Positive, + Negative, +} + +#[derive(Deserialize)] +pub struct CreateIssueRequest { + title: String, + paragraphs: Vec, +} + +#[derive(Serialize)] +pub struct CreateIssueResponse { + title: String, + paragraphs: Vec, +} + +pub async fn add_issue(req: HttpRequest, data: web::Json) -> impl Responder { + if let Some(session_id) = req.cookie("session") { + if let Some(session) = repositories::session::get_session(session_id.value().to_string()) { + repositories::issue::create_issue(&data.title, &data.paragraphs, &session.username); + let resp = CreateIssueResponse { + title: String::from(&data.title), + paragraphs: data.paragraphs.clone(), + }; + return HttpResponse::Ok().json(resp); + } + HttpResponse::Unauthorized().into() + } else { + HttpResponse::Unauthorized().into() + } +} + +#[derive(Deserialize)] +pub struct IssuesPaginationRequest { + min_positive_votes: Option, + min_votes: Option, + offset: i32, + limit: i16, +} + +#[derive(Serialize)] +pub struct IssuesPaginationResponse { + issues: Vec, +} + +pub async fn list_issues(req: web::Query) -> impl Responder { + let issues = repositories::issue::get_issues( + Option::from(FilterOptions { + min_positive_votes: req.min_positive_votes, + min_votes: req.min_votes, + }), + req.offset, + req.limit + ); + HttpResponse::Ok().json(IssuesPaginationResponse { issues }) +} + +#[derive(Deserialize)] +pub struct GetParagraphsRequest { + issue_id: i32, +} + +#[derive(Serialize)] +pub struct GetParagraphsResponse { + paragraphs: Vec>, +} + +pub async fn get_paragraphs(query: web::Query) -> impl Responder { + let paragraphs = repositories::issue::get_paragraphs(query.issue_id); + HttpResponse::Ok().json(GetParagraphsResponse { paragraphs }) +} + +#[derive(Deserialize, Clone)] +pub struct VoteIssueRequest { + issue_id: i32, + vote: IssueVote, +} + +#[derive(Serialize)] +pub struct VoteIssueResponse { + issue_id: i32, + equity: i64, + positive: Option, +} + +pub async fn vote_issue(req: HttpRequest, data: web::Json) -> impl Responder { + let positive_vote = match data.vote { + IssueVote::Positive => true, + IssueVote::Negative => false, + }; + if let Some(session_id) = req.cookie("session") { + if let Some(user_id) = get_user_id(session_id.value().to_string()) { + record_vote_for_issue(positive_vote, data.issue_id, user_id); + let resp = VoteIssueResponse { + issue_id: data.issue_id, + equity: get_issue_vote_equity(data.issue_id), + positive: match data.vote { + IssueVote::Positive => Some(true), + IssueVote::Negative => Some(false), + }, + }; + HttpResponse::Ok().json(resp) + } else { + HttpResponse::Unauthorized().into() + } + } else { + HttpResponse::Unauthorized().into() + } +} + +#[derive(Deserialize)] +pub struct GetUserVoteRequest { + issue_id: i32, +} + +#[derive(Serialize)] +pub struct GetUserVoteResponse { + vote: Option, +} + +pub async fn get_user_vote(req: HttpRequest, query: web::Query) -> impl Responder { + let mut resp = GetUserVoteResponse { vote: None }; + if let Some(session_cookie) = req.cookie("session") { + return if let Some(user_id) = get_user_id(session_cookie.value().to_string()) { + let vote = get_vote_for_user(query.issue_id, user_id); + resp = GetUserVoteResponse { + vote: match vote { + Some(val) => { + if val { + Some(IssueVote::Positive) + } else { + Some(IssueVote::Negative) + } + }, + None => None, + } + }; + HttpResponse::Ok().json(resp) + } else { + HttpResponse::Ok().json(GetUserVoteResponse { vote: None }) + } + } + HttpResponse::Ok().json(resp) +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..d0e83c2 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod issue; +pub(crate) mod session; +pub(crate) mod comment; \ No newline at end of file diff --git a/src/api/session.rs b/src/api/session.rs new file mode 100644 index 0000000..ea15262 --- /dev/null +++ b/src/api/session.rs @@ -0,0 +1,45 @@ +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use crate::repositories; +use crate::repositories::session::get_username_from_session; + +#[derive(Deserialize, Clone)] +pub struct AuthenticateRequest { + user_id: i64, + auth_date: i64, + username: String, + first_name: String, + last_name: String, + photo_url: String, +} + +#[derive(Serialize)] +pub struct AuthenticateResponse { + session_id: String, +} + +pub async fn authenticate(req: web::Json) -> impl Responder { + let session_id = repositories::session::authenticate_session( + req.clone().user_id, + req.clone().auth_date, + req.clone().username, + req.clone().first_name, + req.clone().last_name, + req.clone().photo_url + ); + let resp = AuthenticateResponse { session_id }; + HttpResponse::Ok().json(resp) +} + +#[derive(Serialize)] +pub struct GetUsernameResponse { + username: Option, +} + +pub async fn get_username(req: HttpRequest) -> impl Responder { + if let Some(session_cookie) = req.cookie("session") { + let username = get_username_from_session(session_cookie.value().to_string()); + return HttpResponse::Ok().json(GetUsernameResponse { username }) + } + HttpResponse::Unauthorized().into() +} \ No newline at end of file diff --git a/src/db/db.rs b/src/db/db.rs new file mode 100644 index 0000000..2358422 --- /dev/null +++ b/src/db/db.rs @@ -0,0 +1,12 @@ +use std::env; +use diesel::Connection; +use diesel::PgConnection; +use dotenvy::dotenv; + +pub fn establish_connection() -> PgConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..2e70b07 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1 @@ +pub(crate) mod db; \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..c446ac8 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/src/models/models.rs b/src/models/models.rs new file mode 100644 index 0000000..5d024c2 --- /dev/null +++ b/src/models/models.rs @@ -0,0 +1,149 @@ +use diesel::sql_types::Integer; +use diesel::sql_types::Nullable; +use diesel::sql_types::SmallInt; +use diesel::sql_types::Timestamp; +use diesel::sql_types::Text; +use diesel::sql_types::BigInt; +use chrono::NaiveDateTime; +use diesel::{Insertable, Queryable, QueryableByName, Selectable}; +use serde::Serialize; + +#[derive(Queryable, Selectable, Serialize, Clone)] +#[diesel(table_name = crate::schema::issues)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Issue { + pub id: i32, + pub title: String, + pub paragraph_count: Option, + pub telegram_handle: String, + pub created_at: Option +} + +#[derive(QueryableByName, Serialize, Clone)] +pub struct IssueWithSummaryAndVotes { + #[diesel(sql_type = Integer)] + pub id: i32, + #[diesel(sql_type = Text)] + pub title: String, + #[diesel(sql_type = Text)] + pub summary: String, + #[diesel(sql_type = Nullable)] + pub paragraph_count: Option, + #[diesel(sql_type = Text)] + pub telegram_handle: String, + #[diesel(sql_type = Nullable)] + pub created_at: Option, + #[diesel(sql_type = BigInt)] + pub total_votes: i64, + #[diesel(sql_type = BigInt)] + pub positive_votes: i64, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::issues)] +pub struct NewIssue { + pub title: String, + pub paragraph_count: i16, + pub telegram_handle: String, +} + +#[derive(Queryable, Selectable)] +#[diesel(table_name = crate::schema::sessions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Session { + pub id: i64, + pub session_id: String, + pub auth_date: Option, + pub username: Option, + pub first_name: Option, + pub last_name: Option, + pub photo_url: Option +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::sessions)] +pub struct NewSession { + pub user_id: Option, + pub session_id: String, + pub auth_date: i64, + pub username: Option, + pub first_name: Option, + pub last_name: Option, + pub photo_url: Option +} + +#[derive(Queryable, Selectable, Serialize, Clone)] +#[diesel(table_name = crate::schema::paragraphs)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Paragraph { + pub id: i64, + pub content: String, + pub index: i32, + pub post_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::paragraphs)] +pub struct NewParagraph { + pub content: Option, + pub index: i32, + pub post_id: i32, +} + +#[derive(Queryable, Selectable, Serialize, Clone)] +#[diesel(table_name = crate::schema::issue_votes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct IssueVote { + pub id: i64, + pub positive: Option, + pub issue_id: i32, + pub user_id: i64, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::issue_votes)] +pub struct NewIssueVote { + pub positive: bool, + pub issue_id: i32, + pub user_id: i64, +} + +#[derive(Queryable, Selectable, Serialize, Clone)] +#[diesel(table_name = crate::schema::comments)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Comment { + pub id: i64, + pub content: String, + pub parent: Option, + pub telegram_handle: String, + pub issue_id: i32, + pub created_at: Option +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::comments)] +pub struct NewComment { + pub content: String, + pub parent: Option, + pub telegram_handle: String, + pub issue_id: i32, + pub created_at: NaiveDateTime +} + +#[derive(Queryable, Selectable, Serialize, Clone)] +#[diesel(table_name = crate::schema::comment_votes)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct CommentVote { + pub id: i64, + pub positive: Option, + pub comment_id: i64, + pub user_id: i64, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::comment_votes)] +pub struct NewCommentVote { + pub positive: Option, + pub comment_id: Option, + pub user_id: Option, +} diff --git a/src/repositories/comment.rs b/src/repositories/comment.rs new file mode 100644 index 0000000..721c05c --- /dev/null +++ b/src/repositories/comment.rs @@ -0,0 +1,38 @@ +use crate::db::db::establish_connection; +use crate::models::models::{Comment, NewComment}; +use crate::schema::comments; +use chrono::Utc; +use diesel::{alias, ExpressionMethods, JoinOnDsl, QueryDsl, RunQueryDsl, SelectableHelper, NullableExpressionMethods}; +use crate::schema; + +pub fn get_issue_comments(issue_id: i32) -> Vec { + let connection = &mut establish_connection(); + + let (c1, c2) = alias!(comments as c1, comments as c2); + + comments::table + .left_join(c1.on(comments::parent.eq(comments::id.nullable()))) + .filter(comments::issue_id.eq(issue_id)) + .select(Comment::as_select()) + .load::(connection) + .unwrap_or_else(|_| Vec::new()) +} + +pub fn create_comment(content: String, parent: Option, telegram_handle: String, issue_id: i32) -> Comment { + let connection = &mut establish_connection(); + + let new_comment = NewComment { + content, + parent, + telegram_handle, + issue_id, + created_at: Utc::now().naive_utc() + }; + let insertion = diesel::insert_into(comments::table) + .values(&new_comment) + .returning(Comment::as_select()) + .get_result(connection) + .expect("Error saving comment"); + + insertion +} \ No newline at end of file diff --git a/src/repositories/issue.rs b/src/repositories/issue.rs new file mode 100644 index 0000000..30e4dbe --- /dev/null +++ b/src/repositories/issue.rs @@ -0,0 +1,161 @@ +use crate::db::db::establish_connection; +use crate::models::models::{Issue, IssueWithSummaryAndVotes, NewIssue, NewIssueVote, NewParagraph, Paragraph}; +use crate::schema::issue_votes::{issue_id, positive}; +use crate::schema::paragraphs; +use crate::schema::paragraphs::{content, index}; +use crate::schema::{issue_votes, issues}; +use chrono::NaiveDateTime; +use diesel; +use diesel::associations::HasTable; +use diesel::dsl::count; +use diesel::result::DatabaseErrorKind; +use diesel::result::Error::DatabaseError; +use diesel::{sql_query, BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct FilterOptions { + pub min_positive_votes: Option, + pub min_votes: Option, +} + +pub fn get_issues(filters: Option, offset: i32, limit: i16) -> Vec { + let connection = &mut establish_connection(); + + let query = sql_query("select i.id, title, content as summary, i.paragraph_count, telegram_handle, i.created_at, + count(v.id) as total_votes, + sum(case when v.positive then 1 else 0 end) as positive_votes + from issues i + inner join paragraphs p on i.id = p.post_id + inner join issue_votes v on i.id = v.issue_id + where p.index = 0 + group by v.issue_id, title, telegram_handle, content, i.id;"); + + let mut issues_with_content: Vec = query + .load::(connection) + .unwrap_or_default() + .into_iter() + .collect(); + + if let Some(filter_info) = filters { + if let Some(positive_votes) = filter_info.min_positive_votes { + issues_with_content = issues_with_content + .iter() + .filter(|i| i.positive_votes >= positive_votes) + .map(|i| i.clone()) + .collect() + } + if let Some(min_votes) = filter_info.min_votes { + issues_with_content = issues_with_content + .iter() + .filter(|i| i.total_votes >= min_votes) + .map(|i| i.clone()) + .collect() + } + } + + let len = issues_with_content.len(); + let i1 = offset.max(0) as usize; + let i2 = i1.saturating_add(limit as usize).min(len); + issues_with_content[i1..i2].to_vec() +} + +pub fn create_issue(title: &String, paragraphs: &Vec, telegram_handle: &Option) -> Option { + let connection = &mut establish_connection(); + + if let Some(tg_handle) = telegram_handle { + let new_issue = NewIssue { + title: title.to_string(), + paragraph_count: paragraphs.len() as i16, + telegram_handle: tg_handle.to_string(), + }; + let insertion = diesel::insert_into(issues::table) + .values(&new_issue) + .returning(Issue::as_select()) + .get_result(connection) + .expect("Error saving new issue"); + + let _ = diesel::insert_into(paragraphs::table) + .values(paragraphs.iter().enumerate().map(|(i, p)| + NewParagraph { + content: Some(p.clone()), + index: i as i32, + post_id: insertion.id + }).collect::>()) + .returning(Paragraph::as_select()) + .get_results(connection) + .expect("Error saving paragraphs"); + + return Some(insertion) + } + None +} + +pub fn get_paragraphs(other_issue_id: i32) -> Vec> { + let connection = &mut establish_connection(); + paragraphs::table + .filter(paragraphs::post_id.eq(other_issue_id)) + .order(index) + .select(content) + .load::(connection) + .unwrap_or_default() + .into_iter() + .map(Some) + .collect::>>() +} + + +pub fn record_vote_for_issue(other_positive: bool, other_issue_id: i32, user_id: i64) -> bool { + let connection = &mut establish_connection(); + + let result = diesel::insert_into(issue_votes::table) + .values(NewIssueVote { positive: other_positive, issue_id: other_issue_id, user_id }) + .execute(connection); + + match result { + Ok(_) => true, + Err(DatabaseError(kind, _)) => { + if let DatabaseErrorKind::UniqueViolation = kind { + let _ = diesel::update( + issue_votes::table + .filter( + issue_votes::user_id.eq(user_id).and(issue_votes::issue_id.eq(issue_id)) + ) + ) + .set(issue_votes::positive.eq(positive)) + .execute(connection); + return true + } + false + }, + _ => false + } +} + +pub fn get_issue_vote_equity(issue_identifier: i32) -> i64 { + let connection = &mut establish_connection(); + let positive_votes: i64 = issue_votes::table + .filter(issue_votes::issue_id.eq(issue_identifier).and(issue_votes::positive.eq(true))) + .count() + .get_result(connection) + .unwrap(); + let negative_votes: i64 = issue_votes::table + .filter(issue_votes::issue_id.eq(issue_identifier).and(issue_votes::positive.eq(false))) + .count() + .get_result(connection) + .unwrap(); + + positive_votes - negative_votes +} + +pub fn get_vote_for_user(other_issue_id: i32, other_user_id: i64) -> Option { + let connection = &mut establish_connection(); + let result: Option = issue_votes::table + .filter(issue_votes::issue_id.eq(other_issue_id).and(issue_votes::user_id.eq(other_user_id))) + .limit(1) + .select(issue_votes::positive) + .get_result::>(connection) + .unwrap_or(None); + + result +} diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs new file mode 100644 index 0000000..d0e83c2 --- /dev/null +++ b/src/repositories/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod issue; +pub(crate) mod session; +pub(crate) mod comment; \ No newline at end of file diff --git a/src/repositories/session.rs b/src/repositories/session.rs new file mode 100644 index 0000000..e753c40 --- /dev/null +++ b/src/repositories/session.rs @@ -0,0 +1,66 @@ +use diesel::{OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; +use diesel::ExpressionMethods; +use uuid::Uuid; +use crate::db::db::establish_connection; +use crate::models::models::{NewSession, Session}; +use crate::schema::sessions; +use crate::schema::sessions::dsl::sessions as sessions_dsl; +use crate::schema::sessions::session_id; + +pub fn get_session(sess_id: String) -> Option { + let connection = &mut establish_connection(); + sessions_dsl + .filter(session_id.eq(sess_id)) + .select(Session::as_select()) + .first(connection) + .optional() + .expect("Error loading session") +} + +pub fn authenticate_session(user_id: i64, auth_date: i64, username: String, first_name: String, last_name: String, photo_url: String) -> String { + let sess_id: Uuid = Uuid::new_v4(); + + let connection = &mut establish_connection(); + let new_session = NewSession { + user_id: Some(user_id), + session_id: sess_id.clone().to_string(), + auth_date, + username: Some(username), + first_name: Some(first_name), + last_name: Some(last_name), + photo_url: Some(photo_url), + }; + + let result = diesel::insert_into(sessions::table) + .values(&new_session) + .returning(Session::as_select()) + .get_result(connection) + .unwrap(); + + result.session_id +} + +pub fn get_user_id(sess_id: String) -> Option { + let connection = &mut establish_connection(); + let user_id: Option = sessions::table + .filter(session_id.eq(sess_id)) + .limit(1) + .select(sessions::user_id) + .get_result(connection) + .optional() + .unwrap_or(None); + + user_id +} + +pub fn get_username_from_session(sess_id: String) -> Option { + let connection = &mut establish_connection(); + let username = sessions::table + .filter(sessions::session_id.eq(sess_id)) + .limit(1) + .select(sessions::username) + .get_result(connection) + .unwrap_or_default(); + + username +}