forked from lavina/lavina
Compare commits
7 Commits
bce8b332d2
...
b0789d5457
Author | SHA1 | Date |
---|---|---|
Mikhail | b0789d5457 | |
Mikhail | b3d27e96c2 | |
Mikhail | fac20a215d | |
Nikita Vilunov | 1a21c05d7d | |
Nikita Vilunov | 43d105ab23 | |
Nikita Vilunov | f02971d407 | |
Mikhail | 381b5650bc |
|
@ -0,0 +1,2 @@
|
||||||
|
alter table messages drop column created_at;
|
||||||
|
alter table messages add column created_at datetime default "1970-01-01T00:00:00Z";
|
|
@ -18,7 +18,7 @@ use tracing::{Instrument, Span};
|
||||||
|
|
||||||
use crate::clustering::room::*;
|
use crate::clustering::room::*;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::room::{RoomHandle, RoomId, RoomInfo};
|
use crate::room::{RoomHandle, RoomId, RoomInfo, StoredMessage};
|
||||||
use crate::table::{AnonTable, Key as AnonKey};
|
use crate::table::{AnonTable, Key as AnonKey};
|
||||||
use crate::LavinaCore;
|
use crate::LavinaCore;
|
||||||
|
|
||||||
|
@ -111,6 +111,18 @@ impl PlayerConnection {
|
||||||
Ok(deferred.await?)
|
Ok(deferred.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "PlayerConnection::get_room_message_history")]
|
||||||
|
pub async fn get_room_message_history(&self, room_id: &RoomId, limit: u32) -> Result<Vec<StoredMessage>> {
|
||||||
|
let (promise, deferred) = oneshot();
|
||||||
|
let cmd = ClientCommand::GetRoomHistory {
|
||||||
|
room_id: room_id.clone(),
|
||||||
|
promise,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await;
|
||||||
|
Ok(deferred.await?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler in [Player::send_dialog_message].
|
/// Handler in [Player::send_dialog_message].
|
||||||
#[tracing::instrument(skip(self, body), name = "PlayerConnection::send_dialog_message")]
|
#[tracing::instrument(skip(self, body), name = "PlayerConnection::send_dialog_message")]
|
||||||
pub async fn send_dialog_message(&self, recipient: PlayerId, body: Str) -> Result<()> {
|
pub async fn send_dialog_message(&self, recipient: PlayerId, body: Str) -> Result<()> {
|
||||||
|
@ -212,6 +224,11 @@ pub enum ClientCommand {
|
||||||
recipient: PlayerId,
|
recipient: PlayerId,
|
||||||
promise: Promise<GetInfoResult>,
|
promise: Promise<GetInfoResult>,
|
||||||
},
|
},
|
||||||
|
GetRoomHistory {
|
||||||
|
room_id: RoomId,
|
||||||
|
promise: Promise<Vec<StoredMessage>>,
|
||||||
|
limit: u32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum GetInfoResult {
|
pub enum GetInfoResult {
|
||||||
|
@ -402,6 +419,7 @@ impl Player {
|
||||||
} else {
|
} else {
|
||||||
let room = self.services.rooms.get_room(&self.services, &room_id).await;
|
let room = self.services.rooms.get_room(&self.services, &room_id).await;
|
||||||
if let Some(room) = room {
|
if let Some(room) = room {
|
||||||
|
room.subscribe(&self.player_id, self.handle.clone()).await;
|
||||||
self.my_rooms.insert(room_id, RoomRef::Local(room));
|
self.my_rooms.insert(room_id, RoomRef::Local(room));
|
||||||
} else {
|
} else {
|
||||||
tracing::error!("Room #{room_id:?} not found");
|
tracing::error!("Room #{room_id:?} not found");
|
||||||
|
@ -509,6 +527,14 @@ impl Player {
|
||||||
let result = self.check_user_existence(recipient).await;
|
let result = self.check_user_existence(recipient).await;
|
||||||
let _ = promise.send(result);
|
let _ = promise.send(result);
|
||||||
}
|
}
|
||||||
|
ClientCommand::GetRoomHistory {
|
||||||
|
room_id,
|
||||||
|
promise,
|
||||||
|
limit,
|
||||||
|
} => {
|
||||||
|
let result = self.get_room_history(room_id, limit).await;
|
||||||
|
let _ = promise.send(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,6 +583,23 @@ impl Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "Player::retrieve_room_history")]
|
||||||
|
async fn get_room_history(&mut self, room_id: RoomId, limit: u32) -> Vec<StoredMessage> {
|
||||||
|
let room = self.my_rooms.get(&room_id);
|
||||||
|
if let Some(room) = room {
|
||||||
|
match room {
|
||||||
|
RoomRef::Local(room) => room.get_message_history(&self.services, limit).await,
|
||||||
|
RoomRef::Remote { node_id } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::error!("Room with ID {room_id:?} not found");
|
||||||
|
// todo: return error
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), name = "Player::leave_room")]
|
#[tracing::instrument(skip(self), name = "Player::leave_room")]
|
||||||
async fn leave_room(&mut self, connection_id: ConnectionId, room_id: RoomId) {
|
async fn leave_room(&mut self, connection_id: ConnectionId, room_id: RoomId) {
|
||||||
let room = self.my_rooms.remove(&room_id);
|
let room = self.my_rooms.remove(&room_id);
|
||||||
|
@ -593,7 +636,7 @@ impl Player {
|
||||||
body: Str,
|
body: Str,
|
||||||
) -> SendMessageResult {
|
) -> SendMessageResult {
|
||||||
let Some(room) = self.my_rooms.get(&room_id) else {
|
let Some(room) = self.my_rooms.get(&room_id) else {
|
||||||
tracing::info!("no room found");
|
tracing::info!("Room with ID {room_id:?} not found");
|
||||||
return SendMessageResult::NoSuchRoom;
|
return SendMessageResult::NoSuchRoom;
|
||||||
};
|
};
|
||||||
let created_at = Utc::now();
|
let created_at = Utc::now();
|
||||||
|
@ -632,7 +675,7 @@ impl Player {
|
||||||
#[tracing::instrument(skip(self, new_topic), name = "Player::change_room_topic")]
|
#[tracing::instrument(skip(self, new_topic), name = "Player::change_room_topic")]
|
||||||
async fn change_room_topic(&mut self, connection_id: ConnectionId, room_id: RoomId, new_topic: Str) {
|
async fn change_room_topic(&mut self, connection_id: ConnectionId, room_id: RoomId, new_topic: Str) {
|
||||||
let Some(room) = self.my_rooms.get(&room_id) else {
|
let Some(room) = self.my_rooms.get(&room_id) else {
|
||||||
tracing::info!("no room found");
|
tracing::info!("Room with ID {room_id:?} not found");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match room {
|
match room {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
|
||||||
use crate::repo::Storage;
|
use crate::repo::Storage;
|
||||||
use crate::room::RoomId;
|
use crate::room::{RoomId, StoredMessage};
|
||||||
|
|
||||||
#[derive(FromRow)]
|
#[derive(FromRow)]
|
||||||
pub struct StoredRoom {
|
pub struct StoredRoom {
|
||||||
|
@ -29,6 +29,37 @@ impl Storage {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "Storage::retrieve_room_message_history")]
|
||||||
|
pub async fn get_room_message_history(&self, room_id: u32, limit: u32) -> Result<Vec<StoredMessage>> {
|
||||||
|
let mut executor = self.conn.lock().await;
|
||||||
|
let res = sqlx::query_as(
|
||||||
|
"
|
||||||
|
select
|
||||||
|
messages.id as id,
|
||||||
|
content,
|
||||||
|
created_at,
|
||||||
|
users.name as author_name
|
||||||
|
from
|
||||||
|
messages
|
||||||
|
join
|
||||||
|
users
|
||||||
|
on messages.author_id = users.id
|
||||||
|
where
|
||||||
|
room_id = ?
|
||||||
|
order by
|
||||||
|
messages.id
|
||||||
|
limit ?;
|
||||||
|
",
|
||||||
|
// todo: implement limit
|
||||||
|
)
|
||||||
|
.bind(room_id)
|
||||||
|
.bind(limit)
|
||||||
|
.fetch_all(&mut *executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self, topic), name = "Storage::create_new_room")]
|
#[tracing::instrument(skip(self, topic), name = "Storage::create_new_room")]
|
||||||
pub async fn create_new_room(&self, name: &str, topic: &str) -> Result<u32> {
|
pub async fn create_new_room(&self, name: &str, topic: &str) -> Result<u32> {
|
||||||
let mut executor = self.conn.lock().await;
|
let mut executor = self.conn.lock().await;
|
||||||
|
@ -71,7 +102,7 @@ impl Storage {
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(content)
|
.bind(content)
|
||||||
.bind(author_id)
|
.bind(author_id)
|
||||||
.bind(created_at.to_string())
|
.bind(created_at)
|
||||||
.bind(room_id)
|
.bind(room_id)
|
||||||
.execute(&mut *executor)
|
.execute(&mut *executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -174,6 +205,6 @@ impl Storage {
|
||||||
.fetch_all(&mut *executor)
|
.fetch_all(&mut *executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
res.into_iter().map(|(room_id,)| RoomId::from(room_id)).collect()
|
res.into_iter().map(|(room_id,)| RoomId::try_from(room_id)).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,20 @@ use std::{collections::HashMap, hash::Hash, sync::Arc};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use prometheus::{IntGauge, Registry as MetricRegistry};
|
use prometheus::{IntGauge, Registry as MetricRegistry};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use sqlx::sqlite::SqliteRow;
|
||||||
|
use sqlx::{FromRow, Row};
|
||||||
use tokio::sync::RwLock as AsyncRwLock;
|
use tokio::sync::RwLock as AsyncRwLock;
|
||||||
|
|
||||||
use crate::player::{PlayerHandle, PlayerId, Updates};
|
use crate::player::{PlayerHandle, PlayerId, Updates};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::Services;
|
use crate::{LavinaCore, Services};
|
||||||
|
|
||||||
/// Opaque room id
|
/// Opaque room id
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
pub struct RoomId(Str);
|
pub struct RoomId(Str);
|
||||||
|
|
||||||
impl RoomId {
|
impl RoomId {
|
||||||
pub fn from(str: impl Into<Str>) -> Result<RoomId> {
|
pub fn try_from(str: impl Into<Str>) -> Result<RoomId> {
|
||||||
let bytes = str.into();
|
let bytes = str.into();
|
||||||
if bytes.len() > 32 {
|
if bytes.len() > 32 {
|
||||||
return Err(anyhow::Error::msg("Room name cannot be longer than 32 symbols"));
|
return Err(anyhow::Error::msg("Room name cannot be longer than 32 symbols"));
|
||||||
|
@ -158,6 +160,10 @@ impl RoomHandle {
|
||||||
lock.broadcast_update(update, player_id).await;
|
lock.broadcast_update(update, player_id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_message_history(&self, services: &Services, limit: u32) -> Vec<StoredMessage> {
|
||||||
|
return services.storage.get_room_message_history(self.0.read().await.storage_id, limit).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), name = "RoomHandle::unsubscribe")]
|
#[tracing::instrument(skip(self), name = "RoomHandle::unsubscribe")]
|
||||||
pub async fn unsubscribe(&self, player_id: &PlayerId) {
|
pub async fn unsubscribe(&self, player_id: &PlayerId) {
|
||||||
let mut lock = self.0.write().await;
|
let mut lock = self.0.write().await;
|
||||||
|
@ -279,3 +285,11 @@ pub struct RoomInfo {
|
||||||
pub members: Vec<PlayerId>,
|
pub members: Vec<PlayerId>,
|
||||||
pub topic: Str,
|
pub topic: Str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromRow)]
|
||||||
|
pub struct StoredMessage {
|
||||||
|
pub id: u32,
|
||||||
|
pub author_name: String,
|
||||||
|
pub content: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
|
@ -6,5 +6,6 @@ bitflags! {
|
||||||
const None = 0;
|
const None = 0;
|
||||||
const Sasl = 1 << 0;
|
const Sasl = 1 << 0;
|
||||||
const ServerTime = 1 << 1;
|
const ServerTime = 1 << 1;
|
||||||
|
const ChatHistory = 1 << 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ use proto_irc::client::{client_message, ClientMessage};
|
||||||
use proto_irc::server::CapSubBody;
|
use proto_irc::server::CapSubBody;
|
||||||
use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
|
use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
|
||||||
use proto_irc::user::PrefixedNick;
|
use proto_irc::user::PrefixedNick;
|
||||||
use proto_irc::{Chan, Recipient, Tag};
|
use proto_irc::{ChannelName, Recipient, Tag};
|
||||||
use sasl::AuthBody;
|
use sasl::AuthBody;
|
||||||
mod cap;
|
mod cap;
|
||||||
use handler::Handler;
|
use handler::Handler;
|
||||||
|
@ -140,7 +140,7 @@ impl RegistrationState {
|
||||||
sender: Some(config.server_name.clone().into()),
|
sender: Some(config.server_name.clone().into()),
|
||||||
body: ServerMessageBody::Cap {
|
body: ServerMessageBody::Cap {
|
||||||
target: self.future_nickname.clone().unwrap_or_else(|| "*".into()),
|
target: self.future_nickname.clone().unwrap_or_else(|| "*".into()),
|
||||||
subcmd: CapSubBody::Ls("sasl=PLAIN server-time".into()),
|
subcmd: CapSubBody::Ls("sasl=PLAIN server-time draft/chathistory".into()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
.write_async(writer)
|
.write_async(writer)
|
||||||
|
@ -167,6 +167,13 @@ impl RegistrationState {
|
||||||
self.enabled_capabilities |= Capabilities::ServerTime;
|
self.enabled_capabilities |= Capabilities::ServerTime;
|
||||||
}
|
}
|
||||||
acked.push(cap);
|
acked.push(cap);
|
||||||
|
} else if &*cap.name == "draft/chathistory" {
|
||||||
|
if cap.to_disable {
|
||||||
|
self.enabled_capabilities &= !Capabilities::ChatHistory;
|
||||||
|
} else {
|
||||||
|
self.enabled_capabilities |= Capabilities::ChatHistory;
|
||||||
|
}
|
||||||
|
acked.push(cap);
|
||||||
} else {
|
} else {
|
||||||
naked.push(cap);
|
naked.push(cap);
|
||||||
}
|
}
|
||||||
|
@ -484,7 +491,14 @@ async fn handle_registered_socket<'a>(
|
||||||
|
|
||||||
let rooms_list = connection.get_rooms().await?;
|
let rooms_list = connection.get_rooms().await?;
|
||||||
for room in &rooms_list {
|
for room in &rooms_list {
|
||||||
produce_on_join_cmd_messages(&config, &user, &Chan::Global(room.id.as_inner().clone()), room, writer).await?;
|
produce_on_join_cmd_messages(
|
||||||
|
&config,
|
||||||
|
&user,
|
||||||
|
&ChannelName::Global(room.id.as_inner().clone()),
|
||||||
|
room,
|
||||||
|
writer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
|
@ -569,7 +583,7 @@ async fn handle_update(
|
||||||
if player_id == &new_member_id {
|
if player_id == &new_member_id {
|
||||||
if let Some(room) = core.get_room(&room_id).await {
|
if let Some(room) = core.get_room(&room_id).await {
|
||||||
let room_info = room.get_room_info().await;
|
let room_info = room.get_room_info().await;
|
||||||
let chan = Chan::Global(room_id.as_inner().clone());
|
let chan = ChannelName::Global(room_id.as_inner().clone());
|
||||||
produce_on_join_cmd_messages(&config, &user, &chan, &room_info, writer).await?;
|
produce_on_join_cmd_messages(&config, &user, &chan, &room_info, writer).await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -579,7 +593,7 @@ async fn handle_update(
|
||||||
ServerMessage {
|
ServerMessage {
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
sender: Some(new_member_id.as_inner().clone()),
|
sender: Some(new_member_id.as_inner().clone()),
|
||||||
body: ServerMessageBody::Join(Chan::Global(room_id.as_inner().clone())),
|
body: ServerMessageBody::Join(ChannelName::Global(room_id.as_inner().clone())),
|
||||||
}
|
}
|
||||||
.write_async(writer)
|
.write_async(writer)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -593,7 +607,7 @@ async fn handle_update(
|
||||||
ServerMessage {
|
ServerMessage {
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
sender: Some(former_member_id.as_inner().clone()),
|
sender: Some(former_member_id.as_inner().clone()),
|
||||||
body: ServerMessageBody::Part(Chan::Global(room_id.as_inner().clone())),
|
body: ServerMessageBody::Part(ChannelName::Global(room_id.as_inner().clone())),
|
||||||
}
|
}
|
||||||
.write_async(writer)
|
.write_async(writer)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -617,7 +631,7 @@ async fn handle_update(
|
||||||
tags,
|
tags,
|
||||||
sender: Some(author_id.as_inner().clone()),
|
sender: Some(author_id.as_inner().clone()),
|
||||||
body: ServerMessageBody::PrivateMessage {
|
body: ServerMessageBody::PrivateMessage {
|
||||||
target: Recipient::Chan(Chan::Global(room_id.as_inner().clone())),
|
target: Recipient::Chan(ChannelName::Global(room_id.as_inner().clone())),
|
||||||
body: body.clone(),
|
body: body.clone(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -631,7 +645,7 @@ async fn handle_update(
|
||||||
sender: Some(config.server_name.clone()),
|
sender: Some(config.server_name.clone()),
|
||||||
body: ServerMessageBody::N332Topic {
|
body: ServerMessageBody::N332Topic {
|
||||||
client: user.nickname.clone(),
|
client: user.nickname.clone(),
|
||||||
chat: Chan::Global(room_id.as_inner().clone()),
|
chat: ChannelName::Global(room_id.as_inner().clone()),
|
||||||
topic: new_topic,
|
topic: new_topic,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -644,7 +658,7 @@ async fn handle_update(
|
||||||
ServerMessage {
|
ServerMessage {
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
sender: Some(player_id.as_inner().clone()),
|
sender: Some(player_id.as_inner().clone()),
|
||||||
body: ServerMessageBody::Part(Chan::Global(room_id.as_inner().clone())),
|
body: ServerMessageBody::Part(ChannelName::Global(room_id.as_inner().clone())),
|
||||||
}
|
}
|
||||||
.write_async(writer)
|
.write_async(writer)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -719,8 +733,8 @@ async fn handle_incoming_message(
|
||||||
handle_part(config, user, user_handle, &chan, writer).await?;
|
handle_part(config, user, user_handle, &chan, writer).await?;
|
||||||
}
|
}
|
||||||
ClientMessage::PrivateMessage { recipient, body } => match recipient {
|
ClientMessage::PrivateMessage { recipient, body } => match recipient {
|
||||||
Recipient::Chan(Chan::Global(chan)) => {
|
Recipient::Chan(ChannelName::Global(chan)) => {
|
||||||
let room_id = RoomId::from(chan)?;
|
let room_id = RoomId::try_from(chan)?;
|
||||||
user_handle.send_message(room_id, body).await?;
|
user_handle.send_message(room_id, body).await?;
|
||||||
}
|
}
|
||||||
Recipient::Nick(nick) => {
|
Recipient::Nick(nick) => {
|
||||||
|
@ -731,15 +745,15 @@ async fn handle_incoming_message(
|
||||||
},
|
},
|
||||||
ClientMessage::Topic { chan, topic } => {
|
ClientMessage::Topic { chan, topic } => {
|
||||||
match chan {
|
match chan {
|
||||||
Chan::Global(chan) => {
|
ChannelName::Global(chan) => {
|
||||||
let room_id = RoomId::from(chan)?;
|
let room_id = RoomId::try_from(chan)?;
|
||||||
user_handle.change_topic(room_id.clone(), topic.clone()).await?;
|
user_handle.change_topic(room_id.clone(), topic.clone()).await?;
|
||||||
ServerMessage {
|
ServerMessage {
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
sender: Some(config.server_name.clone()),
|
sender: Some(config.server_name.clone()),
|
||||||
body: ServerMessageBody::N332Topic {
|
body: ServerMessageBody::N332Topic {
|
||||||
client: user.nickname.clone(),
|
client: user.nickname.clone(),
|
||||||
chat: Chan::Global(room_id.as_inner().clone()),
|
chat: ChannelName::Global(room_id.as_inner().clone()),
|
||||||
topic,
|
topic,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -747,7 +761,7 @@ async fn handle_incoming_message(
|
||||||
.await?;
|
.await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
}
|
}
|
||||||
Chan::Local(_) => {}
|
ChannelName::Local(_) => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ClientMessage::Who { target } => match &target {
|
ClientMessage::Who { target } => match &target {
|
||||||
|
@ -773,8 +787,8 @@ async fn handle_incoming_message(
|
||||||
.await?;
|
.await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
}
|
}
|
||||||
Recipient::Chan(Chan::Global(chan)) => {
|
Recipient::Chan(ChannelName::Global(chan)) => {
|
||||||
let room = core.get_room(&RoomId::from(chan.clone())?).await;
|
let room = core.get_room(&RoomId::try_from(chan.clone())?).await;
|
||||||
if let Some(room) = room {
|
if let Some(room) = room {
|
||||||
let room_info = room.get_room_info().await;
|
let room_info = room.get_room_info().await;
|
||||||
for member in room_info.members {
|
for member in room_info.members {
|
||||||
|
@ -800,7 +814,7 @@ async fn handle_incoming_message(
|
||||||
.await?;
|
.await?;
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
}
|
}
|
||||||
Recipient::Chan(Chan::Local(_)) => {
|
Recipient::Chan(ChannelName::Local(_)) => {
|
||||||
log::warn!("Local chans not supported");
|
log::warn!("Local chans not supported");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -853,6 +867,38 @@ async fn handle_incoming_message(
|
||||||
log::info!("Received QUIT");
|
log::info!("Received QUIT");
|
||||||
return Ok(HandleResult::Leave);
|
return Ok(HandleResult::Leave);
|
||||||
}
|
}
|
||||||
|
ClientMessage::ChatHistory { chan, limit } => {
|
||||||
|
let channel_name = match chan.clone() {
|
||||||
|
ChannelName::Global(chan) => chan,
|
||||||
|
ChannelName::Local(chan) => chan,
|
||||||
|
};
|
||||||
|
let room = core.get_room(&RoomId::try_from(channel_name.clone())?).await;
|
||||||
|
if let Some(room) = room {
|
||||||
|
let room_info = room.get_room_info().await;
|
||||||
|
let messages = user_handle.get_room_message_history(&room_info.id, limit).await?;
|
||||||
|
for message in messages {
|
||||||
|
let mut tags = vec![];
|
||||||
|
if user.enabled_capabilities.contains(Capabilities::ServerTime) {
|
||||||
|
let tag = Tag {
|
||||||
|
key: "time".into(),
|
||||||
|
value: Some(message.created_at.to_rfc3339_opts(SecondsFormat::Millis, true).into()),
|
||||||
|
};
|
||||||
|
tags.push(tag);
|
||||||
|
}
|
||||||
|
ServerMessage {
|
||||||
|
tags,
|
||||||
|
sender: Some(message.author_name.into()),
|
||||||
|
body: ServerMessageBody::PrivateMessage {
|
||||||
|
target: Recipient::Chan(chan.clone()),
|
||||||
|
body: message.content.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.write_async(writer)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
writer.flush().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
cmd => {
|
cmd => {
|
||||||
log::warn!("Not implemented handler for client command: {cmd:?}");
|
log::warn!("Not implemented handler for client command: {cmd:?}");
|
||||||
}
|
}
|
||||||
|
@ -888,12 +934,12 @@ async fn handle_join(
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
user: &RegisteredUser,
|
user: &RegisteredUser,
|
||||||
user_handle: &mut PlayerConnection,
|
user_handle: &mut PlayerConnection,
|
||||||
chan: &Chan,
|
chan: &ChannelName,
|
||||||
writer: &mut (impl AsyncWrite + Unpin),
|
writer: &mut (impl AsyncWrite + Unpin),
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match chan {
|
match chan {
|
||||||
Chan::Global(chan_name) => {
|
ChannelName::Global(chan_name) => {
|
||||||
let room_id = RoomId::from(chan_name.clone())?;
|
let room_id = RoomId::try_from(chan_name.clone())?;
|
||||||
match user_handle.join_room(room_id).await? {
|
match user_handle.join_room(room_id).await? {
|
||||||
JoinResult::Success(room_info) => {
|
JoinResult::Success(room_info) => {
|
||||||
produce_on_join_cmd_messages(&config, &user, chan, &room_info, writer).await?;
|
produce_on_join_cmd_messages(&config, &user, chan, &room_info, writer).await?;
|
||||||
|
@ -917,7 +963,7 @@ async fn handle_join(
|
||||||
}
|
}
|
||||||
writer.flush().await?;
|
writer.flush().await?;
|
||||||
}
|
}
|
||||||
Chan::Local(_) => {
|
ChannelName::Local(_) => {
|
||||||
// TODO handle join attempts to local chans with an error, we don't support these
|
// TODO handle join attempts to local chans with an error, we don't support these
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -928,16 +974,16 @@ async fn handle_part(
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
user: &RegisteredUser,
|
user: &RegisteredUser,
|
||||||
user_handle: &mut PlayerConnection,
|
user_handle: &mut PlayerConnection,
|
||||||
chan: &Chan,
|
chan: &ChannelName,
|
||||||
writer: &mut (impl AsyncWrite + Unpin),
|
writer: &mut (impl AsyncWrite + Unpin),
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Chan::Global(chan_name) = chan {
|
if let ChannelName::Global(chan_name) = chan {
|
||||||
let room_id = RoomId::from(chan_name.clone())?;
|
let room_id = RoomId::try_from(chan_name.clone())?;
|
||||||
user_handle.leave_room(room_id).await?;
|
user_handle.leave_room(room_id).await?;
|
||||||
ServerMessage {
|
ServerMessage {
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
sender: Some(user.nickname.clone()),
|
sender: Some(user.nickname.clone()),
|
||||||
body: ServerMessageBody::Part(Chan::Global(chan_name.clone())),
|
body: ServerMessageBody::Part(ChannelName::Global(chan_name.clone())),
|
||||||
}
|
}
|
||||||
.write_async(writer)
|
.write_async(writer)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -951,7 +997,7 @@ async fn handle_part(
|
||||||
async fn produce_on_join_cmd_messages(
|
async fn produce_on_join_cmd_messages(
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
user: &RegisteredUser,
|
user: &RegisteredUser,
|
||||||
chan: &Chan,
|
chan: &ChannelName,
|
||||||
room_info: &RoomInfo,
|
room_info: &RoomInfo,
|
||||||
writer: &mut (impl AsyncWrite + Unpin),
|
writer: &mut (impl AsyncWrite + Unpin),
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
|
@ -619,14 +619,14 @@ async fn server_time_capability() -> Result<()> {
|
||||||
|
|
||||||
server.core.create_player(&PlayerId::from("some_guy")?).await?;
|
server.core.create_player(&PlayerId::from("some_guy")?).await?;
|
||||||
let mut conn = server.core.connect_to_player(&PlayerId::from("some_guy").unwrap()).await;
|
let mut conn = server.core.connect_to_player(&PlayerId::from("some_guy").unwrap()).await;
|
||||||
let res = conn.join_room(RoomId::from("test").unwrap()).await?;
|
let res = conn.join_room(RoomId::try_from("test").unwrap()).await?;
|
||||||
let JoinResult::Success(_) = res else {
|
let JoinResult::Success(_) = res else {
|
||||||
panic!("Failed to join room");
|
panic!("Failed to join room");
|
||||||
};
|
};
|
||||||
|
|
||||||
s.expect(":some_guy JOIN #test").await?;
|
s.expect(":some_guy JOIN #test").await?;
|
||||||
|
|
||||||
let SendMessageResult::Success(res) = conn.send_message(RoomId::from("test").unwrap(), "Hello".into()).await?
|
let SendMessageResult::Success(res) = conn.send_message(RoomId::try_from("test").unwrap(), "Hello".into()).await?
|
||||||
else {
|
else {
|
||||||
panic!("Failed to send message");
|
panic!("Failed to send message");
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::proto::IqClientBody;
|
||||||
use crate::XmppConnection;
|
use crate::XmppConnection;
|
||||||
|
|
||||||
impl<'a> XmppConnection<'a> {
|
impl<'a> XmppConnection<'a> {
|
||||||
|
#[tracing::instrument(skip(self, output, iq), name = "XmppConnection::handle_iq")]
|
||||||
pub async fn handle_iq(&self, output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>) {
|
pub async fn handle_iq(&self, output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>) {
|
||||||
match iq.body {
|
match iq.body {
|
||||||
IqClientBody::Bind(req) => {
|
IqClientBody::Bind(req) => {
|
||||||
|
@ -112,6 +113,7 @@ impl<'a> XmppConnection<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "XmppConnection::bind")]
|
||||||
pub(crate) async fn bind(&self, req: &BindRequest) -> BindResponse {
|
pub(crate) async fn bind(&self, req: &BindRequest) -> BindResponse {
|
||||||
BindResponse(Jid {
|
BindResponse(Jid {
|
||||||
name: Some(self.user.xmpp_name.clone()),
|
name: Some(self.user.xmpp_name.clone()),
|
||||||
|
@ -120,6 +122,7 @@ impl<'a> XmppConnection<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "XmppConnection::disco_info")]
|
||||||
async fn disco_info(&self, to: Option<&Jid>, req: &InfoQuery) -> Result<InfoQuery, IqError> {
|
async fn disco_info(&self, to: Option<&Jid>, req: &InfoQuery) -> Result<InfoQuery, IqError> {
|
||||||
let identity;
|
let identity;
|
||||||
let feature;
|
let feature;
|
||||||
|
@ -163,7 +166,7 @@ impl<'a> XmppConnection<'a> {
|
||||||
server,
|
server,
|
||||||
resource: None,
|
resource: None,
|
||||||
}) if server.0 == self.hostname_rooms => {
|
}) if server.0 == self.hostname_rooms => {
|
||||||
let room_id = RoomId::from(room_name.0.clone()).unwrap();
|
let room_id = RoomId::try_from(room_name.0.clone()).unwrap();
|
||||||
let Some(_) = self.core.get_room(&room_id).await else {
|
let Some(_) = self.core.get_room(&room_id).await else {
|
||||||
// TODO should return item-not-found
|
// TODO should return item-not-found
|
||||||
// example:
|
// example:
|
||||||
|
@ -199,6 +202,7 @@ impl<'a> XmppConnection<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self, core), name = "XmppConnection::disco_items")]
|
||||||
async fn disco_items(&self, to: Option<&Jid>, req: &ItemQuery, core: &LavinaCore) -> ItemQuery {
|
async fn disco_items(&self, to: Option<&Jid>, req: &ItemQuery, core: &LavinaCore) -> ItemQuery {
|
||||||
let item = match to {
|
let item = match to {
|
||||||
Some(Jid {
|
Some(Jid {
|
||||||
|
|
|
@ -12,6 +12,7 @@ use proto_xmpp::xml::{Ignore, ToXml};
|
||||||
use crate::XmppConnection;
|
use crate::XmppConnection;
|
||||||
|
|
||||||
impl<'a> XmppConnection<'a> {
|
impl<'a> XmppConnection<'a> {
|
||||||
|
#[tracing::instrument(skip(self, output, m), name = "XmppConnection::message")]
|
||||||
pub async fn handle_message(&mut self, output: &mut Vec<Event<'static>>, m: Message<Ignore>) -> Result<()> {
|
pub async fn handle_message(&mut self, output: &mut Vec<Event<'static>>, m: Message<Ignore>) -> Result<()> {
|
||||||
if let Some(Jid {
|
if let Some(Jid {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
|
@ -21,7 +22,7 @@ impl<'a> XmppConnection<'a> {
|
||||||
{
|
{
|
||||||
if server.0.as_ref() == &*self.hostname_rooms && m.r#type == MessageType::Groupchat {
|
if server.0.as_ref() == &*self.hostname_rooms && m.r#type == MessageType::Groupchat {
|
||||||
let Some(body) = &m.body else { return Ok(()) };
|
let Some(body) = &m.body else { return Ok(()) };
|
||||||
self.user_handle.send_message(RoomId::from(name.0.clone())?, body.clone()).await?;
|
self.user_handle.send_message(RoomId::try_from(name.0.clone())?, body.clone()).await?;
|
||||||
Message::<()> {
|
Message::<()> {
|
||||||
to: Some(Jid {
|
to: Some(Jid {
|
||||||
name: Some(self.user.xmpp_name.clone()),
|
name: Some(self.user.xmpp_name.clone()),
|
||||||
|
|
|
@ -4,14 +4,15 @@ use anyhow::Result;
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
|
|
||||||
use lavina_core::room::RoomId;
|
use lavina_core::room::RoomId;
|
||||||
use proto_xmpp::bind::{Jid, Name, Server};
|
use proto_xmpp::bind::{Jid, Name, Resource, Server};
|
||||||
use proto_xmpp::client::{Message, MessageType, Presence, Subject};
|
use proto_xmpp::client::{Message, MessageType, Presence, Subject};
|
||||||
use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem};
|
use proto_xmpp::muc::{Affiliation, Delay, Role, XUser, XUserItem, XmppHistoryMessage};
|
||||||
use proto_xmpp::xml::{Ignore, ToXml};
|
use proto_xmpp::xml::{Ignore, ToXml};
|
||||||
|
|
||||||
use crate::XmppConnection;
|
use crate::XmppConnection;
|
||||||
|
|
||||||
impl<'a> XmppConnection<'a> {
|
impl<'a> XmppConnection<'a> {
|
||||||
|
#[tracing::instrument(skip(self, output, p), name = "XmppConnection::handle_presence")]
|
||||||
pub async fn handle_presence(&mut self, output: &mut Vec<Event<'static>>, p: Presence<Ignore>) -> Result<()> {
|
pub async fn handle_presence(&mut self, output: &mut Vec<Event<'static>>, p: Presence<Ignore>) -> Result<()> {
|
||||||
match p.to {
|
match p.to {
|
||||||
None => {
|
None => {
|
||||||
|
@ -22,12 +23,42 @@ impl<'a> XmppConnection<'a> {
|
||||||
server,
|
server,
|
||||||
// resources in MUCs are members' personas – not implemented (yet?)
|
// resources in MUCs are members' personas – not implemented (yet?)
|
||||||
resource: Some(_),
|
resource: Some(_),
|
||||||
}) if server.0 == self.hostname_rooms => {
|
}) if server.0 == self.hostname_rooms => match p.r#type.as_deref() {
|
||||||
let mut response = self.muc_presence(&name).await?;
|
None => {
|
||||||
response.id = p.id;
|
self.join_muc(output, p.id, name).await?;
|
||||||
|
}
|
||||||
|
Some("unavailable") => {
|
||||||
|
self.leave_muc(output, p.id, name).await?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!("Unimplemented case")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// TODO other presence cases
|
||||||
|
let response = Presence::<()>::default();
|
||||||
|
response.serialize(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_muc(&mut self, output: &mut Vec<Event<'static>>, id: Option<String>, name: Name) -> Result<()> {
|
||||||
|
// Response presence
|
||||||
|
let mut muc_presence = self.retrieve_muc_presence(&name).await?;
|
||||||
|
muc_presence.id = id;
|
||||||
|
muc_presence.serialize(output);
|
||||||
|
|
||||||
|
// N last messages from the room history before the user joined
|
||||||
|
let messages = self.retrieve_message_history(&name).await?;
|
||||||
|
for message in messages {
|
||||||
|
message.serialize(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subject is the last stanza sent during a MUC join process.
|
||||||
let subject = Message::<()> {
|
let subject = Message::<()> {
|
||||||
from: Some(Jid {
|
from: Some(Jid {
|
||||||
name: Some(name),
|
name: Some(name.clone()),
|
||||||
server: Server(self.hostname_rooms.clone()),
|
server: Server(self.hostname_rooms.clone()),
|
||||||
resource: None,
|
resource: None,
|
||||||
}),
|
}),
|
||||||
|
@ -43,18 +74,45 @@ impl<'a> XmppConnection<'a> {
|
||||||
body: None,
|
body: None,
|
||||||
custom: vec![],
|
custom: vec![],
|
||||||
};
|
};
|
||||||
response.serialize(output);
|
|
||||||
subject.serialize(output);
|
subject.serialize(output);
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// TODO other presence cases
|
|
||||||
let response = Presence::<()>::default();
|
|
||||||
response.serialize(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn leave_muc(&mut self, output: &mut Vec<Event<'static>>, id: Option<String>, name: Name) -> Result<()> {
|
||||||
|
self.user_handle.leave_room(RoomId::try_from(name.0.clone())?).await?;
|
||||||
|
let response = Presence {
|
||||||
|
id,
|
||||||
|
to: Some(Jid {
|
||||||
|
name: Some(self.user.xmpp_name.clone()),
|
||||||
|
server: Server(self.hostname.clone()),
|
||||||
|
resource: Some(self.user.xmpp_resource.clone()),
|
||||||
|
}),
|
||||||
|
from: Some(Jid {
|
||||||
|
name: Some(name.clone()),
|
||||||
|
server: Server(self.hostname_rooms.clone()),
|
||||||
|
resource: Some(self.user.xmpp_muc_name.clone()),
|
||||||
|
}),
|
||||||
|
r#type: Some("unavailable".into()),
|
||||||
|
custom: vec![XUser {
|
||||||
|
item: XUserItem {
|
||||||
|
affiliation: Affiliation::Member,
|
||||||
|
role: Role::None,
|
||||||
|
jid: Jid {
|
||||||
|
name: Some(self.user.xmpp_name.clone()),
|
||||||
|
server: Server(self.hostname.clone()),
|
||||||
|
resource: Some(self.user.xmpp_resource.clone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
self_presence: true,
|
||||||
|
just_created: false,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
response.serialize(output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self, output, r#type), name = "XmppConnection::self_presence")]
|
||||||
async fn self_presence(&mut self, output: &mut Vec<Event<'static>>, r#type: Option<&str>) {
|
async fn self_presence(&mut self, output: &mut Vec<Event<'static>>, r#type: Option<&str>) {
|
||||||
match r#type {
|
match r#type {
|
||||||
Some("unavailable") => {
|
Some("unavailable") => {
|
||||||
|
@ -82,9 +140,9 @@ impl<'a> XmppConnection<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: return Presence and serialize on the outside.
|
#[tracing::instrument(skip(self), name = "XmppConnection::retrieve_muc_presence")]
|
||||||
async fn muc_presence(&mut self, name: &Name) -> Result<(Presence<XUser>)> {
|
async fn retrieve_muc_presence(&mut self, name: &Name) -> Result<Presence<XUser>> {
|
||||||
let a = self.user_handle.join_room(RoomId::from(name.0.clone())?).await?;
|
let _ = self.user_handle.join_room(RoomId::try_from(name.0.clone())?).await?;
|
||||||
// TODO handle bans
|
// TODO handle bans
|
||||||
let response = Presence {
|
let response = Presence {
|
||||||
to: Some(Jid {
|
to: Some(Jid {
|
||||||
|
@ -114,21 +172,62 @@ impl<'a> XmppConnection<'a> {
|
||||||
};
|
};
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// todo: set up so that the user has been previously joined.
|
/// Retrieve a room's message history. The output can be serialized into a stream of XML stanzas.
|
||||||
// todo: first call to muc_presence is OK, next one is OK too.
|
///
|
||||||
|
/// Example in [XmppHistoryMessage]'s docs.
|
||||||
|
#[tracing::instrument(skip(self), name = "XmppConnection::retrieve_message_history")]
|
||||||
|
async fn retrieve_message_history(&self, room_name: &Name) -> Result<Vec<XmppHistoryMessage>> {
|
||||||
|
let room_id = RoomId::try_from(room_name.0.clone())?;
|
||||||
|
let history_messages = self.user_handle.get_room_message_history(&room_id, 50).await?;
|
||||||
|
let mut response = vec![];
|
||||||
|
|
||||||
|
for history_message in history_messages.into_iter() {
|
||||||
|
response.push(XmppHistoryMessage {
|
||||||
|
id: history_message.id.to_string(),
|
||||||
|
to: Jid {
|
||||||
|
name: Option::from(Name(self.user.xmpp_muc_name.0.clone().into())),
|
||||||
|
server: Server(self.hostname.clone()),
|
||||||
|
resource: None,
|
||||||
|
},
|
||||||
|
from: Jid {
|
||||||
|
name: Option::from(room_name.clone()),
|
||||||
|
server: Server(self.hostname_rooms.clone()),
|
||||||
|
resource: Option::from(Resource(history_message.author_name.clone().into())),
|
||||||
|
},
|
||||||
|
delay: Delay {
|
||||||
|
from: Jid {
|
||||||
|
name: Option::from(Name(history_message.author_name.clone().into())),
|
||||||
|
server: Server(self.hostname_rooms.clone()),
|
||||||
|
resource: None,
|
||||||
|
},
|
||||||
|
stamp: history_message.created_at.to_rfc3339(),
|
||||||
|
},
|
||||||
|
body: history_message.content.clone(),
|
||||||
|
});
|
||||||
|
tracing::info!(
|
||||||
|
"Retrieved message: {:?} {:?}",
|
||||||
|
history_message.author_name,
|
||||||
|
history_message.content.clone()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::testkit::{expect_user_authenticated, TestServer};
|
|
||||||
use crate::Authenticated;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use lavina_core::player::PlayerId;
|
use lavina_core::player::PlayerId;
|
||||||
use proto_xmpp::bind::{Jid, Name, Resource, Server};
|
use proto_xmpp::bind::{Jid, Name, Resource, Server};
|
||||||
use proto_xmpp::client::Presence;
|
use proto_xmpp::client::Presence;
|
||||||
use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem};
|
use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem};
|
||||||
|
|
||||||
|
use crate::testkit::{expect_user_authenticated, TestServer};
|
||||||
|
use crate::Authenticated;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_muc_joining() -> Result<()> {
|
async fn test_muc_joining() -> Result<()> {
|
||||||
let server = TestServer::start().await.unwrap();
|
let server = TestServer::start().await.unwrap();
|
||||||
|
@ -146,7 +245,7 @@ mod tests {
|
||||||
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
||||||
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
||||||
|
|
||||||
let response = conn.muc_presence(&user.xmpp_name).await.unwrap();
|
let muc_presence = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap();
|
||||||
let expected = Presence {
|
let expected = Presence {
|
||||||
to: Some(Jid {
|
to: Some(Jid {
|
||||||
name: Some(conn.user.xmpp_name.clone()),
|
name: Some(conn.user.xmpp_name.clone()),
|
||||||
|
@ -173,7 +272,7 @@ mod tests {
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(expected, response);
|
assert_eq!(expected, muc_presence);
|
||||||
|
|
||||||
server.shutdown().await.unwrap();
|
server.shutdown().await.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -198,7 +297,7 @@ mod tests {
|
||||||
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
||||||
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
||||||
|
|
||||||
let response = conn.muc_presence(&user.xmpp_name).await.unwrap();
|
let response = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap();
|
||||||
let expected = Presence {
|
let expected = Presence {
|
||||||
to: Some(Jid {
|
to: Some(Jid {
|
||||||
name: Some(conn.user.xmpp_name.clone()),
|
name: Some(conn.user.xmpp_name.clone()),
|
||||||
|
@ -233,7 +332,7 @@ mod tests {
|
||||||
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
let mut player_conn = server.core.connect_to_player(&user.player_id).await;
|
||||||
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap();
|
||||||
|
|
||||||
let response = conn.muc_presence(&user.xmpp_name).await.unwrap();
|
let response = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap();
|
||||||
assert_eq!(expected, response);
|
assert_eq!(expected, response);
|
||||||
|
|
||||||
server.shutdown().await.unwrap();
|
server.shutdown().await.unwrap();
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub enum ClientMessage {
|
||||||
realname: Str,
|
realname: Str,
|
||||||
},
|
},
|
||||||
/// `JOIN <chan>`
|
/// `JOIN <chan>`
|
||||||
Join(Chan),
|
Join(ChannelName),
|
||||||
/// `MODE <target>`
|
/// `MODE <target>`
|
||||||
Mode {
|
Mode {
|
||||||
target: Recipient,
|
target: Recipient,
|
||||||
|
@ -48,11 +48,11 @@ pub enum ClientMessage {
|
||||||
},
|
},
|
||||||
/// `TOPIC <chan> :<topic>`
|
/// `TOPIC <chan> :<topic>`
|
||||||
Topic {
|
Topic {
|
||||||
chan: Chan,
|
chan: ChannelName,
|
||||||
topic: Str,
|
topic: Str,
|
||||||
},
|
},
|
||||||
Part {
|
Part {
|
||||||
chan: Chan,
|
chan: ChannelName,
|
||||||
message: Option<Str>,
|
message: Option<Str>,
|
||||||
},
|
},
|
||||||
/// `PRIVMSG <target> :<msg>`
|
/// `PRIVMSG <target> :<msg>`
|
||||||
|
@ -65,6 +65,10 @@ pub enum ClientMessage {
|
||||||
reason: Str,
|
reason: Str,
|
||||||
},
|
},
|
||||||
Authenticate(Str),
|
Authenticate(Str),
|
||||||
|
ChatHistory {
|
||||||
|
chan: ChannelName,
|
||||||
|
limit: u32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod command_args {
|
pub mod command_args {
|
||||||
|
@ -95,6 +99,7 @@ pub fn client_message(input: &str) -> Result<ClientMessage> {
|
||||||
client_message_privmsg,
|
client_message_privmsg,
|
||||||
client_message_quit,
|
client_message_quit,
|
||||||
client_message_authenticate,
|
client_message_authenticate,
|
||||||
|
client_message_chathistory,
|
||||||
)))(input);
|
)))(input);
|
||||||
match res {
|
match res {
|
||||||
Ok((_, e)) => Ok(e),
|
Ok((_, e)) => Ok(e),
|
||||||
|
@ -134,6 +139,7 @@ fn client_message_nick(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_message_pass(input: &str) -> IResult<&str, ClientMessage> {
|
fn client_message_pass(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
let (input, _) = tag("PASS ")(input)?;
|
let (input, _) = tag("PASS ")(input)?;
|
||||||
let (input, r) = opt(tag(":"))(input)?;
|
let (input, r) = opt(tag(":"))(input)?;
|
||||||
|
@ -172,6 +178,7 @@ fn client_message_user(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_message_join(input: &str) -> IResult<&str, ClientMessage> {
|
fn client_message_join(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
let (input, _) = tag("JOIN ")(input)?;
|
let (input, _) = tag("JOIN ")(input)?;
|
||||||
let (input, chan) = chan(input)?;
|
let (input, chan) = chan(input)?;
|
||||||
|
@ -280,6 +287,22 @@ fn client_message_authenticate(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
Ok((input, ClientMessage::Authenticate(body.into())))
|
Ok((input, ClientMessage::Authenticate(body.into())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_message_chathistory(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
|
let (input, _) = tag("CHATHISTORY LATEST ")(input)?;
|
||||||
|
let (input, chan) = chan(input)?;
|
||||||
|
|
||||||
|
let (input, _) = tag(" * ")(input)?;
|
||||||
|
let (input, limit) = limit(input)?;
|
||||||
|
|
||||||
|
Ok((input, ClientMessage::ChatHistory { chan, limit }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limit(input: &str) -> IResult<&str, u32> {
|
||||||
|
let (input, limit) = receiver(input)?;
|
||||||
|
let limit = limit.parse().unwrap();
|
||||||
|
Ok((input, limit))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum CapabilitySubcommand {
|
pub enum CapabilitySubcommand {
|
||||||
/// CAP LS {code}
|
/// CAP LS {code}
|
||||||
|
@ -383,6 +406,7 @@ mod test {
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_pong() {
|
fn test_client_message_pong() {
|
||||||
let input = "PONG 1337";
|
let input = "PONG 1337";
|
||||||
|
@ -391,6 +415,7 @@ mod test {
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_nick() {
|
fn test_client_message_nick() {
|
||||||
let input = "NICK SomeNick";
|
let input = "NICK SomeNick";
|
||||||
|
@ -401,6 +426,7 @@ mod test {
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_whois() {
|
fn test_client_message_whois() {
|
||||||
let test_user = "WHOIS val";
|
let test_user = "WHOIS val";
|
||||||
|
@ -461,6 +487,7 @@ mod test {
|
||||||
assert_matches!(res_more_than_two_params, Ok(result) => assert_eq!(expected_more_than_two_params, result));
|
assert_matches!(res_more_than_two_params, Ok(result) => assert_eq!(expected_more_than_two_params, result));
|
||||||
assert_matches!(res_none_none_params, Ok(result) => assert_eq!(expected_none_none_params, result))
|
assert_matches!(res_none_none_params, Ok(result) => assert_eq!(expected_none_none_params, result))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_user() {
|
fn test_client_message_user() {
|
||||||
let input = "USER SomeNick 8 * :Real Name";
|
let input = "USER SomeNick 8 * :Real Name";
|
||||||
|
@ -472,28 +499,31 @@ mod test {
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_part() {
|
fn test_client_message_part() {
|
||||||
let input = "PART #chan :Pokasiki !!!";
|
let input = "PART #chan :Pokasiki !!!";
|
||||||
let expected = ClientMessage::Part {
|
let expected = ClientMessage::Part {
|
||||||
chan: Chan::Global("chan".into()),
|
chan: ChannelName::Global("chan".into()),
|
||||||
message: Some("Pokasiki !!!".into()),
|
message: Some("Pokasiki !!!".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_part_empty() {
|
fn test_client_message_part_empty() {
|
||||||
let input = "PART #chan";
|
let input = "PART #chan";
|
||||||
let expected = ClientMessage::Part {
|
let expected = ClientMessage::Part {
|
||||||
chan: Chan::Global("chan".into()),
|
chan: ChannelName::Global("chan".into()),
|
||||||
message: None,
|
message: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_cap_req() {
|
fn test_client_cap_req() {
|
||||||
let input = "CAP REQ :multi-prefix -sasl";
|
let input = "CAP REQ :multi-prefix -sasl";
|
||||||
|
@ -513,4 +543,16 @@ mod test {
|
||||||
let result = client_message(input);
|
let result = client_message(input);
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_chat_history_latest() {
|
||||||
|
let input = "CHATHISTORY LATEST #chan * 10";
|
||||||
|
let expected = ClientMessage::ChatHistory {
|
||||||
|
chan: ChannelName::Global("chan".into()),
|
||||||
|
limit: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = client_message(input);
|
||||||
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,20 +48,20 @@ fn params(input: &str) -> IResult<&str, &str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Chan {
|
pub enum ChannelName {
|
||||||
/// `#<name>` — network-global channel, available from any server in the network.
|
/// `#<name>` — network-global channel, available from any server in the network.
|
||||||
Global(Str),
|
Global(Str),
|
||||||
/// `&<name>` — server-local channel, available only to connections to the same server. Rarely used in practice.
|
/// `&<name>` — server-local channel, available only to connections to the same server. Rarely used in practice.
|
||||||
Local(Str),
|
Local(Str),
|
||||||
}
|
}
|
||||||
impl Chan {
|
impl ChannelName {
|
||||||
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Chan::Global(name) => {
|
ChannelName::Global(name) => {
|
||||||
writer.write_all(b"#").await?;
|
writer.write_all(b"#").await?;
|
||||||
writer.write_all(name.as_bytes()).await?;
|
writer.write_all(name.as_bytes()).await?;
|
||||||
}
|
}
|
||||||
Chan::Local(name) => {
|
ChannelName::Local(name) => {
|
||||||
writer.write_all(b"&").await?;
|
writer.write_all(b"&").await?;
|
||||||
writer.write_all(name.as_bytes()).await?;
|
writer.write_all(name.as_bytes()).await?;
|
||||||
}
|
}
|
||||||
|
@ -70,17 +70,17 @@ impl Chan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chan(input: &str) -> IResult<&str, Chan> {
|
fn chan(input: &str) -> IResult<&str, ChannelName> {
|
||||||
fn chan_global(input: &str) -> IResult<&str, Chan> {
|
fn chan_global(input: &str) -> IResult<&str, ChannelName> {
|
||||||
let (input, _) = tag("#")(input)?;
|
let (input, _) = tag("#")(input)?;
|
||||||
let (input, name) = receiver(input)?;
|
let (input, name) = receiver(input)?;
|
||||||
Ok((input, Chan::Global(name.into())))
|
Ok((input, ChannelName::Global(name.into())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chan_local(input: &str) -> IResult<&str, Chan> {
|
fn chan_local(input: &str) -> IResult<&str, ChannelName> {
|
||||||
let (input, _) = tag("&")(input)?;
|
let (input, _) = tag("&")(input)?;
|
||||||
let (input, name) = receiver(input)?;
|
let (input, name) = receiver(input)?;
|
||||||
Ok((input, Chan::Local(name.into())))
|
Ok((input, ChannelName::Local(name.into())))
|
||||||
}
|
}
|
||||||
|
|
||||||
alt((chan_global, chan_local))(input)
|
alt((chan_global, chan_local))(input)
|
||||||
|
@ -89,7 +89,7 @@ fn chan(input: &str) -> IResult<&str, Chan> {
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Recipient {
|
pub enum Recipient {
|
||||||
Nick(Str),
|
Nick(Str),
|
||||||
Chan(Chan),
|
Chan(ChannelName),
|
||||||
}
|
}
|
||||||
impl Recipient {
|
impl Recipient {
|
||||||
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
@ -125,7 +125,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chan_global() {
|
fn test_chan_global() {
|
||||||
let input = "#testchan";
|
let input = "#testchan";
|
||||||
let expected = Chan::Global("testchan".into());
|
let expected = ChannelName::Global("testchan".into());
|
||||||
|
|
||||||
let result = chan(input);
|
let result = chan(input);
|
||||||
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
|
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
|
||||||
|
@ -139,7 +139,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chan_local() {
|
fn test_chan_local() {
|
||||||
let input = "&localchan";
|
let input = "&localchan";
|
||||||
let expected = Chan::Local("localchan".into());
|
let expected = ChannelName::Local("localchan".into());
|
||||||
|
|
||||||
let result = chan(input);
|
let result = chan(input);
|
||||||
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
|
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
|
||||||
|
|
|
@ -69,8 +69,8 @@ pub enum ServerMessageBody {
|
||||||
target: Recipient,
|
target: Recipient,
|
||||||
body: Str,
|
body: Str,
|
||||||
},
|
},
|
||||||
Join(Chan),
|
Join(ChannelName),
|
||||||
Part(Chan),
|
Part(ChannelName),
|
||||||
Error {
|
Error {
|
||||||
reason: Str,
|
reason: Str,
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,7 @@ pub enum ServerMessageBody {
|
||||||
},
|
},
|
||||||
N332Topic {
|
N332Topic {
|
||||||
client: Str,
|
client: Str,
|
||||||
chat: Chan,
|
chat: ChannelName,
|
||||||
topic: Str,
|
topic: Str,
|
||||||
},
|
},
|
||||||
/// A reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request.
|
/// A reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request.
|
||||||
|
@ -141,12 +141,12 @@ pub enum ServerMessageBody {
|
||||||
},
|
},
|
||||||
N353NamesReply {
|
N353NamesReply {
|
||||||
client: Str,
|
client: Str,
|
||||||
chan: Chan,
|
chan: ChannelName,
|
||||||
members: NonEmpty<PrefixedNick>,
|
members: NonEmpty<PrefixedNick>,
|
||||||
},
|
},
|
||||||
N366NamesReplyEnd {
|
N366NamesReplyEnd {
|
||||||
client: Str,
|
client: Str,
|
||||||
chan: Chan,
|
chan: ChannelName,
|
||||||
},
|
},
|
||||||
N431ErrNoNicknameGiven {
|
N431ErrNoNicknameGiven {
|
||||||
client: Str,
|
client: Str,
|
||||||
|
@ -154,7 +154,7 @@ pub enum ServerMessageBody {
|
||||||
},
|
},
|
||||||
N474BannedFromChan {
|
N474BannedFromChan {
|
||||||
client: Str,
|
client: Str,
|
||||||
chan: Chan,
|
chan: ChannelName,
|
||||||
message: Str,
|
message: Str,
|
||||||
},
|
},
|
||||||
N502UsersDontMatch {
|
N502UsersDontMatch {
|
||||||
|
|
|
@ -15,6 +15,9 @@ pub mod streamerror;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
pub mod xml;
|
pub mod xml;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod testkit;
|
||||||
|
|
||||||
// Implemented as a macro instead of a fn due to borrowck limitations
|
// Implemented as a macro instead of a fn due to borrowck limitations
|
||||||
macro_rules! skip_text {
|
macro_rules! skip_text {
|
||||||
($reader: ident, $buf: ident) => {
|
($reader: ident, $buf: ident) => {
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
|
|
||||||
use quick_xml::events::{BytesEnd, BytesStart, Event};
|
use anyhow::{anyhow, Result};
|
||||||
use quick_xml::name::ResolveResult;
|
use quick_xml::events::attributes::Attribute;
|
||||||
|
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||||
|
use quick_xml::name::{QName, ResolveResult};
|
||||||
|
|
||||||
use crate::bind::Jid;
|
use crate::bind::Jid;
|
||||||
use crate::xml::*;
|
use crate::xml::*;
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
|
|
||||||
pub const XMLNS: &'static str = "http://jabber.org/protocol/muc";
|
pub const XMLNS: &'static str = "http://jabber.org/protocol/muc";
|
||||||
pub const XMLNS_USER: &'static str = "http://jabber.org/protocol/muc#user";
|
pub const XMLNS_USER: &'static str = "http://jabber.org/protocol/muc#user";
|
||||||
|
pub const XMLNS_DELAY: &'static str = "urn:xmpp:delay";
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Default)]
|
#[derive(PartialEq, Eq, Debug, Default)]
|
||||||
pub struct History {
|
pub struct History {
|
||||||
|
@ -154,6 +156,7 @@ pub struct XUser {
|
||||||
/// Code 201. The room from which the presence stanza was sent was just created.
|
/// Code 201. The room from which the presence stanza was sent was just created.
|
||||||
pub just_created: bool,
|
pub just_created: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToXml for XUser {
|
impl ToXml for XUser {
|
||||||
fn serialize(&self, output: &mut Vec<Event<'static>>) {
|
fn serialize(&self, output: &mut Vec<Event<'static>>) {
|
||||||
let mut tag = BytesStart::new("x");
|
let mut tag = BytesStart::new("x");
|
||||||
|
@ -180,6 +183,7 @@ pub struct XUserItem {
|
||||||
pub jid: Jid,
|
pub jid: Jid,
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToXml for XUserItem {
|
impl ToXml for XUserItem {
|
||||||
fn serialize(&self, output: &mut Vec<Event<'static>>) {
|
fn serialize(&self, output: &mut Vec<Event<'static>>) {
|
||||||
let mut meg = BytesStart::new("item");
|
let mut meg = BytesStart::new("item");
|
||||||
|
@ -198,6 +202,7 @@ pub enum Affiliation {
|
||||||
Outcast,
|
Outcast,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Affiliation {
|
impl Affiliation {
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
match s {
|
match s {
|
||||||
|
@ -228,6 +233,7 @@ pub enum Role {
|
||||||
Visitor,
|
Visitor,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Role {
|
impl Role {
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
match s {
|
match s {
|
||||||
|
@ -249,9 +255,83 @@ impl Role {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct Delay {
|
||||||
|
pub from: Jid,
|
||||||
|
pub stamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToXml for Delay {
|
||||||
|
fn serialize(&self, events: &mut Vec<Event>) {
|
||||||
|
let mut tag = BytesStart::new("delay");
|
||||||
|
tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"xmlns"),
|
||||||
|
value: XMLNS_DELAY.as_bytes().into(),
|
||||||
|
});
|
||||||
|
tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"from"),
|
||||||
|
value: self.from.to_string().into_bytes().into(),
|
||||||
|
});
|
||||||
|
tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"stamp"),
|
||||||
|
value: self.stamp.as_bytes().into(),
|
||||||
|
});
|
||||||
|
events.push(Event::Empty(tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Message-stanza of a historic message.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```xml
|
||||||
|
/// <message from="duqedadi@conference.example.com/misha" xml:lang="en" to="misha@example.com/tux" type="groupchat" id="7ca7cb14-b2af-49c9-bd90-05dabb1113a5">
|
||||||
|
/// <delay xmlns="urn:xmpp:delay" stamp="2024-05-17T16:05:28.440337Z" from="duqedadi@conference.example.com"/>
|
||||||
|
/// <body></body>
|
||||||
|
/// </message>
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct XmppHistoryMessage {
|
||||||
|
pub id: String,
|
||||||
|
pub to: Jid,
|
||||||
|
pub from: Jid,
|
||||||
|
pub delay: Delay,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToXml for XmppHistoryMessage {
|
||||||
|
fn serialize(&self, events: &mut Vec<Event<'static>>) {
|
||||||
|
let mut message_tag = BytesStart::new("message");
|
||||||
|
message_tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"id"),
|
||||||
|
value: self.id.as_str().as_bytes().into(),
|
||||||
|
});
|
||||||
|
message_tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"to"),
|
||||||
|
value: self.to.to_string().into_bytes().into(),
|
||||||
|
});
|
||||||
|
message_tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"from"),
|
||||||
|
value: self.from.to_string().into_bytes().into(),
|
||||||
|
});
|
||||||
|
message_tag.push_attribute(Attribute {
|
||||||
|
key: QName(b"type"),
|
||||||
|
value: b"groupchat".into(),
|
||||||
|
});
|
||||||
|
events.push(Event::Start(message_tag));
|
||||||
|
self.delay.serialize(events);
|
||||||
|
let body_tag = BytesStart::new("body");
|
||||||
|
events.push(Event::Start(body_tag));
|
||||||
|
events.push(Event::Text(BytesText::new(self.body.to_string().as_str()).into_owned()));
|
||||||
|
events.push(Event::End(BytesEnd::new("body")));
|
||||||
|
events.push(Event::End(BytesEnd::new("message")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::bind::{Name, Resource, Server};
|
||||||
|
use crate::testkit::assemble_string_from_event_flow;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_history_success_empty() {
|
fn test_history_success_empty() {
|
||||||
|
@ -334,4 +414,40 @@ mod test {
|
||||||
};
|
};
|
||||||
assert_eq!(res, expected);
|
assert_eq!(res, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_history_message_serialization() {
|
||||||
|
// Arrange
|
||||||
|
let history_message = XmppHistoryMessage {
|
||||||
|
id: "id".to_string(),
|
||||||
|
to: Jid {
|
||||||
|
name: Some(Name("sauer@example.com".into())),
|
||||||
|
server: Server("localhost".into()),
|
||||||
|
resource: Some(Resource("tester".into())),
|
||||||
|
},
|
||||||
|
from: Jid {
|
||||||
|
name: Some(Name("pepe".into())),
|
||||||
|
server: Server("rooms.localhost".into()),
|
||||||
|
resource: Some(Resource("sauer".into())),
|
||||||
|
},
|
||||||
|
delay: Delay {
|
||||||
|
from: Jid {
|
||||||
|
name: Some(Name("pepe".into())),
|
||||||
|
server: Server("rooms.localhost".into()),
|
||||||
|
resource: Some(Resource("tester".into())),
|
||||||
|
},
|
||||||
|
stamp: "2021-10-10T10:10:10Z".to_string(),
|
||||||
|
},
|
||||||
|
body: "Hello World.".to_string(),
|
||||||
|
};
|
||||||
|
let mut events = vec![];
|
||||||
|
let expected = r#"<message id="id" to="sauer@example.com@localhost/tester" from="pepe@rooms.localhost/sauer" type="groupchat"><delay xmlns="urn:xmpp:delay" from="pepe@rooms.localhost/tester" stamp="2021-10-10T10:10:10Z"/><body>Hello World.</body></message>"#;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
history_message.serialize(&mut events);
|
||||||
|
let flow = assemble_string_from_event_flow(&events);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(flow, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::Writer;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
pub fn assemble_string_from_event_flow(events: &Vec<Event<'_>>) -> String {
|
||||||
|
let mut writer = Writer::new(Cursor::new(Vec::new()));
|
||||||
|
for event in events {
|
||||||
|
writer.write_event(event).unwrap();
|
||||||
|
}
|
||||||
|
let result = writer.into_inner().into_inner();
|
||||||
|
String::from_utf8(result).unwrap()
|
||||||
|
}
|
|
@ -67,3 +67,35 @@ Or you can build it and run manually:
|
||||||
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/lavina --config config.toml
|
./target/release/lavina --config config.toml
|
||||||
|
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Install sqlx-cli into ~/.local/bin:
|
||||||
|
|
||||||
|
cargo install --locked sqlx-cli
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
Migrations run on every application start. For manual run, use sqlx:
|
||||||
|
|
||||||
|
sqlx mig run \
|
||||||
|
--source ./crates/lavina-core/migrations/ \
|
||||||
|
--database-url sqlite://db.sqlite
|
||||||
|
|
||||||
|
To see current status:
|
||||||
|
|
||||||
|
sqlx mig info \
|
||||||
|
--source ./crates/lavina-core/migrations/ \
|
||||||
|
--database-url sqlite://db.sqlite
|
||||||
|
|
||||||
|
sqlx mig info outputs
|
||||||
|
|
||||||
|
0/installed first
|
||||||
|
1/installed msg author
|
||||||
|
2/installed created at for messages
|
||||||
|
3/installed dialogs
|
||||||
|
4/installed new challenges
|
||||||
|
5/pending message datetime
|
||||||
|
|
|
@ -164,7 +164,7 @@ async fn endpoint_send_room_message(
|
||||||
let Ok(req) = serde_json::from_slice::<rooms::SendMessageReq>(&str[..]) else {
|
let Ok(req) = serde_json::from_slice::<rooms::SendMessageReq>(&str[..]) else {
|
||||||
return Ok(malformed_request());
|
return Ok(malformed_request());
|
||||||
};
|
};
|
||||||
let Ok(room_id) = RoomId::from(req.room_id) else {
|
let Ok(room_id) = RoomId::try_from(req.room_id) else {
|
||||||
return Ok(room_not_found());
|
return Ok(room_not_found());
|
||||||
};
|
};
|
||||||
let Ok(player_id) = PlayerId::from(req.author_id) else {
|
let Ok(player_id) = PlayerId::from(req.author_id) else {
|
||||||
|
@ -187,7 +187,7 @@ async fn endpoint_set_room_topic(
|
||||||
let Ok(req) = serde_json::from_slice::<rooms::SetTopicReq>(&str[..]) else {
|
let Ok(req) = serde_json::from_slice::<rooms::SetTopicReq>(&str[..]) else {
|
||||||
return Ok(malformed_request());
|
return Ok(malformed_request());
|
||||||
};
|
};
|
||||||
let Ok(room_id) = RoomId::from(req.room_id) else {
|
let Ok(room_id) = RoomId::try_from(req.room_id) else {
|
||||||
return Ok(room_not_found());
|
return Ok(room_not_found());
|
||||||
};
|
};
|
||||||
let Ok(player_id) = PlayerId::from(req.author_id) else {
|
let Ok(player_id) = PlayerId::from(req.author_id) else {
|
||||||
|
|
|
@ -29,7 +29,7 @@ async fn endpoint_cluster_join_room(
|
||||||
return Ok(malformed_request());
|
return Ok(malformed_request());
|
||||||
};
|
};
|
||||||
tracing::info!("Incoming request: {:?}", &req);
|
tracing::info!("Incoming request: {:?}", &req);
|
||||||
let Ok(room_id) = RoomId::from(req.room_id) else {
|
let Ok(room_id) = RoomId::try_from(req.room_id) else {
|
||||||
dbg!(&req.room_id);
|
dbg!(&req.room_id);
|
||||||
return Ok(room_not_found());
|
return Ok(room_not_found());
|
||||||
};
|
};
|
||||||
|
@ -55,7 +55,7 @@ async fn endpoint_cluster_add_message(
|
||||||
dbg!(&req.created_at);
|
dbg!(&req.created_at);
|
||||||
return Ok(malformed_request());
|
return Ok(malformed_request());
|
||||||
};
|
};
|
||||||
let Ok(room_id) = RoomId::from(req.room_id) else {
|
let Ok(room_id) = RoomId::try_from(req.room_id) else {
|
||||||
dbg!(&req.room_id);
|
dbg!(&req.room_id);
|
||||||
return Ok(room_not_found());
|
return Ok(room_not_found());
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue