Broadcast update comments and issues
This commit is contained in:
parent
27db59aa26
commit
87da5a28c2
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,7 @@
|
||||
/target
|
||||
mail.toml
|
||||
mail.test.toml
|
||||
web.toml
|
||||
web.test.toml
|
||||
db.toml
|
||||
db.test.toml
|
||||
|
@ -1,65 +1,73 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::shared::write_auth_token;
|
||||
use crate::{model, Msg, APP};
|
||||
|
||||
pub mod issue;
|
||||
|
||||
pub fn handle(msg: WsMsg) {
|
||||
let app = match unsafe { APP.as_mut().unwrap() }.write() {
|
||||
Ok(app) => app,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
WsMsg::Ping | WsMsg::Pong => {}
|
||||
_ => app.update(Msg::WsMsg(msg)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
// auth
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
|
||||
model.user = Some(user.clone());
|
||||
}
|
||||
Msg::WsMsg(WsMsg::AuthorizeExpired) => {
|
||||
if let Ok(msg) = write_auth_token(None) {
|
||||
orders.skip().send_msg(msg);
|
||||
}
|
||||
}
|
||||
// project
|
||||
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
|
||||
model.project = Some(project.clone());
|
||||
}
|
||||
// issues
|
||||
Msg::WsMsg(WsMsg::ProjectIssuesLoaded(v)) => {
|
||||
let mut v = v.clone();
|
||||
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||
model.issues = v;
|
||||
}
|
||||
// users
|
||||
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
|
||||
model.users = v.clone();
|
||||
}
|
||||
// comments
|
||||
Msg::WsMsg(WsMsg::IssueCommentsLoaded(comments)) => {
|
||||
let mut v = comments.clone();
|
||||
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
||||
model.comments = v;
|
||||
}
|
||||
Msg::WsMsg(WsMsg::CommentDeleted(comment_id)) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.comments, &mut old);
|
||||
for comment in old.into_iter() {
|
||||
if *comment_id != comment.id {
|
||||
model.comments.push(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
orders.render();
|
||||
}
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::model::*;
|
||||
use crate::shared::write_auth_token;
|
||||
use crate::{Msg, APP};
|
||||
|
||||
pub mod issue;
|
||||
|
||||
pub fn handle(msg: WsMsg) {
|
||||
let app = match unsafe { APP.as_mut().unwrap() }.write() {
|
||||
Ok(app) => app,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
WsMsg::Ping | WsMsg::Pong => {}
|
||||
_ => app.update(Msg::WsMsg(msg)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
// auth
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
|
||||
model.user = Some(user.clone());
|
||||
}
|
||||
Msg::WsMsg(WsMsg::AuthorizeExpired) => {
|
||||
if let Ok(msg) = write_auth_token(None) {
|
||||
orders.skip().send_msg(msg);
|
||||
}
|
||||
}
|
||||
// project
|
||||
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
|
||||
model.project = Some(project.clone());
|
||||
}
|
||||
// issues
|
||||
Msg::WsMsg(WsMsg::ProjectIssuesLoaded(v)) => {
|
||||
let mut v = v.clone();
|
||||
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||
model.issues = v;
|
||||
}
|
||||
// users
|
||||
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
|
||||
model.users = v.clone();
|
||||
}
|
||||
// comments
|
||||
Msg::WsMsg(WsMsg::IssueCommentsLoaded(comments)) => {
|
||||
let issue_id = match model.modals.get(0) {
|
||||
Some(ModalType::EditIssue(issue_id, _)) => *issue_id,
|
||||
_ => return,
|
||||
};
|
||||
if comments.iter().any(|c| c.issue_id != issue_id) {
|
||||
return;
|
||||
}
|
||||
let mut v = comments.clone();
|
||||
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
|
||||
model.comments = v;
|
||||
}
|
||||
Msg::WsMsg(WsMsg::CommentDeleted(comment_id)) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut model.comments, &mut old);
|
||||
for comment in old.into_iter() {
|
||||
if *comment_id != comment.id {
|
||||
model.comments.push(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
orders.render();
|
||||
}
|
||||
|
@ -19,8 +19,12 @@ pub mod users;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub type DbPool = r2d2::Pool<ConnectionManager<dev::VerboseConnection>>;
|
||||
#[cfg(debug_assertions)]
|
||||
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<dev::VerboseConnection>>;
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<dev::PgConnection>>;
|
||||
|
||||
pub struct DbExecutor {
|
||||
pub pool: DbPool,
|
||||
@ -42,13 +46,13 @@ impl Default for DbExecutor {
|
||||
|
||||
pub fn build_pool() -> DbPool {
|
||||
dotenv::dotenv().ok();
|
||||
let config = Configuration::read();
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
||||
#[cfg(not(debug_assertions))]
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url.clone());
|
||||
let manager = ConnectionManager::<PgConnection>::new(config.database_url.clone());
|
||||
#[cfg(debug_assertions)]
|
||||
let manager: ConnectionManager<VerboseConnection> =
|
||||
ConnectionManager::<dev::VerboseConnection>::new(database_url.as_str());
|
||||
ConnectionManager::<dev::VerboseConnection>::new(config.database_url.as_str());
|
||||
r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
|
||||
@ -144,9 +148,15 @@ pub struct Configuration {
|
||||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
let database_url = if cfg!(test) {
|
||||
"postgres://postgres@localhost:5432/jirs_test"
|
||||
} else {
|
||||
"postgres://postgres@localhost:5432/jirs"
|
||||
}
|
||||
.to_string();
|
||||
Self {
|
||||
concurrency: 2,
|
||||
database_url: "postgres://postgres@localhost:5432/jirs".to_string(),
|
||||
database_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,7 +180,13 @@ impl Configuration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn config_file() -> &'static str {
|
||||
"db.toml"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn config_file() -> &'static str {
|
||||
"db.test.toml"
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ use actix::{Handler, Message};
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::DbExecutor;
|
||||
use crate::db::{DbExecutor, DbPooledConn};
|
||||
use crate::errors::ServiceErrors;
|
||||
use crate::models::{IssueAssignee, User, UserForm};
|
||||
use crate::models::{CreateProjectForm, IssueAssignee, Project, User, UserForm};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct FindUser {
|
||||
@ -114,6 +114,7 @@ impl Handler<Register> for DbExecutor {
|
||||
type Result = Result<(), ServiceErrors>;
|
||||
|
||||
fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use crate::schema::projects::dsl::projects;
|
||||
use crate::schema::users::dsl::*;
|
||||
|
||||
let conn = &self
|
||||
@ -121,32 +122,33 @@ impl Handler<Register> for DbExecutor {
|
||||
.get()
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
|
||||
let query = users
|
||||
.filter(
|
||||
email
|
||||
.eq(msg.email.as_str())
|
||||
.and(name.ne(msg.name.as_str()))
|
||||
.or(email.ne(msg.email.as_str()).and(name.eq(msg.name.as_str())))
|
||||
.or(email.eq(msg.email.as_str()).and(name.eq(msg.name.as_str()))),
|
||||
)
|
||||
.count();
|
||||
|
||||
info!(
|
||||
"{}",
|
||||
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
|
||||
);
|
||||
|
||||
let matching: i64 = query.get_result(conn).unwrap_or(1);
|
||||
let matching = count_matching_users(msg.name.as_str(), msg.email.as_str(), conn);
|
||||
|
||||
if matching > 0 {
|
||||
return Err(ServiceErrors::RegisterCollision);
|
||||
}
|
||||
|
||||
let project: Project = match projects.first(conn) {
|
||||
Ok(project) => project,
|
||||
_ => {
|
||||
let form = CreateProjectForm {
|
||||
name: "initial".to_string(),
|
||||
url: "".to_string(),
|
||||
description: "".to_string(),
|
||||
category: Default::default(),
|
||||
};
|
||||
diesel::insert_into(projects)
|
||||
.values(form)
|
||||
.get_result(conn)
|
||||
.map_err(|_| ServiceErrors::RegisterCollision)?
|
||||
}
|
||||
};
|
||||
|
||||
let form = UserForm {
|
||||
name: msg.name,
|
||||
email: msg.email,
|
||||
avatar_url: None,
|
||||
project_id: None,
|
||||
project_id: project.id,
|
||||
};
|
||||
|
||||
match diesel::insert_into(users).values(form).execute(conn) {
|
||||
@ -157,3 +159,66 @@ impl Handler<Register> for DbExecutor {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
|
||||
use crate::schema::users::dsl;
|
||||
|
||||
let query = dsl::users
|
||||
.filter(dsl::email.eq(email).and(dsl::name.ne(name)))
|
||||
.or_filter(dsl::email.ne(email).and(dsl::name.eq(name)))
|
||||
.or_filter(dsl::email.eq(email).and(dsl::name.eq(name)))
|
||||
.count();
|
||||
|
||||
info!(
|
||||
"{}",
|
||||
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
|
||||
);
|
||||
|
||||
query.get_result::<i64>(conn).unwrap_or(1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::build_pool;
|
||||
use crate::models::{CreateProjectForm, Project};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_collision() {
|
||||
use crate::schema::projects::dsl::projects;
|
||||
use crate::schema::users::dsl::users;
|
||||
|
||||
let pool = build_pool();
|
||||
let conn = &pool.get().unwrap();
|
||||
|
||||
diesel::delete(users).execute(conn).unwrap();
|
||||
diesel::delete(projects).execute(conn).unwrap();
|
||||
|
||||
let project_form = CreateProjectForm {
|
||||
name: "baz".to_string(),
|
||||
url: "/uz".to_string(),
|
||||
description: "None".to_string(),
|
||||
category: Default::default(),
|
||||
};
|
||||
let project: Project = diesel::insert_into(projects)
|
||||
.values(project_form)
|
||||
.get_result(conn)
|
||||
.unwrap();
|
||||
|
||||
let user_form = UserForm {
|
||||
name: "Foo".to_string(),
|
||||
email: "foo@example.com".to_string(),
|
||||
avatar_url: None,
|
||||
project_id: project.id,
|
||||
};
|
||||
diesel::insert_into(users)
|
||||
.values(user_form)
|
||||
.execute(conn)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count_matching_users("Foo", "bar@example.com", conn), 1);
|
||||
assert_eq!(count_matching_users("Bar", "foo@example.com", conn), 1);
|
||||
assert_eq!(count_matching_users("Foo", "foo@example.com", conn), 1);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,13 @@ impl Configuration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn config_file() -> &'static str {
|
||||
"mail.toml"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn config_file() -> &'static str {
|
||||
"mail.test.toml"
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,19 @@ extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use actix::Actor;
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer};
|
||||
|
||||
use crate::ws::WsServer;
|
||||
|
||||
pub mod db;
|
||||
pub mod errors;
|
||||
pub mod mail;
|
||||
pub mod middleware;
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod utils;
|
||||
pub mod web;
|
||||
pub mod ws;
|
||||
|
||||
@ -33,10 +37,13 @@ async fn main() -> Result<(), String> {
|
||||
crate::mail::MailExecutor::default,
|
||||
);
|
||||
|
||||
let ws_server = WsServer::default().start();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(Cors::default())
|
||||
.data(ws_server.clone())
|
||||
.data(db_addr.clone())
|
||||
.data(mail_addr.clone())
|
||||
.data(crate::db::build_pool())
|
||||
|
@ -155,6 +155,15 @@ pub struct UpdateProjectForm {
|
||||
pub category: Option<ProjectCategory>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "projects"]
|
||||
pub struct CreateProjectForm {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
pub category: ProjectCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
@ -201,7 +210,7 @@ pub struct UserForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub project_id: Option<i32>,
|
||||
pub project_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable)]
|
||||
|
1
jirs-server/src/utils.rs
Normal file
1
jirs-server/src/utils.rs
Normal file
@ -0,0 +1 @@
|
||||
|
@ -73,7 +73,13 @@ impl Configuration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn config_file() -> &'static str {
|
||||
"web.toml"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn config_file() -> &'static str {
|
||||
"web.test.toml"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix::Addr;
|
||||
use actix::*;
|
||||
use actix_web::web::Data;
|
||||
|
||||
use jirs_data::{IssueFieldId, PayloadVariant, WsMsg};
|
||||
@ -10,6 +10,110 @@ use crate::db::issues::{LoadProjectIssues, UpdateIssue};
|
||||
use crate::db::DbExecutor;
|
||||
use crate::ws::{current_user, WsResult};
|
||||
|
||||
/*
|
||||
pub struct UpdateIssueHandler {
|
||||
id: i32,
|
||||
field_id: IssueFieldId,
|
||||
payload: PayloadVariant,
|
||||
}
|
||||
|
||||
impl Message for UpdateIssueHandler {
|
||||
type Result = WsResult;
|
||||
}
|
||||
|
||||
impl Actor for UpdateIssueHandler {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<UpdateIssueHandler> for WebSocketActor {
|
||||
type Result = WsResult;
|
||||
|
||||
fn handle(&mut self, msg: UpdateIssueHandler, ctx: &mut Self::Context) -> Self::Result {
|
||||
self.require_user()?;
|
||||
|
||||
let UpdateIssueHandler {
|
||||
id,
|
||||
field_id,
|
||||
payload,
|
||||
} = msg;
|
||||
|
||||
let mut msg = UpdateIssue::default();
|
||||
msg.issue_id = id;
|
||||
match (field_id, payload) {
|
||||
(IssueFieldId::Type, PayloadVariant::IssueType(t)) => {
|
||||
msg.issue_type = Some(t);
|
||||
}
|
||||
(IssueFieldId::Title, PayloadVariant::String(s)) => {
|
||||
msg.title = Some(s);
|
||||
}
|
||||
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
||||
msg.description = Some(s);
|
||||
}
|
||||
(IssueFieldId::Status, PayloadVariant::IssueStatus(s)) => {
|
||||
msg.status = Some(s);
|
||||
}
|
||||
(IssueFieldId::ListPosition, PayloadVariant::I32(i)) => {
|
||||
msg.list_position = Some(i);
|
||||
}
|
||||
(IssueFieldId::Assignees, PayloadVariant::VecI32(v)) => {
|
||||
msg.user_ids = Some(v);
|
||||
}
|
||||
(IssueFieldId::Reporter, PayloadVariant::I32(i)) => {
|
||||
msg.reporter_id = Some(i);
|
||||
}
|
||||
(IssueFieldId::Priority, PayloadVariant::IssuePriority(p)) => {
|
||||
msg.priority = Some(p);
|
||||
}
|
||||
(IssueFieldId::Estimate, PayloadVariant::OptionI32(o)) => {
|
||||
msg.estimate = o;
|
||||
}
|
||||
(IssueFieldId::TimeSpend, PayloadVariant::OptionI32(o)) => {
|
||||
msg.time_spent = o;
|
||||
}
|
||||
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
|
||||
msg.time_remaining = o;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let mut updated: Option<jirs_data::Issue> = None;
|
||||
|
||||
self.db
|
||||
.send(msg)
|
||||
.into_actor(self)
|
||||
.then(move |res, _act, _ctx| {
|
||||
updated = res.ok().and_then(|r| r.ok()).map(|i| i.into());
|
||||
fut::ready(())
|
||||
})
|
||||
.wait(ctx);
|
||||
|
||||
let mut issue = match updated {
|
||||
Some(issue) => issue,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let mut assignees = vec![];
|
||||
|
||||
self.db
|
||||
.send(LoadAssignees { issue_id: issue.id })
|
||||
.into_actor(self)
|
||||
.then(|res, _act, _ctx| {
|
||||
if let Ok(Ok(v)) = res {
|
||||
assignees = v;
|
||||
}
|
||||
fut::ready(())
|
||||
})
|
||||
.wait(ctx);
|
||||
|
||||
for assignee in assignees {
|
||||
issue.user_ids.push(assignee.user_id);
|
||||
}
|
||||
|
||||
Ok(Some(WsMsg::IssueUpdated(issue)))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn update_issue(
|
||||
db: &Data<Addr<DbExecutor>>,
|
||||
user: &Option<jirs_data::User>,
|
||||
|
@ -1,9 +1,14 @@
|
||||
use actix::{Actor, Addr, StreamHandler};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use actix::{
|
||||
Actor, ActorContext, Addr, AsyncContext, Context, Handler, Message, Recipient, StreamHandler,
|
||||
};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{get, web, Error, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
use futures::executor::block_on;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
use jirs_data::{ProjectId, UserId, WsMsg};
|
||||
|
||||
use crate::db::authorize_user::AuthorizeUser;
|
||||
use crate::db::tokens::FindBindToken;
|
||||
@ -26,13 +31,14 @@ pub fn current_user(current_user: &Option<jirs_data::User>) -> Result<&jirs_data
|
||||
}
|
||||
|
||||
trait WsMessageSender {
|
||||
fn send_msg(&mut self, msg: jirs_data::WsMsg);
|
||||
fn send_msg(&mut self, msg: &jirs_data::WsMsg);
|
||||
}
|
||||
|
||||
struct WebSocketActor {
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
current_user: Option<jirs_data::User>,
|
||||
addr: Addr<WsServer>,
|
||||
}
|
||||
|
||||
impl Actor for WebSocketActor {
|
||||
@ -40,104 +46,141 @@ impl Actor for WebSocketActor {
|
||||
}
|
||||
|
||||
impl WsMessageSender for ws::WebsocketContext<WebSocketActor> {
|
||||
fn send_msg(&mut self, msg: WsMsg) {
|
||||
self.binary(bincode::serialize(&msg).unwrap())
|
||||
fn send_msg(&mut self, msg: &WsMsg) {
|
||||
self.binary(bincode::serialize(msg).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<InnerMsg> for WebSocketActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: InnerMsg, ctx: &mut Self::Context) -> Self::Result {
|
||||
match msg {
|
||||
InnerMsg::Transfer(msg) => ctx.send_msg(&msg),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketActor {
|
||||
fn handle_ws_msg(&mut self, msg: WsMsg) -> WsResult {
|
||||
use futures::executor::block_on;
|
||||
fn broadcast(&self, msg: &WsMsg) {
|
||||
let user = match self.current_user.as_ref() {
|
||||
Some(u) => u,
|
||||
_ => return,
|
||||
};
|
||||
self.addr
|
||||
.do_send(InnerMsg::BroadcastToChannel(user.project_id, msg.clone()));
|
||||
}
|
||||
|
||||
fn handle_ws_msg(
|
||||
&mut self,
|
||||
msg: WsMsg,
|
||||
ctx: &mut <WebSocketActor as Actor>::Context,
|
||||
) -> WsResult {
|
||||
if msg != WsMsg::Ping && msg != WsMsg::Pong {
|
||||
info!("incoming message: {:?}", msg);
|
||||
debug!("incoming message: {:?}", msg);
|
||||
}
|
||||
|
||||
let msg =
|
||||
match msg {
|
||||
WsMsg::Ping => Some(WsMsg::Pong),
|
||||
WsMsg::Pong => Some(WsMsg::Ping),
|
||||
let msg = match msg {
|
||||
WsMsg::Ping => Some(WsMsg::Pong),
|
||||
WsMsg::Pong => Some(WsMsg::Ping),
|
||||
|
||||
// Issues
|
||||
WsMsg::IssueUpdateRequest(id, field_id, payload) => block_on(
|
||||
issues::update_issue(&self.db, &self.current_user, id, field_id, payload),
|
||||
)?,
|
||||
WsMsg::IssueCreateRequest(payload) => {
|
||||
block_on(issues::add_issue(&self.db, &self.current_user, payload))?
|
||||
}
|
||||
WsMsg::IssueDeleteRequest(id) => {
|
||||
block_on(issues::delete_issue(&self.db, &self.current_user, id))?
|
||||
}
|
||||
WsMsg::ProjectIssuesRequest => {
|
||||
block_on(issues::load_issues(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
// projects
|
||||
WsMsg::ProjectRequest => {
|
||||
block_on(projects::current_project(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
WsMsg::ProjectUpdateRequest(payload) => block_on(projects::update_project(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
))?,
|
||||
|
||||
// auth
|
||||
WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid))?,
|
||||
WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?,
|
||||
WsMsg::AuthenticateRequest(email, name) => {
|
||||
block_on(auth::authenticate(&self.db, &self.mail, name, email))?
|
||||
}
|
||||
|
||||
// register
|
||||
WsMsg::SignUpRequest(email, username) => {
|
||||
block_on(users::register(&self.db, &self.mail, username, email))?
|
||||
}
|
||||
|
||||
// users
|
||||
WsMsg::ProjectUsersRequest => {
|
||||
block_on(users::load_project_users(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
// comments
|
||||
WsMsg::IssueCommentsRequest(issue_id) => block_on(comments::load_issues(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
issue_id,
|
||||
))?,
|
||||
|
||||
WsMsg::CreateComment(payload) => block_on(comments::create_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
))?,
|
||||
|
||||
WsMsg::UpdateComment(payload) => block_on(comments::update_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
))?,
|
||||
|
||||
WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
comment_id,
|
||||
))?,
|
||||
|
||||
// else fail
|
||||
_ => {
|
||||
error!("No handle for {:?} specified", msg);
|
||||
// Issues
|
||||
WsMsg::IssueUpdateRequest(id, field_id, payload) => match block_on(
|
||||
issues::update_issue(&self.db, &self.current_user, id, field_id, payload),
|
||||
) {
|
||||
Ok(Some(msg)) => {
|
||||
self.broadcast(&msg);
|
||||
None
|
||||
}
|
||||
};
|
||||
_ => None,
|
||||
},
|
||||
WsMsg::IssueCreateRequest(payload) => {
|
||||
block_on(issues::add_issue(&self.db, &self.current_user, payload))?
|
||||
}
|
||||
WsMsg::IssueDeleteRequest(id) => {
|
||||
block_on(issues::delete_issue(&self.db, &self.current_user, id))?
|
||||
}
|
||||
WsMsg::ProjectIssuesRequest => {
|
||||
block_on(issues::load_issues(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
// projects
|
||||
WsMsg::ProjectRequest => {
|
||||
block_on(projects::current_project(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
WsMsg::ProjectUpdateRequest(payload) => block_on(projects::update_project(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
))?,
|
||||
|
||||
// auth
|
||||
WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid, ctx))?,
|
||||
WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?,
|
||||
WsMsg::AuthenticateRequest(email, name) => {
|
||||
block_on(auth::authenticate(&self.db, &self.mail, name, email))?
|
||||
}
|
||||
|
||||
// register
|
||||
WsMsg::SignUpRequest(email, username) => {
|
||||
block_on(users::register(&self.db, &self.mail, username, email))?
|
||||
}
|
||||
|
||||
// users
|
||||
WsMsg::ProjectUsersRequest => {
|
||||
block_on(users::load_project_users(&self.db, &self.current_user))?
|
||||
}
|
||||
|
||||
// comments
|
||||
WsMsg::IssueCommentsRequest(issue_id) => block_on(comments::load_issues(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
issue_id,
|
||||
))?,
|
||||
|
||||
WsMsg::CreateComment(payload) => block_on(comments::create_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
))?,
|
||||
|
||||
WsMsg::UpdateComment(payload) => match block_on(comments::update_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
payload,
|
||||
)) {
|
||||
Ok(Some(msg)) => {
|
||||
self.broadcast(&msg);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment(
|
||||
&self.db,
|
||||
&self.current_user,
|
||||
comment_id,
|
||||
))?,
|
||||
|
||||
// else fail
|
||||
_ => {
|
||||
error!("No handle for {:?} specified", msg);
|
||||
None
|
||||
}
|
||||
};
|
||||
if msg.is_some() && msg != Some(WsMsg::Pong) {
|
||||
info!("sending message {:?}", msg);
|
||||
}
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn check_auth_token(&mut self, token: uuid::Uuid) -> WsResult {
|
||||
async fn check_auth_token(
|
||||
&mut self,
|
||||
token: uuid::Uuid,
|
||||
ctx: &mut <WebSocketActor as Actor>::Context,
|
||||
) -> WsResult {
|
||||
let m = match self
|
||||
.db
|
||||
.send(AuthorizeUser {
|
||||
@ -148,6 +191,7 @@ impl WebSocketActor {
|
||||
Ok(Ok(u)) => {
|
||||
let user: jirs_data::User = u.into();
|
||||
self.current_user = Some(user.clone());
|
||||
self.join_channel(ctx.address().recipient()).await;
|
||||
Some(WsMsg::AuthorizeLoaded(Ok(user)))
|
||||
}
|
||||
Ok(Err(_)) => Some(WsMsg::AuthorizeLoaded(
|
||||
@ -167,6 +211,27 @@ impl WebSocketActor {
|
||||
};
|
||||
Ok(Some(WsMsg::BindTokenOk(token.access_token)))
|
||||
}
|
||||
|
||||
async fn join_channel(&self, addr: Recipient<InnerMsg>) {
|
||||
info!("joining channel...");
|
||||
info!(" current user {:?}", self.current_user);
|
||||
let user = match self.current_user.as_ref() {
|
||||
None => return,
|
||||
Some(u) => u,
|
||||
};
|
||||
match self
|
||||
.addr
|
||||
.send(InnerMsg::Join(user.project_id, user.id, addr))
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("{}", e),
|
||||
_ => info!(" joined channel"),
|
||||
};
|
||||
}
|
||||
|
||||
// fn require_user(&self) -> Result<&jirs_data::User, WsMsg> {
|
||||
// current_user(&self.current_user)
|
||||
// }
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
|
||||
@ -182,15 +247,107 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
|
||||
Ok(m) => m,
|
||||
_ => return,
|
||||
};
|
||||
match self.handle_ws_msg(msg) {
|
||||
Ok(Some(msg)) => ctx.send_msg(msg),
|
||||
Err(e) => ctx.send_msg(e),
|
||||
match self.handle_ws_msg(msg, ctx) {
|
||||
Ok(Some(msg)) => ctx.send_msg(&msg),
|
||||
Err(e) => ctx.send_msg(&e),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||
info!("Disconnected");
|
||||
if let Some(user) = self.current_user.as_ref() {
|
||||
self.addr.do_send(InnerMsg::Leave(user.project_id, user.id));
|
||||
}
|
||||
ctx.stop()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message, Debug)]
|
||||
#[rtype(result = "()")]
|
||||
pub enum InnerMsg {
|
||||
Join(ProjectId, UserId, Recipient<InnerMsg>),
|
||||
Leave(ProjectId, UserId),
|
||||
BroadcastToChannel(ProjectId, WsMsg),
|
||||
Transfer(WsMsg),
|
||||
}
|
||||
|
||||
pub struct WsServer {
|
||||
sessions: HashMap<i32, Recipient<InnerMsg>>,
|
||||
rooms: HashMap<i32, HashSet<i32>>,
|
||||
}
|
||||
|
||||
impl Default for WsServer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sessions: HashMap::new(),
|
||||
rooms: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for WsServer {
|
||||
type Result = ();
|
||||
}
|
||||
|
||||
impl Actor for WsServer {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<InnerMsg> for WsServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: InnerMsg, _ctx: &mut Self::Context) -> Self::Result {
|
||||
debug!("receive {:?}", msg);
|
||||
match msg {
|
||||
InnerMsg::Join(project_id, user_id, recipient) => {
|
||||
self.sessions.insert(user_id, recipient);
|
||||
self.ensure_room(project_id);
|
||||
if let Some(room) = self.rooms.get_mut(&project_id) {
|
||||
room.insert(user_id);
|
||||
}
|
||||
}
|
||||
InnerMsg::Leave(project_id, user_id) => {
|
||||
self.ensure_room(project_id);
|
||||
if let Some(room) = self.rooms.get_mut(&project_id) {
|
||||
room.remove(&user_id);
|
||||
}
|
||||
self.sessions.remove(&user_id);
|
||||
}
|
||||
InnerMsg::BroadcastToChannel(project_id, msg) => {
|
||||
debug!("Begin broadcast to channel {} msg {:?}", project_id, msg);
|
||||
let set = match self.rooms.get(&project_id) {
|
||||
Some(s) => s,
|
||||
_ => return debug!(" channel not found, aborting..."),
|
||||
};
|
||||
for r in set {
|
||||
let recipient = match self.sessions.get(r) {
|
||||
Some(r) => r,
|
||||
_ => {
|
||||
debug!("recipient is dead, skipping...");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match recipient.do_send(InnerMsg::Transfer(msg.clone())) {
|
||||
Ok(_) => debug!("msg sent"),
|
||||
Err(e) => error!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WsServer {
|
||||
pub fn ensure_room(&mut self, room: i32) {
|
||||
if !self.rooms.contains_key(&room) {
|
||||
self.rooms.insert(room, HashSet::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ws/")]
|
||||
@ -199,12 +356,14 @@ pub async fn index(
|
||||
stream: web::Payload,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
ws_server: Data<Addr<WsServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
WebSocketActor {
|
||||
db,
|
||||
mail,
|
||||
current_user: None,
|
||||
addr: ws_server.get_ref().clone(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
|
Loading…
Reference in New Issue
Block a user