Skip to content

Commit 104c6c0

Browse files
authored
Merge pull request #6 from echo-webkom/omfj/queue-ws
2 parents 8fc110d + 1e188f4 commit 104c6c0

File tree

8 files changed

+142
-9
lines changed

8 files changed

+142
-9
lines changed

Cargo.lock

Lines changed: 34 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2024"
66
[dependencies]
77
anyhow = "1.0.101"
88
async-trait = "0.1"
9-
axum = "0.8.8"
9+
axum = { version = "0.8.8", features = ["ws"] }
1010
chrono = { version = "0.4", features = ["serde"] }
1111
dotenv = "0.15.0"
1212
poise = "0.6.1"

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44

55
https://discord.com/oauth2/authorize?client_id=1367651749262921868&permissions=277025458240&integration_type=0&scope=bot
66

7+
## Create your own
8+
9+
You can easily create your own version of this bot.
10+
11+
It needs the premissions in 0Auth2:
12+
13+
- Bot
14+
- Create commands
15+
- Send messages
16+
- Send messages in Threads
17+
- Read Message History
18+
- Add Reactions
19+
- Use Appliation Commands
20+
721
## How to run
822

923
Make sure that you have copied `.env.example` to `.env` and filled in the missing enviornment variables.

src/adapters/http/mod.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use axum::{
2+
extract::{
3+
ws::{Message, WebSocket, WebSocketUpgrade},
4+
Path, State,
5+
},
6+
response::Response,
7+
routing::{any, get},
28
Json, Router,
3-
extract::{Path, State},
4-
routing::get,
59
};
610
use tower::ServiceBuilder;
711
use tower_http::trace::TraceLayer;
812
use tracing::info;
913

1014
use std::{io, sync::Arc};
1115

12-
use crate::domain::{OrderRepository, QueueEntry, QueueRepository};
16+
use crate::domain::{OrderRepository, QueueEntry, QueueEvent, QueueRepository};
1317

1418
#[derive(Clone)]
1519
pub struct AppState {
@@ -35,7 +39,9 @@ impl HttpAdapter {
3539
});
3640

3741
let app = Router::new()
42+
.route("/{guild_id}/status", get(queue_status))
3843
.route("/{guild_id}/queue", get(list_queue))
44+
.route("/{guild_id}/queue/ws", any(list_queue_ws))
3945
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
4046
.with_state(state);
4147

@@ -46,10 +52,51 @@ impl HttpAdapter {
4652
}
4753
}
4854

55+
async fn queue_status(State(state): State<Arc<AppState>>, Path(guild_id): Path<String>) -> String {
56+
let is_open = state.queue.is_open(&guild_id);
57+
let status = if is_open { "open" } else { "closed" };
58+
status.to_string()
59+
}
60+
4961
async fn list_queue(
5062
State(state): State<Arc<AppState>>,
5163
Path(guild_id): Path<String>,
5264
) -> Json<Vec<QueueEntry>> {
5365
let queue = state.queue.list(&guild_id).await;
5466
Json(queue)
5567
}
68+
69+
async fn list_queue_ws(
70+
State(state): State<Arc<AppState>>,
71+
Path(guild_id): Path<String>,
72+
ws: WebSocketUpgrade,
73+
) -> Response {
74+
ws.on_upgrade(move |socket| list_queue_ws_handler(state, guild_id, socket))
75+
}
76+
77+
async fn list_queue_ws_handler(state: Arc<AppState>, guild_id: String, mut socket: WebSocket) {
78+
info!("new websocket connection for guild_id: {}", guild_id);
79+
80+
let mut rx = state.queue.subscribe();
81+
82+
// Initial state
83+
let queue = state.queue.list(&guild_id).await;
84+
let msg = serde_json::to_string(&queue).unwrap();
85+
if socket.send(Message::Text(msg.into())).await.is_err() {
86+
return;
87+
}
88+
89+
while let Ok(event) = rx.recv().await {
90+
match event {
91+
QueueEvent::Updated { guild_id: gid } => {
92+
if gid == guild_id {
93+
let queue = state.queue.list(&guild_id).await;
94+
let msg = serde_json::to_string(&queue).unwrap();
95+
if socket.send(Message::Text(msg.into())).await.is_err() {
96+
break;
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}

src/domain/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ pub mod order;
22
pub mod queue;
33

44
pub use order::{DailyStats, OrderRepository};
5-
pub use queue::{QueueEntry, QueueRepository};
5+
pub use queue::{QueueEntry, QueueEvent, QueueRepository};

src/domain/queue.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ impl QueueEntry {
1313
}
1414
}
1515

16+
#[derive(Debug, Clone)]
17+
pub enum QueueEvent {
18+
Updated { guild_id: String },
19+
}
20+
1621
#[async_trait::async_trait]
1722
pub trait QueueRepository: Send + Sync {
1823
/// Open the queue to allow new entries
@@ -47,4 +52,7 @@ pub trait QueueRepository: Send + Sync {
4752

4853
/// Clear the queue
4954
async fn clear(&self, guild_id: &str);
55+
56+
/// Subscribe to queue change events
57+
fn subscribe(&self) -> tokio::sync::broadcast::Receiver<QueueEvent>;
5058
}

src/infrastructure/redis_queue_repository.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use std::{collections::HashSet, sync::RwLock};
22

33
use redis::AsyncCommands;
4+
use tokio::sync::broadcast;
45
use tracing::{debug, error, info, instrument, warn};
56

6-
use crate::domain::{QueueEntry, QueueRepository};
7+
use crate::domain::{QueueEntry, QueueEvent, QueueRepository};
78

89
fn queue_key(guild_id: &str) -> String {
910
format!("queue:{guild_id}")
@@ -12,13 +13,16 @@ fn queue_key(guild_id: &str) -> String {
1213
pub struct RedisQueueRepository {
1314
redis: redis::Client,
1415
open_guilds: RwLock<HashSet<String>>,
16+
event_tx: broadcast::Sender<QueueEvent>,
1517
}
1618

1719
impl RedisQueueRepository {
1820
pub fn new(redis: redis::Client) -> Self {
21+
let (event_tx, _) = broadcast::channel(64);
1922
Self {
2023
redis,
2124
open_guilds: RwLock::new(HashSet::new()),
25+
event_tx,
2226
}
2327
}
2428
}
@@ -39,6 +43,7 @@ impl QueueRepository for RedisQueueRepository {
3943
info!(guild_id, "Closing queue for guild");
4044
self.open_guilds.write().unwrap().remove(guild_id);
4145
self.clear(guild_id).await;
46+
self.broadcast_update(guild_id);
4247
}
4348

4449
#[instrument(skip(self), fields(guild_id))]
@@ -111,6 +116,7 @@ impl QueueRepository for RedisQueueRepository {
111116
0
112117
});
113118
info!(guild_id, user_id = %entry.user_id, queue_size = new_size, "Added user to queue");
119+
self.broadcast_update(guild_id);
114120
new_size
115121
}
116122

@@ -134,6 +140,9 @@ impl QueueRepository for RedisQueueRepository {
134140
Some(e) => info!(guild_id, user_id = %e.user_id, "Popped user from queue"),
135141
None => debug!(guild_id, "No entry to pop from queue"),
136142
}
143+
if entry.is_some() {
144+
self.broadcast_update(guild_id);
145+
}
137146
entry
138147
}
139148

@@ -159,6 +168,9 @@ impl QueueRepository for RedisQueueRepository {
159168
.filter_map(|json_str| serde_json::from_str(&json_str).ok())
160169
.collect();
161170
info!(guild_id, count = entries.len(), "Popped entries from queue");
171+
if !entries.is_empty() {
172+
self.broadcast_update(guild_id);
173+
}
162174
entries
163175
}
164176

@@ -201,6 +213,26 @@ impl QueueRepository for RedisQueueRepository {
201213
} else {
202214
error!(guild_id, "Failed to get Redis connection for clear");
203215
}
216+
self.broadcast_update(guild_id);
217+
}
218+
219+
fn subscribe(&self) -> tokio::sync::broadcast::Receiver<QueueEvent> {
220+
self.event_tx.subscribe()
221+
}
222+
}
223+
224+
impl RedisQueueRepository {
225+
fn broadcast_update(&self, guild_id: &str) {
226+
if let Err(err) = self.event_tx.send(QueueEvent::Updated {
227+
guild_id: guild_id.to_string(),
228+
}) {
229+
error!(
230+
guild_id,
231+
"error" = ?err,
232+
"guild_id" = guild_id,
233+
"Failed to broadcast queue update event"
234+
);
235+
}
204236
}
205237
}
206238

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::env;
22

33
use tracing::{error, info};
44
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
5-
use vaffelbot_rs::{VaffelBot, config::Config};
5+
use vaffelbot_rs::{config::Config, VaffelBot};
66

77
#[tokio::main]
88
async fn main() {

0 commit comments

Comments
 (0)