1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
use std::{collections::HashSet, fmt::Display};
use jid::JID;
use rusqlite::{
ToSql,
types::{FromSql, ToSqlOutput, Value},
};
pub struct ContactUpdate {
pub name: Option<String>,
pub groups: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "reactive_stores", derive(reactive_stores::Store))]
pub struct Contact {
// jid is the id used to reference everything, but not the primary key
pub user_jid: JID,
pub subscription: Subscription,
/// client user defined name
pub name: Option<String>,
// TODO: avatar, nickname
/// nickname picked by contact
// nickname: Option<String>,
#[cfg_attr(feature = "reactive_stores", store(key: String = |group| group.clone()))]
pub groups: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Contact subscription state.
pub enum Subscription {
/// No subscriptions.
None,
/// Pending outgoing subscription request.
PendingOut,
/// Pending incoming subscription request.
PendingIn,
/// Pending incoming & pending outgoing subscription requests.
PendingInPendingOut,
/// Subscribed to.
OnlyOut,
/// Subscription from.
OnlyIn,
/// Subscribed to & pending incoming subscription request.
OutPendingIn,
/// Subscription from & pending outgoing subscription request.
InPendingOut,
/// Buddy (subscriptions both ways).
Buddy,
// TODO: perhaps don't need, just emit event to remove contact
// Remove,
}
impl ToSql for Subscription {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(match self {
Subscription::None => ToSqlOutput::Owned(Value::Text("none".to_string())),
Subscription::PendingOut => ToSqlOutput::Owned(Value::Text("pending-out".to_string())),
Subscription::PendingIn => ToSqlOutput::Owned(Value::Text("pending-in".to_string())),
Subscription::PendingInPendingOut => {
ToSqlOutput::Owned(Value::Text("pending-in-pending-out".to_string()))
}
Subscription::OnlyOut => ToSqlOutput::Owned(Value::Text("only-out".to_string())),
Subscription::OnlyIn => ToSqlOutput::Owned(Value::Text("only-in".to_string())),
Subscription::OutPendingIn => {
ToSqlOutput::Owned(Value::Text("out-pending-in".to_string()))
}
Subscription::InPendingOut => {
ToSqlOutput::Owned(Value::Text("in-pending-out".to_string()))
}
Subscription::Buddy => ToSqlOutput::Owned(Value::Text("buddy".to_string())),
})
}
}
impl FromSql for Subscription {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
Ok(match value.as_str()? {
"none" => Self::None,
"pending-out" => Self::PendingOut,
"pending-in" => Self::PendingIn,
"pending-in-pending-out" => Self::PendingInPendingOut,
"only-out" => Self::OnlyOut,
"only-in" => Self::OnlyIn,
"out-pending-in" => Self::OutPendingIn,
"in-pending-out" => Self::InPendingOut,
"buddy" => Self::Buddy,
// TODO: don't have these lol
value => panic!("unexpected subscription `{value}`"),
})
}
}
impl Display for Subscription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Subscription::None => write!(f, "No Subscriptions"),
Subscription::PendingOut => write!(f, "Pending Outgoing Subscription Request"),
Subscription::PendingIn => write!(f, "Pending Incoming Subscription Request"),
Subscription::PendingInPendingOut => write!(
f,
"Pending Incoming & Pending Outgoing Subscription Requests"
),
Subscription::OnlyOut => write!(f, "Subscribed To"),
Subscription::OnlyIn => write!(f, "Subscription From"),
Subscription::OutPendingIn => {
write!(f, "Subscribed To & Pending Incoming Subscription Request")
}
Subscription::InPendingOut => write!(
f,
"Subscription From & Pending Outgoing Subscription Request"
),
Subscription::Buddy => write!(f, "Buddy (Subscriptions Both Ways)"),
}
}
}
// none
// >
// >>
// <
// <<
// ><
// >><
// ><<
// >><<
impl From<stanza::roster::Item> for Contact {
fn from(value: stanza::roster::Item) -> Self {
let subscription = match value.ask {
true => match value.subscription {
Some(s) => match s {
stanza::roster::Subscription::Both => Subscription::Buddy,
stanza::roster::Subscription::From => Subscription::InPendingOut,
stanza::roster::Subscription::None => Subscription::PendingOut,
stanza::roster::Subscription::Remove => Subscription::PendingOut,
stanza::roster::Subscription::To => Subscription::OnlyOut,
},
None => Subscription::PendingOut,
},
false => match value.subscription {
Some(s) => match s {
stanza::roster::Subscription::Both => Subscription::Buddy,
stanza::roster::Subscription::From => Subscription::OnlyIn,
stanza::roster::Subscription::None => Subscription::None,
stanza::roster::Subscription::Remove => Subscription::None,
stanza::roster::Subscription::To => Subscription::OnlyOut,
},
None => Subscription::None,
},
};
Contact {
user_jid: value.jid,
subscription,
name: value.name,
groups: HashSet::from_iter(value.groups.into_iter().filter_map(|group| group.0)),
}
}
}
|