Broadcast update comments and issues

This commit is contained in:
Adrian Wozniak 2020-04-19 10:57:09 +02:00
parent 27db59aa26
commit 87da5a28c2
11 changed files with 561 additions and 177 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
/target /target
mail.toml mail.toml
mail.test.toml
web.toml web.toml
web.test.toml
db.toml db.toml
db.test.toml

View File

@ -2,8 +2,9 @@ use seed::prelude::*;
use jirs_data::WsMsg; use jirs_data::WsMsg;
use crate::model::*;
use crate::shared::write_auth_token; use crate::shared::write_auth_token;
use crate::{model, Msg, APP}; use crate::{Msg, APP};
pub mod issue; pub mod issue;
@ -19,7 +20,7 @@ pub fn handle(msg: WsMsg) {
} }
} }
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
// auth // auth
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => { Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
@ -46,6 +47,13 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
} }
// comments // comments
Msg::WsMsg(WsMsg::IssueCommentsLoaded(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(); let mut v = comments.clone();
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
model.comments = v; model.comments = v;

View File

@ -19,8 +19,12 @@ pub mod users;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub type DbPool = r2d2::Pool<ConnectionManager<dev::VerboseConnection>>; pub type DbPool = r2d2::Pool<ConnectionManager<dev::VerboseConnection>>;
#[cfg(debug_assertions)]
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<dev::VerboseConnection>>;
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
#[cfg(not(debug_assertions))]
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<dev::PgConnection>>;
pub struct DbExecutor { pub struct DbExecutor {
pub pool: DbPool, pub pool: DbPool,
@ -42,13 +46,13 @@ impl Default for DbExecutor {
pub fn build_pool() -> DbPool { pub fn build_pool() -> DbPool {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let config = Configuration::read();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL");
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
let manager = ConnectionManager::<PgConnection>::new(database_url.clone()); let manager = ConnectionManager::<PgConnection>::new(config.database_url.clone());
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let manager: ConnectionManager<VerboseConnection> = let manager: ConnectionManager<VerboseConnection> =
ConnectionManager::<dev::VerboseConnection>::new(database_url.as_str()); ConnectionManager::<dev::VerboseConnection>::new(config.database_url.as_str());
r2d2::Pool::builder() r2d2::Pool::builder()
.build(manager) .build(manager)
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e)) .unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
@ -144,9 +148,15 @@ pub struct Configuration {
impl Default for Configuration { impl Default for Configuration {
fn default() -> Self { fn default() -> Self {
let database_url = if cfg!(test) {
"postgres://postgres@localhost:5432/jirs_test"
} else {
"postgres://postgres@localhost:5432/jirs"
}
.to_string();
Self { Self {
concurrency: 2, concurrency: 2,
database_url: "postgres://postgres@localhost:5432/jirs".to_string(), database_url,
} }
} }
} }
@ -170,7 +180,13 @@ impl Configuration {
Ok(()) Ok(())
} }
#[cfg(not(test))]
pub fn config_file() -> &'static str { pub fn config_file() -> &'static str {
"db.toml" "db.toml"
} }
#[cfg(test)]
pub fn config_file() -> &'static str {
"db.test.toml"
}
} }

View File

@ -2,9 +2,9 @@ use actix::{Handler, Message};
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::DbExecutor; use crate::db::{DbExecutor, DbPooledConn};
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::models::{IssueAssignee, User, UserForm}; use crate::models::{CreateProjectForm, IssueAssignee, Project, User, UserForm};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FindUser { pub struct FindUser {
@ -114,6 +114,7 @@ impl Handler<Register> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::projects;
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
let conn = &self let conn = &self
@ -121,32 +122,33 @@ impl Handler<Register> for DbExecutor {
.get() .get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = users let matching = count_matching_users(msg.name.as_str(), msg.email.as_str(), conn);
.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);
if matching > 0 { if matching > 0 {
return Err(ServiceErrors::RegisterCollision); 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 { let form = UserForm {
name: msg.name, name: msg.name,
email: msg.email, email: msg.email,
avatar_url: None, avatar_url: None,
project_id: None, project_id: project.id,
}; };
match diesel::insert_into(users).values(form).execute(conn) { match diesel::insert_into(users).values(form).execute(conn) {
@ -157,3 +159,66 @@ impl Handler<Register> for DbExecutor {
Ok(()) 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);
}
}

View File

@ -86,7 +86,13 @@ impl Configuration {
Ok(()) Ok(())
} }
#[cfg(not(test))]
fn config_file() -> &'static str { fn config_file() -> &'static str {
"mail.toml" "mail.toml"
} }
#[cfg(test)]
fn config_file() -> &'static str {
"mail.test.toml"
}
} }

View File

@ -5,15 +5,19 @@ extern crate diesel;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use actix::Actor;
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer};
use crate::ws::WsServer;
pub mod db; pub mod db;
pub mod errors; pub mod errors;
pub mod mail; pub mod mail;
pub mod middleware; pub mod middleware;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
pub mod utils;
pub mod web; pub mod web;
pub mod ws; pub mod ws;
@ -33,10 +37,13 @@ async fn main() -> Result<(), String> {
crate::mail::MailExecutor::default, crate::mail::MailExecutor::default,
); );
let ws_server = WsServer::default().start();
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.wrap(Cors::default()) .wrap(Cors::default())
.data(ws_server.clone())
.data(db_addr.clone()) .data(db_addr.clone())
.data(mail_addr.clone()) .data(mail_addr.clone())
.data(crate::db::build_pool()) .data(crate::db::build_pool())

View File

@ -155,6 +155,15 @@ pub struct UpdateProjectForm {
pub category: Option<ProjectCategory>, 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)] #[derive(Debug, Serialize, Deserialize, Queryable)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
@ -201,7 +210,7 @@ pub struct UserForm {
pub name: String, pub name: String,
pub email: String, pub email: String,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
pub project_id: Option<i32>, pub project_id: i32,
} }
#[derive(Debug, Serialize, Deserialize, Queryable)] #[derive(Debug, Serialize, Deserialize, Queryable)]

1
jirs-server/src/utils.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -73,7 +73,13 @@ impl Configuration {
Ok(()) Ok(())
} }
#[cfg(not(test))]
pub fn config_file() -> &'static str { pub fn config_file() -> &'static str {
"web.toml" "web.toml"
} }
#[cfg(test)]
pub fn config_file() -> &'static str {
"web.test.toml"
}
} }

View File

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use actix::Addr; use actix::*;
use actix_web::web::Data; use actix_web::web::Data;
use jirs_data::{IssueFieldId, PayloadVariant, WsMsg}; use jirs_data::{IssueFieldId, PayloadVariant, WsMsg};
@ -10,6 +10,110 @@ use crate::db::issues::{LoadProjectIssues, UpdateIssue};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult}; 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( pub async fn update_issue(
db: &Data<Addr<DbExecutor>>, db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>, user: &Option<jirs_data::User>,

View File

@ -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::web::Data;
use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use actix_web::{get, web, Error, HttpRequest, HttpResponse};
use actix_web_actors::ws; 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::authorize_user::AuthorizeUser;
use crate::db::tokens::FindBindToken; use crate::db::tokens::FindBindToken;
@ -26,13 +31,14 @@ pub fn current_user(current_user: &Option<jirs_data::User>) -> Result<&jirs_data
} }
trait WsMessageSender { trait WsMessageSender {
fn send_msg(&mut self, msg: jirs_data::WsMsg); fn send_msg(&mut self, msg: &jirs_data::WsMsg);
} }
struct WebSocketActor { struct WebSocketActor {
db: Data<Addr<DbExecutor>>, db: Data<Addr<DbExecutor>>,
mail: Data<Addr<MailExecutor>>, mail: Data<Addr<MailExecutor>>,
current_user: Option<jirs_data::User>, current_user: Option<jirs_data::User>,
addr: Addr<WsServer>,
} }
impl Actor for WebSocketActor { impl Actor for WebSocketActor {
@ -40,28 +46,55 @@ impl Actor for WebSocketActor {
} }
impl WsMessageSender for ws::WebsocketContext<WebSocketActor> { impl WsMessageSender for ws::WebsocketContext<WebSocketActor> {
fn send_msg(&mut self, msg: WsMsg) { fn send_msg(&mut self, msg: &WsMsg) {
self.binary(bincode::serialize(&msg).unwrap()) 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 { impl WebSocketActor {
fn handle_ws_msg(&mut self, msg: WsMsg) -> WsResult { fn broadcast(&self, msg: &WsMsg) {
use futures::executor::block_on; let user = match self.current_user.as_ref() {
Some(u) => u,
if msg != WsMsg::Ping && msg != WsMsg::Pong { _ => return,
info!("incoming message: {:?}", msg); };
self.addr
.do_send(InnerMsg::BroadcastToChannel(user.project_id, msg.clone()));
} }
let msg = fn handle_ws_msg(
match msg { &mut self,
msg: WsMsg,
ctx: &mut <WebSocketActor as Actor>::Context,
) -> WsResult {
if msg != WsMsg::Ping && msg != WsMsg::Pong {
debug!("incoming message: {:?}", msg);
}
let msg = match msg {
WsMsg::Ping => Some(WsMsg::Pong), WsMsg::Ping => Some(WsMsg::Pong),
WsMsg::Pong => Some(WsMsg::Ping), WsMsg::Pong => Some(WsMsg::Ping),
// Issues // Issues
WsMsg::IssueUpdateRequest(id, field_id, payload) => block_on( WsMsg::IssueUpdateRequest(id, field_id, payload) => match block_on(
issues::update_issue(&self.db, &self.current_user, id, field_id, payload), issues::update_issue(&self.db, &self.current_user, id, field_id, payload),
)?, ) {
Ok(Some(msg)) => {
self.broadcast(&msg);
None
}
_ => None,
},
WsMsg::IssueCreateRequest(payload) => { WsMsg::IssueCreateRequest(payload) => {
block_on(issues::add_issue(&self.db, &self.current_user, payload))? block_on(issues::add_issue(&self.db, &self.current_user, payload))?
} }
@ -84,7 +117,7 @@ impl WebSocketActor {
))?, ))?,
// auth // auth
WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid))?, WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid, ctx))?,
WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?, WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?,
WsMsg::AuthenticateRequest(email, name) => { WsMsg::AuthenticateRequest(email, name) => {
block_on(auth::authenticate(&self.db, &self.mail, name, email))? block_on(auth::authenticate(&self.db, &self.mail, name, email))?
@ -113,11 +146,17 @@ impl WebSocketActor {
payload, payload,
))?, ))?,
WsMsg::UpdateComment(payload) => block_on(comments::update_comment( WsMsg::UpdateComment(payload) => match block_on(comments::update_comment(
&self.db, &self.db,
&self.current_user, &self.current_user,
payload, payload,
))?, )) {
Ok(Some(msg)) => {
self.broadcast(&msg);
None
}
_ => None,
},
WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment( WsMsg::CommentDeleteRequest(comment_id) => block_on(comments::delete_comment(
&self.db, &self.db,
@ -137,7 +176,11 @@ impl WebSocketActor {
Ok(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 let m = match self
.db .db
.send(AuthorizeUser { .send(AuthorizeUser {
@ -148,6 +191,7 @@ impl WebSocketActor {
Ok(Ok(u)) => { Ok(Ok(u)) => {
let user: jirs_data::User = u.into(); let user: jirs_data::User = u.into();
self.current_user = Some(user.clone()); self.current_user = Some(user.clone());
self.join_channel(ctx.address().recipient()).await;
Some(WsMsg::AuthorizeLoaded(Ok(user))) Some(WsMsg::AuthorizeLoaded(Ok(user)))
} }
Ok(Err(_)) => Some(WsMsg::AuthorizeLoaded( Ok(Err(_)) => Some(WsMsg::AuthorizeLoaded(
@ -167,6 +211,27 @@ impl WebSocketActor {
}; };
Ok(Some(WsMsg::BindTokenOk(token.access_token))) 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 { 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, Ok(m) => m,
_ => return, _ => return,
}; };
match self.handle_ws_msg(msg) { match self.handle_ws_msg(msg, ctx) {
Ok(Some(msg)) => ctx.send_msg(msg), Ok(Some(msg)) => ctx.send_msg(&msg),
Err(e) => ctx.send_msg(e), 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/")] #[get("/ws/")]
@ -199,12 +356,14 @@ pub async fn index(
stream: web::Payload, stream: web::Payload,
db: Data<Addr<DbExecutor>>, db: Data<Addr<DbExecutor>>,
mail: Data<Addr<MailExecutor>>, mail: Data<Addr<MailExecutor>>,
ws_server: Data<Addr<WsServer>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
ws::start( ws::start(
WebSocketActor { WebSocketActor {
db, db,
mail, mail,
current_user: None, current_user: None,
addr: ws_server.get_ref().clone(),
}, },
&req, &req,
stream, stream,