Compare commits

..

No commits in common. "05b0d38490a69d058cdd0ee7b17140634d116af2" and "ec41f1d4ff07a00223b6ed34fc5b65c38d3cd535" have entirely different histories.

11 changed files with 40 additions and 316 deletions

View File

@ -73,7 +73,6 @@ 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,36 +2,22 @@
## next ## next
feat(luz): everything in rfc6120 and rfc6121 ci/cd: doc generation
feat(luz): handle_online feature: error handling on stream according to rfc6120
feat(luz): handle_offline docs: jid
feat(luz): handle_stanza docs: jabber
feat(luz): database docs: starttls
feat(luz): error handling on stream according to rfc6120 docs: sasl
feat(luz): send message docs: resource binding
feat(luz): receive message
feat(luz): retreive messages stored in database
feat(luz): get roster (online and offline)
feat(luz): set roster
feat(luz): reconnect supervisorcommand
feat: thiserror everywhere
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
feature: starttls feature: starttls
feature: sasl feature: sasl
feature: resource binding feature: resource binding
## in progress
feature: logging
## done
feature: jabber client connection feature: jabber client connection
feature: jid feature: jid

View File

@ -15,4 +15,3 @@ 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,53 +1,5 @@
PRAGMA foreign_keys = on;
-- a user jid will never change, only a chat user will change
-- 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( CREATE TABLE roster(
jid jid primary key, id INTEGER PRIMARY KEY,
name TEXT, jid TEXT NOT NULL,
subscription text not null, nickname TEXT,
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)
); );

View File

@ -1,30 +0,0 @@
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,21 +46,7 @@ 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(State), Reconnect,
}
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 {
@ -115,70 +101,10 @@ impl Supervisor {
} }
break; break;
}, },
SupervisorCommand::Reconnect(state) => { SupervisorCommand::Reconnect => {
// TODO: please omfg // TODO: please omfg
// send abort to read stream, as already done, consider // send abort to read stream, as already done, consider
let (read_state, mut write_state); todo!()
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,6 +21,7 @@ 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,23 +1,11 @@
#[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,
} }
@ -27,11 +15,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,31 +4,27 @@ 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::client::{ use stanza::{
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>,
@ -142,6 +138,7 @@ 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(
@ -194,7 +191,7 @@ impl CommandMessage {
to: None, to: None,
r#type: IqType::Get, r#type: IqType::Get,
lang: None, lang: None,
query: Some(iq::Query::Roster(stanza::roster::Query { query: Some(iq::Query::Roster(roster::Query {
ver: None, ver: None,
items: Vec::new(), items: Vec::new(),
})), })),
@ -269,62 +266,16 @@ 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,
/// get the roster. if offline, retreive cached version from database. should be stored in application memory /// gets the roster. if offline, retreives cached version from database. should be stored in application memory.
GetRoster, GetRoster,
// add a contact to your roster, with a status of none, no subscriptions SendMessage(JID, String),
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(Online), Connected,
Disconnected(Offline), Roster(Vec<roster::Item>),
/// 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,
},
} }

View File

@ -1,19 +0,0 @@
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),
}

View File

@ -1,29 +0,0 @@
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,
}