add: connect to socket by community
direct messages/groups connect by channel id, everything else should connect by channel with the "is_channel" header set to true
This commit is contained in:
parent
c1c8cdbfcd
commit
0304461389
20 changed files with 241 additions and 160 deletions
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -533,9 +533,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.40"
|
version = "0.4.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
@ -716,28 +716,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-queue",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
@ -757,15 +735,6 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-queue"
|
|
||||||
version = "0.3.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
|
@ -3271,11 +3240,11 @@ dependencies = [
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"cf-turnstile",
|
"cf-turnstile",
|
||||||
"contrasted",
|
"contrasted",
|
||||||
"crossbeam",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"image",
|
"image",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
|
"redis",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3299,7 +3268,6 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bb8-postgres",
|
"bb8-postgres",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"futures-util",
|
|
||||||
"md-5",
|
"md-5",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"redis",
|
"redis",
|
||||||
|
@ -3556,9 +3524,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.21"
|
version = "0.8.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231"
|
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
|
@ -3577,9 +3545,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.25"
|
version = "0.22.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485"
|
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3591,9 +3559,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_write"
|
name = "toml_write"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976"
|
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "totp-rs"
|
name = "totp-rs"
|
||||||
|
|
|
@ -6,7 +6,7 @@ edition = "2024"
|
||||||
[features]
|
[features]
|
||||||
postgres = ["tetratto-core/postgres"]
|
postgres = ["tetratto-core/postgres"]
|
||||||
sqlite = ["tetratto-core/sqlite"]
|
sqlite = ["tetratto-core/sqlite"]
|
||||||
redis = ["tetratto-core/redis"]
|
redis = ["tetratto-core/redis", "dep:redis"]
|
||||||
default = ["sqlite", "redis"]
|
default = ["sqlite", "redis"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -34,4 +34,5 @@ mime_guess = "2.0.5"
|
||||||
cf-turnstile = "0.2.0"
|
cf-turnstile = "0.2.0"
|
||||||
contrasted = "0.1.2"
|
contrasted = "0.1.2"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
|
|
||||||
|
redis = { version = "0.30.0", optional = true }
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
<a
|
<a
|
||||||
href="/chats/0/0"
|
href="/chats/0/0"
|
||||||
class="button quaternary channel_icon {% if selected_community == 0 %}selected{% endif %}"
|
class="button quaternary channel_icon {% if selected_community == 0 %}selected{% endif %}"
|
||||||
|
data-turbo="false"
|
||||||
>
|
>
|
||||||
{{ icon "message-circle" }}
|
{{ icon "message-circle" }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
<a
|
<a
|
||||||
href="/chats/{{ community.id }}/0"
|
href="/chats/{{ community.id }}/0"
|
||||||
class="button quaternary channel_icon {% if selected_community == community.id %}selected{% endif %}"
|
class="button quaternary channel_icon {% if selected_community == community.id %}selected{% endif %}"
|
||||||
|
data-turbo="false"
|
||||||
>
|
>
|
||||||
{{ components::community_avatar(id=community.id,
|
{{ components::community_avatar(id=community.id,
|
||||||
community=community, size="48px") }}
|
community=community, size="48px") }}
|
||||||
|
@ -440,64 +442,76 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% if selected_channel %}
|
<script id="socket_init" data-turbo-permanent="true">
|
||||||
<script>
|
window.SUBSCRIBE_CHANNEL = "{{ selected_community }}" === "0";
|
||||||
setTimeout(() => {
|
globalThis.socket_init = () => {
|
||||||
if (window.socket) {
|
if (window.socket) {
|
||||||
if (window.socket_id === "{{ selected_channel }}") {
|
window.socket.send("Close");
|
||||||
console.log("cannot open; already in session");
|
window.socket.close();
|
||||||
return;
|
window.socket = undefined;
|
||||||
} else {
|
console.log("closed lingering");
|
||||||
window.socket.close();
|
|
||||||
window.socket = undefined;
|
|
||||||
console.log("closed lingering");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const anchor of document.querySelectorAll("a")) {
|
if ("{{ selected_community }}" !== "0") {
|
||||||
if (anchor.href.includes("{{ selected_channel }}")) {
|
const endpoint = `${window.location.origin.replace("http", "ws")}/api/v1/_connect/{{ selected_community }}`;
|
||||||
continue;
|
const socket = new WebSocket(endpoint);
|
||||||
}
|
window.socket = socket;
|
||||||
|
window.socket_id = "{{ selected_community }}";
|
||||||
anchor.addEventListener("click", () => {
|
} else {
|
||||||
window.socket.close();
|
const endpoint = `${window.location.origin.replace("http", "ws")}/api/v1/_connect/{{ selected_channel }}`;
|
||||||
window.socket = undefined;
|
const socket = new WebSocket(endpoint);
|
||||||
console.log("force abandon socket");
|
window.socket = socket;
|
||||||
});
|
window.socket_id = "{{ selected_channel }}";
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${window.location.origin.replace("http", "ws")}/api/v1/channels/{{ selected_channel }}/ws`;
|
window.socket.addEventListener("open", () => {
|
||||||
const socket = new WebSocket(endpoint);
|
|
||||||
window.socket = socket;
|
|
||||||
window.socket_id = "{{ selected_channel }}";
|
|
||||||
|
|
||||||
socket.addEventListener("close", () => {
|
|
||||||
return socket.send("Close");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener("open", () => {
|
|
||||||
// auth
|
// auth
|
||||||
socket.send(
|
window.socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
method: "Headers",
|
method: "Headers",
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
// SocketHeaders
|
// SocketHeaders
|
||||||
channel: "{{ selected_channel }}",
|
|
||||||
user: "{{ user.id }}",
|
user: "{{ user.id }}",
|
||||||
|
is_channel: window.SUBSCRIBE_CHANNEL,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
socket.addEventListener("message", async (event) => {
|
{% if selected_channel %}
|
||||||
|
<script>
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!window.SUBSCRIBE_CHANNEL) {
|
||||||
|
// sub community
|
||||||
|
if (window.socket_id !== "{{ selected_community }}") {
|
||||||
|
socket_init();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// sub channel
|
||||||
|
if (window.socket_id !== "{{ selected_channel }}") {
|
||||||
|
socket_init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.socket.addEventListener("message", async (event) => {
|
||||||
if (event.data === "Ping") {
|
if (event.data === "Ping") {
|
||||||
return socket.send("Pong");
|
return socket.send("Pong");
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
const data = JSON.parse(msg.data);
|
const [channel_id, data] = JSON.parse(msg.data);
|
||||||
|
|
||||||
if (msg.method === "Message" && window.CURRENT_PAGE === 0) {
|
if (msg.method === "Message" && window.CURRENT_PAGE === 0) {
|
||||||
|
if (channel_id !== "{{ selected_channel }}") {
|
||||||
|
// message not for us... maybe send notification later
|
||||||
|
// something like /api/v1/messages/{id}/mark_unread
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (document.getElementById("stream_body")) {
|
if (document.getElementById("stream_body")) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.style.display = "contents";
|
element.style.display = "contents";
|
||||||
|
|
|
@ -83,7 +83,7 @@ pub async fn proxy_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let None = user.connections.get(&ConnectionService::LastFm) {
|
if user.connections.get(&ConnectionService::LastFm).is_none() {
|
||||||
// connection doesn't exist
|
// connection doesn't exist
|
||||||
return Json(Error::GeneralNotFound("connection".to_string()).into());
|
return Json(Error::GeneralNotFound("connection".to_string()).into());
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,11 +66,11 @@ pub async fn me_request(jar: CookieJar, Extension(data): Extension<State>) -> im
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "User exists".to_string(),
|
message: "User exists".to_string(),
|
||||||
payload: Some(user),
|
payload: Some(user),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the settings of the given user.
|
/// Update the settings of the given user.
|
||||||
|
|
|
@ -63,7 +63,7 @@ pub async fn create_group_request(
|
||||||
|
|
||||||
// check for existing
|
// check for existing
|
||||||
if members.len() == 1 {
|
if members.len() == 1 {
|
||||||
let other_user = members.get(0).unwrap().to_owned();
|
let other_user = members.first().unwrap().to_owned();
|
||||||
if let Ok(channel) = data.get_channel_by_owner_member(user.id, other_user).await {
|
if let Ok(channel) = data.get_channel_by_owner_member(user.id, other_user).await {
|
||||||
return Json(ApiReturn {
|
return Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -80,21 +80,18 @@ pub async fn create_group_request(
|
||||||
Err(e) => return Json(e.into()),
|
Err(e) => return Json(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if other_user.settings.private_chats {
|
if other_user.settings.private_chats && data
|
||||||
if data
|
|
||||||
.get_userfollow_by_initiator_receiver(other_user.id, user.id)
|
.get_userfollow_by_initiator_receiver(other_user.id, user.id)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err() {
|
||||||
{
|
return Json(Error::NotAllowed.into());
|
||||||
return Json(Error::NotAllowed.into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let mut props = Channel::new(0, user.id, 0, req.title);
|
let mut props = Channel::new(0, user.id, 0, req.title);
|
||||||
props.members = members;
|
props.members = members;
|
||||||
let id = props.id.clone();
|
let id = props.id;
|
||||||
|
|
||||||
match data.create_channel(props).await {
|
match data.create_channel(props).await {
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
use redis::Commands;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
ws::{Message as WsMessage, WebSocket, WebSocketUpgrade},
|
ws::{Message as WsMessage, WebSocket, WebSocketUpgrade},
|
||||||
|
@ -12,7 +14,7 @@ use tetratto_core::{
|
||||||
model::{
|
model::{
|
||||||
auth::User,
|
auth::User,
|
||||||
channels::Message,
|
channels::Message,
|
||||||
socket::{SocketMessage, SocketMethod},
|
socket::{PacketType, SocketMessage, SocketMethod},
|
||||||
ApiReturn, Error,
|
ApiReturn, Error,
|
||||||
},
|
},
|
||||||
DataManager,
|
DataManager,
|
||||||
|
@ -21,10 +23,10 @@ use crate::{get_user_from_token, routes::api::v1::CreateMessage, State};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use futures_util::{sink::SinkExt, stream::StreamExt};
|
use futures_util::{sink::SinkExt, stream::StreamExt};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct SocketHeaders {
|
pub struct SocketHeaders {
|
||||||
pub channel: String,
|
|
||||||
pub user: String,
|
pub user: String,
|
||||||
|
pub is_channel: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a subscription to the websocket.
|
/// Handle a subscription to the websocket.
|
||||||
|
@ -43,11 +45,11 @@ pub async fn subscription_handler(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: String) {
|
pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: String) {
|
||||||
let (mut sink, mut stream) = socket.split();
|
let (mut sink, mut stream) = socket.split();
|
||||||
|
|
||||||
let mut user: Option<User> = None;
|
let mut user: Option<User> = None;
|
||||||
let mut con = db.2.clone().get_con().await;
|
let mut headers: Option<SocketHeaders> = None;
|
||||||
|
|
||||||
// handle incoming messages on socket
|
// handle incoming messages on socket
|
||||||
let dbc = db.clone();
|
let dbc = db.clone();
|
||||||
|
@ -60,7 +62,7 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: Strin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.method != SocketMethod::Headers && user.is_none() {
|
if data.method != SocketMethod::Headers && user.is_none() && headers.is_none() {
|
||||||
// we've sent something else before authenticating... that's not right
|
// we've sent something else before authenticating... that's not right
|
||||||
let _ = sink.close().await;
|
let _ = sink.close().await;
|
||||||
return;
|
return;
|
||||||
|
@ -70,6 +72,7 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: Strin
|
||||||
SocketMethod::Headers => {
|
SocketMethod::Headers => {
|
||||||
let data: SocketHeaders = data.data();
|
let data: SocketHeaders = data.data();
|
||||||
|
|
||||||
|
headers = Some(data.clone());
|
||||||
user = Some(
|
user = Some(
|
||||||
match dbc
|
match dbc
|
||||||
.get_user_by_id(match data.user.parse::<usize>() {
|
.get_user_by_id(match data.user.parse::<usize>() {
|
||||||
|
@ -89,39 +92,42 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: Strin
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let channel = match dbc
|
if data.is_channel {
|
||||||
.get_channel_by_id(match data.channel.parse::<usize>() {
|
// verify permissions for single channel
|
||||||
|
let channel = match dbc
|
||||||
|
.get_channel_by_id(match community_id.parse::<usize>() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => {
|
||||||
|
let _ = sink.close().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = sink.close().await;
|
let _ = sink.close().await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
.await
|
|
||||||
{
|
let user = user.as_ref().unwrap();
|
||||||
Ok(c) => c,
|
|
||||||
Err(_) => {
|
let membership = match dbc
|
||||||
|
.get_membership_by_owner_community(user.id, channel.id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(_) => {
|
||||||
|
let _ = sink.close().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !channel.check_read(user.id, Some(membership.role)) {
|
||||||
let _ = sink.close().await;
|
let _ = sink.close().await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let user = user.as_ref().unwrap();
|
|
||||||
|
|
||||||
let membership = match dbc
|
|
||||||
.get_membership_by_owner_community(user.id, channel.id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ua) => ua,
|
|
||||||
Err(_) => {
|
|
||||||
let _ = sink.close().await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !channel.check_read(user.id, Some(membership.role)) {
|
|
||||||
let _ = sink.close().await;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -134,6 +140,37 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: Strin
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get channel permissions
|
||||||
|
let user = user.unwrap();
|
||||||
|
let headers = headers.unwrap();
|
||||||
|
|
||||||
|
let mut channel_read_statuses: HashMap<usize, bool> = HashMap::new();
|
||||||
|
if !headers.is_channel {
|
||||||
|
// check permissions for every channel in community
|
||||||
|
let community_id = match community_id.parse::<usize>() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let membership = match dbc
|
||||||
|
.get_membership_by_owner_community(user.id, community_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(_) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for channel in dbc.get_channels_by_community(community_id).await.unwrap() {
|
||||||
|
channel_read_statuses.insert(
|
||||||
|
channel.id,
|
||||||
|
channel.check_read(user.id, Some(membership.role)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
let mut recv_task = tokio::spawn(async move {
|
let mut recv_task = tokio::spawn(async move {
|
||||||
while let Some(Ok(WsMessage::Text(text))) = stream.next().await {
|
while let Some(Ok(WsMessage::Text(text))) = stream.next().await {
|
||||||
if text != "Close" {
|
if text != "Close" {
|
||||||
|
@ -147,28 +184,86 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, channel_id: Strin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let dbc = db.clone();
|
||||||
let mut redis_task = tokio::spawn(async move {
|
let mut redis_task = tokio::spawn(async move {
|
||||||
// forward messages from redis to the mpsc
|
// forward messages from redis to the mpsc
|
||||||
|
let mut con = dbc.2.get_con().await;
|
||||||
let mut pubsub = con.as_pubsub();
|
let mut pubsub = con.as_pubsub();
|
||||||
pubsub.subscribe(channel_id).unwrap();
|
|
||||||
|
|
||||||
|
pubsub.subscribe(user.id).unwrap();
|
||||||
|
pubsub.subscribe(community_id.clone()).unwrap();
|
||||||
|
|
||||||
|
// listen for pubsub messages
|
||||||
while let Ok(msg) = pubsub.get_message() {
|
while let Ok(msg) = pubsub.get_message() {
|
||||||
// payload is a stringified SocketMessage
|
// payload is a stringified SocketMessage
|
||||||
if sink
|
let smsg = msg.get_payload::<String>().unwrap();
|
||||||
.send(WsMessage::Text(msg.get_payload::<String>().unwrap().into()))
|
let packet: SocketMessage = serde_json::from_str(&smsg).unwrap();
|
||||||
.await
|
|
||||||
.is_err()
|
if packet.method == SocketMethod::Forward(PacketType::Ping) {
|
||||||
{
|
// forward with custom message
|
||||||
return;
|
if sink.send(WsMessage::Text("Ping".into())).await.is_err() {
|
||||||
|
drop(sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if packet.method == SocketMethod::Message {
|
||||||
|
// check perms and then forward
|
||||||
|
let d: (String, Message) = packet.data();
|
||||||
|
|
||||||
|
if let Some(cs) = channel_read_statuses.get(&d.1.channel) {
|
||||||
|
if !cs {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !headers.is_channel {
|
||||||
|
// since we didn't select by just a channel, there HAS to be
|
||||||
|
// an entry for the channel for us to check this message
|
||||||
|
continue;
|
||||||
|
// we don't need to check messages when we're subscribed to
|
||||||
|
// a channel, since that is checked on headers submission when
|
||||||
|
// we subscribe to a channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
|
||||||
|
drop(sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// forward to client
|
||||||
|
if sink.send(WsMessage::Text(smsg.into())).await.is_err() {
|
||||||
|
drop(sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let db2c = db.2.clone();
|
||||||
|
let heartbeat_task = tokio::spawn(async move {
|
||||||
|
let mut con = db2c.get_con().await;
|
||||||
|
let mut heartbeat = tokio::time::interval(Duration::from_secs(10));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
con.publish::<usize, String, ()>(
|
||||||
|
user.id,
|
||||||
|
serde_json::to_string(&SocketMessage {
|
||||||
|
method: SocketMethod::Forward(PacketType::Ping),
|
||||||
|
data: "Ping".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
heartbeat.tick().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = (&mut recv_task) => redis_task.abort(),
|
_ = (&mut recv_task) => redis_task.abort(),
|
||||||
_ = (&mut redis_task) => recv_task.abort()
|
_ = (&mut redis_task) => recv_task.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
heartbeat_task.abort(); // kill
|
||||||
tracing::info!("socket terminate");
|
tracing::info!("socket terminate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -290,7 +290,7 @@ pub fn routes() -> Router {
|
||||||
)
|
)
|
||||||
// messages
|
// messages
|
||||||
.route(
|
.route(
|
||||||
"/channels/{id}/ws",
|
"/_connect/{id}",
|
||||||
any(channels::messages::subscription_handler),
|
any(channels::messages::subscription_handler),
|
||||||
)
|
)
|
||||||
.route("/messages", post(channels::messages::create_request))
|
.route("/messages", post(channels::messages::create_request))
|
||||||
|
|
|
@ -211,7 +211,7 @@ pub async fn message_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let message: Message = match serde_json::from_str(&req.data) {
|
let message: (String, Message) = match serde_json::from_str(&req.data) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(Html(
|
return Err(Html(
|
||||||
|
@ -220,6 +220,8 @@ pub async fn message_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let message = message.1;
|
||||||
|
|
||||||
let membership = match data
|
let membership = match data
|
||||||
.0
|
.0
|
||||||
.get_membership_by_owner_community(user.id, community)
|
.get_membership_by_owner_community(user.id, community)
|
||||||
|
|
|
@ -12,7 +12,7 @@ default = ["sqlite", "redis"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
toml = "0.8.21"
|
toml = "0.8.22"
|
||||||
tetratto-shared = { path = "../shared" }
|
tetratto-shared = { path = "../shared" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
|
@ -30,4 +30,3 @@ rusqlite = { version = "0.35.0", optional = true }
|
||||||
tokio-postgres = { version = "0.7.13", optional = true }
|
tokio-postgres = { version = "0.7.13", optional = true }
|
||||||
bb8-postgres = { version = "0.9.0", optional = true }
|
bb8-postgres = { version = "0.9.0", optional = true }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
futures-util = "0.3.31"
|
|
||||||
|
|
|
@ -151,6 +151,7 @@ impl Default for TurnstileConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
#[derive(Default)]
|
||||||
pub struct ConnectionsConfig {
|
pub struct ConnectionsConfig {
|
||||||
/// <https://developer.spotify.com/documentation/web-api>
|
/// <https://developer.spotify.com/documentation/web-api>
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -163,15 +164,6 @@ pub struct ConnectionsConfig {
|
||||||
pub last_fm_secret: Option<String>,
|
pub last_fm_secret: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectionsConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
spotify_client_id: None,
|
|
||||||
last_fm_key: None,
|
|
||||||
last_fm_secret: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration file
|
/// Configuration file
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
|
|
@ -528,7 +528,7 @@ macro_rules! auto_method {
|
||||||
if !user.permissions.check(FinePermission::$permission) {
|
if !user.permissions.check(FinePermission::$permission) {
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
} else {
|
} else {
|
||||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
|
||||||
user.id,
|
user.id,
|
||||||
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
format!("invoked `{}` with x value `{x}`", stringify!($name)),
|
||||||
))
|
))
|
||||||
|
@ -607,7 +607,7 @@ macro_rules! auto_method {
|
||||||
if !user.permissions.check(FinePermission::$permission) {
|
if !user.permissions.check(FinePermission::$permission) {
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
} else {
|
} else {
|
||||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
|
||||||
user.id,
|
user.id,
|
||||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||||
))
|
))
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl DataManager {
|
||||||
pub async fn fill_messages(
|
pub async fn fill_messages(
|
||||||
&self,
|
&self,
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
ignore_users: &Vec<usize>,
|
ignore_users: &[usize],
|
||||||
) -> Result<Vec<(Message, User)>> {
|
) -> Result<Vec<(Message, User)>> {
|
||||||
let mut out: Vec<(Message, User)> = Vec::new();
|
let mut out: Vec<(Message, User)> = Vec::new();
|
||||||
|
|
||||||
|
@ -158,10 +158,16 @@ impl DataManager {
|
||||||
let mut con = self.2.get_con().await;
|
let mut con = self.2.get_con().await;
|
||||||
|
|
||||||
if let Err(e) = con.publish::<usize, String, ()>(
|
if let Err(e) = con.publish::<usize, String, ()>(
|
||||||
data.channel,
|
if channel.community != 0 {
|
||||||
|
// broadcast to community ws
|
||||||
|
channel.community
|
||||||
|
} else {
|
||||||
|
// broadcast to channel ws
|
||||||
|
channel.id
|
||||||
|
},
|
||||||
serde_json::to_string(&SocketMessage {
|
serde_json::to_string(&SocketMessage {
|
||||||
method: SocketMethod::Message,
|
method: SocketMethod::Message,
|
||||||
data: serde_json::to_string(&data).unwrap(),
|
data: serde_json::to_string(&(data.channel.to_string(), data)).unwrap(),
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
) {
|
) {
|
||||||
|
@ -211,7 +217,13 @@ impl DataManager {
|
||||||
let mut con = self.2.get_con().await;
|
let mut con = self.2.get_con().await;
|
||||||
|
|
||||||
if let Err(e) = con.publish::<usize, String, ()>(
|
if let Err(e) = con.publish::<usize, String, ()>(
|
||||||
message.channel,
|
if channel.community != 0 {
|
||||||
|
// broadcast to community ws
|
||||||
|
channel.community
|
||||||
|
} else {
|
||||||
|
// broadcast to channel ws
|
||||||
|
channel.id
|
||||||
|
},
|
||||||
serde_json::to_string(&SocketMessage {
|
serde_json::to_string(&SocketMessage {
|
||||||
method: SocketMethod::Delete,
|
method: SocketMethod::Delete,
|
||||||
data: serde_json::to_string(&DeleteMessageEvent { id: id.to_string() }).unwrap(),
|
data: serde_json::to_string(&DeleteMessageEvent { id: id.to_string() }).unwrap(),
|
||||||
|
|
|
@ -121,7 +121,7 @@ impl DataManager {
|
||||||
pub async fn fill_posts(
|
pub async fn fill_posts(
|
||||||
&self,
|
&self,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
ignore_users: &Vec<usize>,
|
ignore_users: &[usize],
|
||||||
) -> Result<Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)>> {
|
) -> Result<Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)>> {
|
||||||
let mut out: Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)> = Vec::new();
|
let mut out: Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)> = Vec::new();
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ impl DataManager {
|
||||||
&self,
|
&self,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
user_id: usize,
|
user_id: usize,
|
||||||
ignore_users: &Vec<usize>,
|
ignore_users: &[usize],
|
||||||
) -> Result<
|
) -> Result<
|
||||||
Vec<(
|
Vec<(
|
||||||
Post,
|
Post,
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl DataManager {
|
||||||
pub async fn fill_questions(
|
pub async fn fill_questions(
|
||||||
&self,
|
&self,
|
||||||
questions: Vec<Question>,
|
questions: Vec<Question>,
|
||||||
ignore_users: &Vec<usize>,
|
ignore_users: &[usize],
|
||||||
) -> Result<Vec<(Question, User)>> {
|
) -> Result<Vec<(Question, User)>> {
|
||||||
let mut out: Vec<(Question, User)> = Vec::new();
|
let mut out: Vec<(Question, User)> = Vec::new();
|
||||||
|
|
||||||
|
|
|
@ -371,19 +371,12 @@ pub struct ExternalConnectionInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[derive(Default)]
|
||||||
pub struct ExternalConnectionData {
|
pub struct ExternalConnectionData {
|
||||||
pub external_urls: HashMap<String, String>,
|
pub external_urls: HashMap<String, String>,
|
||||||
pub data: HashMap<String, String>,
|
pub data: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ExternalConnectionData {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
external_urls: HashMap::new(),
|
|
||||||
data: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Notification {
|
pub struct Notification {
|
||||||
|
|
|
@ -93,7 +93,7 @@ impl Message {
|
||||||
created: now,
|
created: now,
|
||||||
edited: now,
|
edited: now,
|
||||||
content,
|
content,
|
||||||
context: MessageContext::default(),
|
context: MessageContext,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum PacketType {
|
||||||
|
/// A regular check to ensure the connection is still alive.
|
||||||
|
Ping,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum SocketMethod {
|
pub enum SocketMethod {
|
||||||
/// Authentication and channel identification.
|
/// Authentication and channel identification.
|
||||||
|
@ -8,6 +14,8 @@ pub enum SocketMethod {
|
||||||
Message,
|
Message,
|
||||||
/// A message was deleted in the channel.
|
/// A message was deleted in the channel.
|
||||||
Delete,
|
Delete,
|
||||||
|
/// Forward message from server to client. (Redis pubsub to ws)
|
||||||
|
Forward(PacketType),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
|
@ -9,4 +9,4 @@ license.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
toml = "0.8.21"
|
toml = "0.8.22"
|
||||||
|
|
|
@ -8,7 +8,7 @@ license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "4.1.0"
|
ammonia = "4.1.0"
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.41"
|
||||||
comrak = "0.38.0"
|
comrak = "0.38.0"
|
||||||
hex_fmt = "0.3.0"
|
hex_fmt = "0.3.0"
|
||||||
num-bigint = "0.4.6"
|
num-bigint = "0.4.6"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue