Compare commits

...

2 Commits

Author SHA1 Message Date
cel 🌸 05b0d38490 WIP: rfc 6121 data(base)types 2025-02-12 17:33:12 +00:00
cel 🌸 8e6aa698b3 reconnection supervisor command 2025-02-12 06:19:02 +00:00
11 changed files with 317 additions and 41 deletions

View File

@ -73,6 +73,7 @@ file/media sharing (further research needed):
- [ ] xep-0234: jingle file transfer - [ ] xep-0234: jingle file transfer
need more research: need more research:
- [ ] xep-0154: user profile
- [ ] message editing - [ ] message editing
- [ ] xep-0308: last message correction (should not be used for older than last message according to spec) - [ ] xep-0308: last message correction (should not be used for older than last message according to spec)
- [ ] chat read markers - [ ] chat read markers

42
TODO.md
View File

@ -2,22 +2,36 @@
## next ## next
ci/cd: doc generation feat(luz): everything in rfc6120 and rfc6121
feature: error handling on stream according to rfc6120 feat(luz): handle_online
docs: jid feat(luz): handle_offline
docs: jabber feat(luz): handle_stanza
docs: starttls feat(luz): database
docs: sasl feat(luz): error handling on stream according to rfc6120
docs: resource binding feat(luz): send message
feature: starttls feat(luz): receive message
feature: sasl feat(luz): retreive messages stored in database
feature: resource binding feat(luz): get roster (online and offline)
feat(luz): set roster
## in progress feat(luz): reconnect supervisorcommand
feat: thiserror everywhere
feature: logging feat(luz): proper stanza ids
test: proper tests
ci: doc generation
docs(jid): jid
feat(peanuts): derive macros for IntoElement and FromElement
docs(jabber): connection, starttls, sasl, binding, bound_stream, etc.
feat: proper logging for everything basically
feat(luz): passwordprovider trait, to avoid storing password in struct
feat(luz): auto-reconnect state stored in luz actor, for if e.g. server shut down
refactor(luz): dealing properly with all the joinsets and joinhandles
feat(peanuts): some kind of way to configure the reader and writer to log the raw xml written to the stream, probably by having a method that allows you to add a log writer to them. will need to investigate some kind of log namespacing.
feat(jabber): storing resource within the bound_stream connection
## done ## done
feature: starttls
feature: sasl
feature: resource binding
feature: jabber client connection feature: jabber client connection
feature: jid feature: jid

View File

@ -15,3 +15,4 @@ tokio-stream = "0.1.17"
tokio-util = "0.7.13" tokio-util = "0.7.13"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
uuid = "1.13.1"

View File

@ -1,5 +1,53 @@
CREATE TABLE roster( PRAGMA foreign_keys = on;
id INTEGER PRIMARY KEY,
jid TEXT NOT NULL, -- a user jid will never change, only a chat user will change
nickname TEXT, -- TODO: avatar, nick, etc.
create table user(
jid jid primary key,
cached_status text,
);
-- enum for subscription state
create table subscription(
state text primary key,
);
insert into subscription ( state ) values ('none'), ('pending-out'), ('pending-in'), ('only-out'), ('only-in'), ('out-pending-in'), ('in-pending-out'), ('buddy');
-- a roster contains users, with client-set nickname
CREATE TABLE roster(
jid jid primary key,
name TEXT,
subscription text not null,
foreign key(subscription) references subscription(state),
foreign key(jid) references users(jid)
);
create table groups(
group text primary key
);
create table groups_roster(
group_id text,
contact_id jid,
foreign key(group_id) references group(id),
foreign key(contact_id) references roster(id),
primary key(group_id, contact_id)
);
-- chat includes reference to user jid chat is with
create table chats (
id uuid primary key,
contact_id jid not null unique,
);
-- messages include reference to chat they are in, and who sent them.
create table messages (
id uuid primary key,
body text,
chat_id uuid not null,
from jid not null,
-- TODO: read bool not null,
foreign key(chat_id) references chats(id),
foreign key(from) references users(jid)
); );

30
luz/src/chat.rs Normal file
View File

@ -0,0 +1,30 @@
use uuid::Uuid;
use crate::roster::Contact;
pub enum Chat {
Direct(DM),
Channel(Channel),
}
#[derive(Debug)]
pub struct Message {
id: Uuid,
// contains full contact information
from: Contact,
// TODO: rich text, other contents, threads
body: Body,
}
#[derive(Debug)]
pub struct Body {
body: String,
}
pub struct DM {
contact: Contact,
message_history: Vec<Message>,
}
// TODO: group chats
pub struct Channel {}

View File

@ -46,7 +46,21 @@ pub enum SupervisorCommand {
Disconnect, Disconnect,
// for if there was a stream error, require to reconnect // for if there was a stream error, require to reconnect
// couldn't stream errors just cause a crash? lol // couldn't stream errors just cause a crash? lol
Reconnect, Reconnect(State),
}
pub enum State {
Write(mpsc::Receiver<WriteMessage>),
Read(
(
SqlitePool,
mpsc::Sender<UpdateMessage>,
tokio::task::JoinSet<()>,
mpsc::Sender<SupervisorCommand>,
WriteHandle,
Arc<Mutex<HashMap<String, oneshot::Sender<Result<Stanza, Error>>>>>,
),
),
} }
impl Supervisor { impl Supervisor {
@ -101,10 +115,70 @@ impl Supervisor {
} }
break; break;
}, },
SupervisorCommand::Reconnect => { SupervisorCommand::Reconnect(state) => {
// TODO: please omfg // TODO: please omfg
// send abort to read stream, as already done, consider // send abort to read stream, as already done, consider
todo!() let (read_state, mut write_state);
match state {
// TODO: proper state things for read and write thread
State::Write(receiver) => {
write_state = receiver;
let (send, recv) = oneshot::channel();
let _ = self.reader_handle.send(ReadControl::Abort(send)).await;
if let Ok(state) = recv.await {
read_state = state;
} else {
break
}
},
State::Read(read) => {
read_state = read;
let (send, recv) = oneshot::channel();
let _ = self.writer_handle.send(WriteControl::Abort(send)).await;
// TODO: need a tokio select, in case the state arrives from somewhere else
if let Ok(state) = recv.await {
write_state = state;
} else {
break
}
},
}
let mut jid = self.jid.lock().await;
let mut domain = jid.domainpart.clone();
let connection = jabber::connect_and_login(&mut jid, &*self.password, &mut domain).await;
match connection {
Ok(c) => {
let (read, write) = c.split();
let (send, recv) = oneshot::channel();
self.writer_crash = recv;
self.writer_handle =
WriteControlHandle::reconnect(write, send, write_state);
let (send, recv) = oneshot::channel();
self.reader_crash = recv;
let (db, update_sender, tasks, supervisor_command, write_sender, pending_iqs) = read_state;
self.reader_handle = ReadControlHandle::reconnect(
read,
send,
db,
update_sender,
supervisor_command,
write_sender,
tasks,
pending_iqs,
);
},
Err(e) => {
// if reconnection failure, respond to all current write messages with lost connection error. the received processes should complete themselves.
write_state.close();
while let Some(msg) = write_state.recv().await {
let _ = msg.respond_to.send(Err(Error::LostConnection));
}
let _ = self.sender.send(UpdateMessage::Error(e.into())).await;
break;
},
}
}, },
} }
}, },

View File

@ -21,7 +21,6 @@ use super::{
}; };
pub struct Read { pub struct Read {
// TODO: place iq hashmap here
control_receiver: mpsc::Receiver<ReadControl>, control_receiver: mpsc::Receiver<ReadControl>,
stream: BoundJabberReader<Tls>, stream: BoundJabberReader<Tls>,
on_crash: oneshot::Sender<( on_crash: oneshot::Sender<(

View File

@ -1,11 +1,23 @@
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
AlreadyConnected, AlreadyConnected,
Presence(Reason),
Roster(Reason),
SendMessage(Reason),
AlreadyDisconnected,
LostConnection,
}
#[derive(Debug)]
pub enum Reason {
// TODO: organisastion of error into internal error thing
Timeout,
Stream(stanza::stream_error::Error),
Stanza(stanza::stanza_error::Error),
Jabber(jabber::Error), Jabber(jabber::Error),
XML(peanuts::Error), XML(peanuts::Error),
SQL(sqlx::Error), SQL(sqlx::Error),
JID(jid::ParseError), // JID(jid::ParseError),
AlreadyDisconnected,
LostConnection, LostConnection,
} }
@ -15,11 +27,11 @@ impl From<peanuts::Error> for Error {
} }
} }
impl From<jid::ParseError> for Error { // impl From<jid::ParseError> for Error {
fn from(e: jid::ParseError) -> Self { // fn from(e: jid::ParseError) -> Self {
Self::JID(e) // Self::JID(e)
} // }
} // }
impl From<sqlx::Error> for Error { impl From<sqlx::Error> for Error {
fn from(e: sqlx::Error) -> Self { fn from(e: sqlx::Error) -> Self {

View File

@ -4,27 +4,31 @@ use std::{
sync::Arc, sync::Arc,
}; };
use chat::{Body, Message};
use connection::{write::WriteMessage, SupervisorSender}; use connection::{write::WriteMessage, SupervisorSender};
use jabber::JID; use jabber::JID;
use presence::{Offline, Online, Presence};
use roster::Contact;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use stanza::{ use stanza::client::{
client::{ iq::{self, Iq, IqType},
iq::{self, Iq, IqType}, Stanza,
Stanza,
},
roster::{self, Query},
}; };
use tokio::{ use tokio::{
sync::{mpsc, oneshot, Mutex}, sync::{mpsc, oneshot, Mutex},
task::JoinSet, task::JoinSet,
}; };
use uuid::Uuid;
use crate::connection::write::WriteHandle; use crate::connection::write::WriteHandle;
use crate::connection::{SupervisorCommand, SupervisorHandle}; use crate::connection::{SupervisorCommand, SupervisorHandle};
use crate::error::Error; use crate::error::Error;
mod chat;
mod connection; mod connection;
mod error; mod error;
mod presence;
mod roster;
pub struct Luz { pub struct Luz {
receiver: mpsc::Receiver<CommandMessage>, receiver: mpsc::Receiver<CommandMessage>,
@ -138,7 +142,6 @@ impl Luz {
self.jid.clone(), self.jid.clone(),
self.db.clone(), self.db.clone(),
self.sender.clone(), self.sender.clone(),
// TODO: iq hashmap
self.pending_iqs.clone() self.pending_iqs.clone()
)), )),
None => self.tasks.spawn(msg.handle_offline( None => self.tasks.spawn(msg.handle_offline(
@ -191,7 +194,7 @@ impl CommandMessage {
to: None, to: None,
r#type: IqType::Get, r#type: IqType::Get,
lang: None, lang: None,
query: Some(iq::Query::Roster(roster::Query { query: Some(iq::Query::Roster(stanza::roster::Query {
ver: None, ver: None,
items: Vec::new(), items: Vec::new(),
})), })),
@ -266,16 +269,62 @@ impl LuzHandle {
} }
pub enum CommandMessage { pub enum CommandMessage {
/// connect to XMPP chat server. gets roster and
Connect, Connect,
/// disconnect from XMPP chat server.
Disconnect, Disconnect,
/// gets the roster. if offline, retreives cached version from database. should be stored in application memory. /// get the roster. if offline, retreive cached version from database. should be stored in application memory
GetRoster, GetRoster,
SendMessage(JID, String), // add a contact to your roster, with a status of none, no subscriptions
AddContact(JID),
/// send a friend request i.e. a subscription request with a subscription pre-approval. if not already added to roster server adds to roster.
BuddyRequest(JID),
/// send a subscription request, without pre-approval. if not already added to roster server adds to roster.
SubscriptionRequest(JID),
/// accept a friend request by accepting a pending subscription and sending a subscription request back. if not already added to roster adds to roster.
AcceptBuddyRequest(JID),
/// accept a pending subscription and doesn't send a subscription request back. if not already added to roster adds to roster.
AcceptSubscriptionRequest(JID),
/// unsubscribe to a contact, but don't remove their subscription.
UnsubscribeFromContact(JID),
/// stop a contact from being subscribed, but stay subscribed to the contact.
UnsubscribeContact(JID),
/// remove subscriptions to and from contact, but keep in roster.
UnfriendContact(JID),
/// remove a contact from the contact list. will remove subscriptions if not already done then delete contact from roster.
DeleteContact(JID),
/// set online status
SendStatus(Online),
SendOffline(Offline),
/// send a directed presence (usually to a non-contact).
// TODO: should probably make it so people can add non-contact auto presence sharing in the client.
SendDirectedPresence {
to: JID,
presence: Presence,
},
SendMessage {
id: Uuid,
to: JID,
body: Body,
},
} }
#[derive(Debug)] #[derive(Debug)]
pub enum UpdateMessage { pub enum UpdateMessage {
Error(Error), Error(Error),
Connected, Connected(Online),
Roster(Vec<roster::Item>), Disconnected(Offline),
/// full roster (replace full app roster state with this)
Roster(Vec<Contact>),
/// roster update (only update app roster state)
RosterPush(Contact),
Presence {
from: JID,
presence: Presence,
},
MessageDispatched(Uuid),
Message {
from: JID,
message: Message,
},
} }

19
luz/src/presence.rs Normal file
View File

@ -0,0 +1,19 @@
use stanza::client::presence::Show;
#[derive(Debug)]
pub struct Online {
show: Option<Show>,
status: Option<String>,
priority: Option<i8>,
}
#[derive(Debug)]
pub struct Offline {
status: Option<String>,
}
#[derive(Debug)]
pub enum Presence {
Online(Online),
Offline(Offline),
}

29
luz/src/roster.rs Normal file
View File

@ -0,0 +1,29 @@
use std::collections::HashSet;
use jid::JID;
use uuid::Uuid;
#[derive(Debug)]
pub struct Contact {
// jid is the id used to reference everything, but not the primary key
jid: JID,
subscription: Subscription,
/// client user defined name
name: Option<String>,
// TODO: avatar, nickname
/// nickname picked by contact
// nickname: Option<String>,
groups: HashSet<String>,
}
#[derive(Debug)]
enum Subscription {
None,
PendingOut,
PendingIn,
OnlyOut,
OnlyIn,
OutPendingIn,
InPendingOut,
Buddy,
}