initial commit

This commit is contained in:
2026-04-17 09:49:34 +02:00
commit 8553c710f2
23 changed files with 1212 additions and 0 deletions

5
bot/.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
target/
.git/
*.db
*.db-journal
.env

6
bot/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/target
Cargo.lock
*.log
.env
config.toml
*.db

22
bot/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "omegle-matrix-client"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.30", features = ["bundled"] }
toml = "0.8"
reqwest = { version = "0.12", features = ["json"] }
matrix-sdk = { version = "0.7", features = ["e2e-encryption"] }
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
url = "2"
log = "0.4"
async-trait = "0.1"
dashmap = "5"

25
bot/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM rust:1.85-bookworm AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y pkg-config libssl-dev
COPY Cargo.toml ./
COPY src ./src
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /build/target/release/omegle-matrix-client /app/
COPY config.toml /app/config.toml
RUN mkdir -p /data && chown -R 1000:1000 /data
ENV RUST_LOG=info
ENTRYPOINT ["/app/omegle-matrix-client"]

56
bot/src/config.rs Normal file
View File

@@ -0,0 +1,56 @@
use anyhow::Result;
use serde::Deserialize;
use std::env;
use std::fs;
#[derive(Deserialize, Clone)]
pub struct Config {
#[serde(default)]
pub matrix: MatrixConfig,
#[serde(default)]
pub omegle: OmegleConfig,
}
#[derive(Deserialize, Clone, Default)]
pub struct MatrixConfig {
#[serde(default)]
pub homeserver: String,
#[serde(default)]
pub username: String,
#[serde(default)]
pub password: String,
}
#[derive(Deserialize, Clone, Default)]
pub struct OmegleConfig {
#[serde(default)]
pub websocket_url: String,
}
impl Config {
pub fn load(path: &str) -> Result<Self> {
let homeserver = env::var("MATRIX_HOMESERVER").unwrap_or_default();
let username = env::var("MATRIX_USERNAME").unwrap_or_default();
let password = env::var("MATRIX_PASSWORD").unwrap_or_default();
let websocket_url = env::var("OMEGLE_WEBSOCKET_URL").unwrap_or_default();
if !homeserver.is_empty()
|| !username.is_empty()
|| !password.is_empty()
|| !websocket_url.is_empty()
{
return Ok(Config {
matrix: MatrixConfig {
homeserver,
username,
password,
},
omegle: OmegleConfig { websocket_url },
});
}
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
}

25
bot/src/main.rs Normal file
View File

@@ -0,0 +1,25 @@
mod config;
mod matrix;
mod omegle;
mod state;
mod utils;
use anyhow::Result;
use std::sync::Arc;
use crate::config::Config;
use crate::state::db::Db;
use crate::matrix::client::MatrixBot;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let config = Config::load("config.toml")?;
let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "omegle.db".to_string());
let db = Arc::new(Db::new(&db_path)?);
let bot = MatrixBot::new(config, db).await?;
bot.run().await?;
Ok(())
}

532
bot/src/matrix/client.rs Normal file
View File

@@ -0,0 +1,532 @@
use anyhow::Result;
use matrix_sdk::{
config::SyncSettings,
Client,
Room,
};
use matrix_sdk::ruma::{UserId, OwnedEventId};
use matrix_sdk::ruma::events::room::message::{
SyncRoomMessageEvent, MessageType, RoomMessageEventContent,
Relation,
};
use matrix_sdk::ruma::events::relation::Replacement;
use matrix_sdk::ruma::events::room::member::StrippedRoomMemberEvent;
use matrix_sdk::ruma::events::typing::SyncTypingEvent;
use std::sync::Arc;
use dashmap::DashMap;
use tokio::sync::mpsc;
use crate::config::Config;
use crate::state::db::Db;
use crate::omegle::client::WsOmegleClient;
use crate::omegle::OmegleProvider;
use crate::utils::flags::country_code_to_flag;
pub struct MatrixBot {
client: Client,
db: Arc<Db>,
config: Config,
handlers: Arc<DashMap<String, mpsc::Sender<BotCommand>>>,
}
#[derive(Debug)]
pub enum BotCommand {
Connect { msg_id: OwnedEventId },
Match { prefer_same_country: bool, user_id: String, msg_id: OwnedEventId },
Skip { user_id: String, msg_id: OwnedEventId },
Pause,
Disconnect,
SendMessage(String),
SendTyping(bool),
}
// Helper functions for nice messages
async fn send_text(room: &Room, text: &str) -> Result<OwnedEventId> {
let content = RoomMessageEventContent::text_plain(text);
let resp = room.send(content).await?;
Ok(resp.event_id)
}
async fn send_info(room: &Room, text: &str) -> Result<OwnedEventId> {
let html = format!("<i>{}</i>", text);
let content = RoomMessageEventContent::text_html(text, html);
let resp = room.send(content).await?;
Ok(resp.event_id)
}
async fn edit_info(room: &Room, event_id: OwnedEventId, text: &str) -> Result<OwnedEventId> {
let html = format!("<i>{}</i>", text);
let mut content = RoomMessageEventContent::text_html(text, html);
content.relates_to = Some(Relation::Replacement(Replacement::new(event_id, content.clone().into())));
let resp = room.send(content).await?;
Ok(resp.event_id)
}
async fn send_status(room: &Room, text: &str, emoji: &str) -> Result<OwnedEventId> {
let body = format!("{} {}", emoji, text);
let html = format!("<b>{}</b> {}", emoji, text);
let content = RoomMessageEventContent::text_html(body, html);
let resp = room.send(content).await?;
Ok(resp.event_id)
}
async fn edit_status(room: &Room, event_id: OwnedEventId, text: &str, emoji: &str) -> Result<OwnedEventId> {
let body = format!("{} {}", emoji, text);
let html = format!("<b>{}</b> {}", emoji, text);
let mut content = RoomMessageEventContent::text_html(body, html);
content.relates_to = Some(Relation::Replacement(Replacement::new(event_id, content.clone().into())));
let resp = room.send(content).await?;
Ok(resp.event_id)
}
impl MatrixBot {
pub async fn new(config: Config, db: Arc<Db>) -> Result<Self> {
let user_id = UserId::parse(&config.matrix.username)?;
let client = Client::builder()
.homeserver_url(&config.matrix.homeserver)
.build()
.await?;
client.matrix_auth().login_username(&user_id, &config.matrix.password).send().await?;
Ok(Self {
client,
db,
config,
handlers: Arc::new(DashMap::new()),
})
}
pub async fn run(self) -> Result<()> {
let db_inv = self.db.clone();
self.client.add_event_handler(move |ev: StrippedRoomMemberEvent, room: Room, client: Client| {
let _ = db_inv.clone();
async move {
if room.state() == matrix_sdk::RoomState::Invited {
if let Some(user_id) = client.user_id() {
if ev.state_key == user_id.to_string() {
let _ = room.join().await;
}
}
}
}
});
let db_msg = self.db.clone();
let config_msg = self.config.clone();
let handlers_msg = self.handlers.clone();
self.client.add_event_handler(move |ev: SyncRoomMessageEvent, room: Room, client: Client| {
let db = db_msg.clone();
let config = config_msg.clone();
let handlers = handlers_msg.clone();
async move {
if room.state() == matrix_sdk::RoomState::Joined {
let room_id = room.room_id().to_string();
if let matrix_sdk::ruma::events::room::message::SyncRoomMessageEvent::Original(original) = ev {
// Skip messages from self
if let Some(user_id) = client.user_id() {
if original.sender == user_id {
return;
}
}
if let MessageType::Text(text) = original.content.msgtype {
let body = text.body.trim();
if body.starts_with('!') {
handle_command(body, &original.sender, &room, &db, &config, &handlers, &client).await;
} else if let Some(tx) = handlers.get(&room_id) {
let _ = tx.send(BotCommand::SendMessage(body.to_string())).await;
}
}
}
}
}
});
let handlers_typing = self.handlers.clone();
self.client.add_event_handler(move |ev: SyncTypingEvent, room: Room, client: Client| {
let handlers = handlers_typing.clone();
async move {
if room.state() == matrix_sdk::RoomState::Joined {
let room_id = room.room_id().to_string();
if let Some(tx) = handlers.get(&room_id) {
let typing = if let Some(user_id) = client.user_id() {
ev.content.user_ids.iter().any(|id| id != user_id)
} else {
!ev.content.user_ids.is_empty()
};
let _ = tx.send(BotCommand::SendTyping(typing)).await;
}
}
}
});
self.client.sync(SyncSettings::default()).await?;
Ok(())
}
}
async fn handle_command(
body: &str,
sender: &UserId,
room: &Room,
db: &Db,
config: &Config,
handlers: &Arc<DashMap<String, mpsc::Sender<BotCommand>>>,
client: &Client,
) {
let parts: Vec<&str> = body.split_whitespace().collect();
let cmd = parts[0];
let room_id = room.room_id().to_string();
let user_id = sender.to_string();
match cmd {
"!help" => {
let help_text = "<b>Available commands:</b><br/>\
<code>!help</code> - Show this help message<br/>\
<code>!connect</code> - Connect to Omgle WebSocket<br/>\
<code>!match [--same-country]</code> - Request a new match (uses your interests)<br/>\
<code>!skip</code> - Skip current peer and automatch (uses your interests)<br/>\
<code>!stop</code> - Skip current peer without automatching<br/>\
<code>!disconnect</code> - Disconnect from Omgle WebSocket<br/>\
<code>!autoskip add/remove/list &lt;CC&gt;</code> - Manage your automatic skipping list (global)<br/>\
<code>!interests add/remove/list/clear &lt;interest&gt;</code> - Manage your interests (global)";
let plain = "Available commands:\n!help, !connect, !match, !skip, !stop, !pause, !disconnect, !autoskip, !interests";
let content = RoomMessageEventContent::text_html(plain, help_text);
let _ = room.send(content).await;
}
"!connect" => {
if handlers.contains_key(&room_id) {
let _ = send_info(room, "Already connected").await;
return;
}
let msg_id = match send_info(room, "🔄 Connecting to Omgle...").await {
Ok(id) => id,
Err(_) => return,
};
let (tx, rx) = mpsc::channel(100);
handlers.insert(room_id.clone(), tx.clone());
let room_clone = room.clone();
let config_clone = config.clone();
let db_clone = db.conn.clone();
let db_struct_clone = Arc::new(Db { conn: db_clone });
let handlers_clone = handlers.clone();
let client_clone = client.clone();
tokio::spawn(async move {
let mut omgle_client = WsOmegleClient::new();
if let Err(e) = omgle_client.connect(&config_clone.omegle.websocket_url).await {
let _ = edit_info(&room_clone, msg_id, &format!("❌ Failed to connect: {}", e)).await;
handlers_clone.remove(&room_id);
return;
}
let _ = tx.send(BotCommand::Connect { msg_id }).await;
let _ = omgle_client.request_people_online().await;
handle_omgle_session(omgle_client, rx, room_clone, db_struct_clone, room_id.clone(), handlers_clone, client_clone).await;
});
}
"!match" => {
if let Some(tx) = handlers.get(&room_id) {
let prefer_same_country = parts.contains(&"--same-country");
let msg_id = match send_info(room, "🔍 Matching...").await {
Ok(id) => id,
Err(_) => return,
};
let _ = tx.send(BotCommand::Match { prefer_same_country, user_id, msg_id }).await;
} else {
let _ = send_info(room, "❌ Not connected to WebSocket. Use <code>!connect</code> first.").await;
}
}
"!interests" => {
if parts.len() < 2 { return; }
let mut config = db.get_user_config(&user_id).unwrap();
match parts[1] {
"add" => {
for &i in &parts[2..] {
config.interests.push(i.to_string());
}
db.update_user_config(&config).unwrap();
let _ = send_info(room, &format!("✅ Added interests: {:?}", &parts[2..])).await;
}
"remove" => {
config.interests.retain(|i| !parts[2..].contains(&i.as_str()));
db.update_user_config(&config).unwrap();
let _ = send_info(room, &format!("🗑️ Removed interests: {:?}", &parts[2..])).await;
}
"list" => {
let _ = send_info(room, &format!("📝 Your interests: {:?}", config.interests)).await;
}
"clear" => {
config.interests.clear();
db.update_user_config(&config).unwrap();
let _ = send_info(room, "✨ Interests cleared").await;
}
_ => {
let _ = send_info(room, "❌ Invalid <code>!interests</code> subcommand.").await;
}
}
}
"!skip" => {
if let Some(tx) = handlers.get(&room_id) {
let msg_id = match send_info(room, "⏩ Skipping...").await {
Ok(id) => id,
Err(_) => return,
};
let _ = tx.send(BotCommand::Skip { user_id, msg_id }).await;
} else {
let _ = send_info(room, "❌ Not connected to WebSocket.").await;
}
}
"!stop" => {
if let Some(tx) = handlers.get(&room_id) {
let _ = tx.send(BotCommand::Pause).await;
} else {
let _ = send_info(room, "❌ Not connected to WebSocket.").await;
}
}
"!disconnect" => {
if let Some(tx) = handlers.get(&room_id) {
let _ = tx.send(BotCommand::Disconnect).await;
} else {
let _ = send_info(room, "❌ Not connected to WebSocket.").await;
}
}
"!autoskip" => {
if parts.len() < 2 { return; }
let mut config = db.get_user_config(&user_id).unwrap();
match parts[1] {
"add" => {
for &c in &parts[2..] {
config.autoskip_countries.push(c.to_uppercase());
}
db.update_user_config(&config).unwrap();
let _ = send_info(room, &format!("✅ Added to your skip list: {:?}", &parts[2..])).await;
}
"remove" => {
config.autoskip_countries.retain(|c| !parts[2..].contains(&c.as_str()));
db.update_user_config(&config).unwrap();
let _ = send_info(room, &format!("🗑️ Removed from your skip list: {:?}", &parts[2..])).await;
}
"list" => {
let _ = send_info(room, &format!("📝 Your auto-skip countries: {:?}", config.autoskip_countries)).await;
}
_ => {
let _ = send_info(room, "❌ Invalid <code>!autoskip</code> subcommand.").await;
}
}
}
_ => {
let _ = send_info(room, &format!("❌ Invalid command: <code>{}</code>. Type <code>!help</code> for a list of commands.", cmd)).await;
}
}
}
async fn handle_omgle_session(
mut omgle: WsOmegleClient,
mut rx: mpsc::Receiver<BotCommand>,
room: Room,
db: Arc<Db>,
room_id: String,
handlers: Arc<DashMap<String, mpsc::Sender<BotCommand>>>,
_client: Client,
) {
let mut last_typing = std::time::Instant::now();
let mut typing_active = false;
let mut last_people_online_request = std::time::Instant::now();
let mut message_count = 0;
let mut local_typing_active = false;
let mut active_user_id: Option<String> = None;
let mut peer_connected = false;
let mut last_prefer_same_country = false;
let mut pending_msg_id: Option<OwnedEventId> = None;
loop {
tokio::select! {
cmd = rx.recv() => {
match cmd {
Some(BotCommand::Connect { msg_id }) => {
let _ = edit_info(&room, msg_id, "✅ Connected to Omgle").await;
},
Some(BotCommand::Match { prefer_same_country, user_id, msg_id }) => {
if peer_connected {
let _ = omgle.disconnect_peer().await;
}
active_user_id = Some(user_id.clone());
last_prefer_same_country = prefer_same_country;
pending_msg_id = Some(msg_id);
let user_config = db.get_user_config(&user_id).unwrap();
let _ = omgle.request_match(prefer_same_country, user_config.interests).await;
peer_connected = false;
local_typing_active = false;
typing_active = false;
let _ = room.typing_notice(false).await;
let mut room_state = db.get_room_state(&room_id).unwrap();
room_state.active_user_id = Some(user_id);
room_state.is_connected = true;
let _ = db.update_room_state(&room_state);
},
Some(BotCommand::Skip { user_id, msg_id }) => {
if peer_connected {
active_user_id = Some(user_id.clone());
pending_msg_id = Some(msg_id);
let user_config = db.get_user_config(&user_id).unwrap();
let _ = omgle.disconnect_peer().await;
peer_connected = false;
local_typing_active = false;
typing_active = false;
let _ = room.typing_notice(false).await;
let _ = omgle.request_match(last_prefer_same_country, user_config.interests).await;
let mut room_state = db.get_room_state(&room_id).unwrap();
room_state.active_user_id = Some(user_id);
let _ = db.update_room_state(&room_state);
} else {
let _ = edit_info(&room, msg_id, "❌ No stranger to skip.").await;
}
},
Some(BotCommand::Pause) => {
if peer_connected {
let _ = send_info(&room, "⏸️ Paused (Skipped peer)").await;
let _ = omgle.disconnect_peer().await;
peer_connected = false;
local_typing_active = false;
typing_active = false;
let _ = room.typing_notice(false).await;
} else {
let _ = send_info(&room, "❌ No stranger to pause.").await;
}
},
Some(BotCommand::Disconnect) => {
let _ = omgle.disconnect().await;
let mut room_state = db.get_room_state(&room_id).unwrap();
room_state.is_connected = false;
let _ = db.update_room_state(&room_state);
let _ = send_info(&room, "🔌 Disconnected from Omgle").await;
handlers.remove(&room_id);
return;
},
Some(BotCommand::SendMessage(text)) => {
if peer_connected {
local_typing_active = false;
let _ = omgle.send_message(&text).await;
message_count += 1;
}
},
Some(BotCommand::SendTyping(typing)) => {
if peer_connected {
local_typing_active = typing;
let _ = omgle.send_typing(typing).await;
}
},
None => break,
}
}
ev = omgle.next_event() => {
match ev {
Ok(Some(msg)) => {
match msg.channel.as_str() {
"connected" => {
if let Some(msg_id) = pending_msg_id.take() {
let _ = edit_status(&room, msg_id, "Stranger connected", "🎲").await;
} else {
let _ = send_status(&room, "Stranger connected", "🎲").await;
}
message_count = 0;
peer_connected = true;
}
"peerCountry" => {
if let Ok(data) = serde_json::from_value::<crate::omegle::protocol::CountryData>(msg.data) {
let flag = country_code_to_flag(&data.country);
let text = format!("Connected to {} {} ({})", flag, data.country_name, &data.country);
let _ = send_status(&room, &text, "🌍").await;
if let Some(user_id) = &active_user_id {
let config = db.get_user_config(user_id).unwrap();
if config.autoskip_countries.contains(&data.country.to_uppercase()) {
pending_msg_id = send_info(&room, "⏩ Auto-skipping...").await.ok();
let _ = omgle.disconnect_peer().await;
peer_connected = false;
local_typing_active = false;
typing_active = false;
let _ = room.typing_notice(false).await;
let _ = omgle.request_match(last_prefer_same_country, config.interests).await;
}
}
}
}
"peopleOnline" => {
if let Some(count) = msg.data.as_u64() {
let topic = format!("🟢 Online: {}", count);
let _ = room.set_room_topic(&topic).await;
}
}
"message" => {
if let Some(text) = msg.data.as_str() {
let _ = send_text(&room, text).await;
message_count += 1;
}
}
"typing" => {
let typing = msg.data.as_bool().unwrap_or(false);
typing_active = typing;
last_typing = std::time::Instant::now();
let _ = room.typing_notice(typing).await;
}
"disconnect" => {
if peer_connected {
let _ = send_status(&room, "Stranger disconnected", "😕").await;
peer_connected = false;
local_typing_active = false;
typing_active = false;
let _ = room.typing_notice(false).await;
if message_count <= 4 {
if let Some(user_id) = &active_user_id {
pending_msg_id = send_info(&room, "⏩ Automatching...").await.ok();
let config = db.get_user_config(user_id).unwrap();
let _ = omgle.request_match(last_prefer_same_country, config.interests).await;
}
}
}
}
_ => {}
}
}
Ok(None) => break,
Err(_) => break,
}
}
_ = tokio::time::sleep(tokio::time::Duration::from_millis(500)) => {
if typing_active && last_typing.elapsed().as_secs() >= 3 {
typing_active = false;
let _ = room.typing_notice(false).await;
}
if local_typing_active && peer_connected {
let _ = omgle.send_typing(true).await;
}
if last_people_online_request.elapsed().as_secs() >= 60 {
let _ = omgle.request_people_online().await;
last_people_online_request = std::time::Instant::now();
}
}
}
}
let mut room_state = db.get_room_state(&room_id).unwrap();
room_state.is_connected = false;
let _ = db.update_room_state(&room_state);
handlers.remove(&room_id);
}

1
bot/src/matrix/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod client;

106
bot/src/omegle/client.rs Normal file
View File

@@ -0,0 +1,106 @@
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use serde_json::json;
use tokio::net::TcpStream;
use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
use tokio_tungstenite::tungstenite::protocol::Message;
use crate::omegle::protocol::OmegleMessage;
use crate::omegle::OmegleProvider;
pub struct WsOmegleClient {
ws: Option<WebSocketStream<MaybeTlsStream<TcpStream>>>,
}
impl WsOmegleClient {
pub fn new() -> Self {
Self { ws: None }
}
}
#[async_trait]
impl OmegleProvider for WsOmegleClient {
async fn connect(&mut self, url: &str) -> Result<()> {
let (ws_stream, _) = connect_async(url).await?;
self.ws = Some(ws_stream);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(mut ws) = self.ws.take() {
let _ = ws.close(None).await;
}
Ok(())
}
async fn disconnect_peer(&mut self) -> Result<()> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
let msg = json!({
"channel": "disconnect"
});
ws.send(Message::Text(msg.to_string())).await?;
Ok(())
}
async fn request_match(&mut self, prefer_same_country: bool, interests: Vec<String>) -> Result<()> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
let match_msg = json!({
"channel": "match",
"data": {
"data": "text",
"params": {
"interests": interests,
"preferSameCountry": prefer_same_country
}
}
});
ws.send(Message::Text(match_msg.to_string())).await?;
Ok(())
}
async fn send_message(&mut self, text: &str) -> Result<()> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
let msg = json!({
"channel": "message",
"data": text
});
ws.send(Message::Text(msg.to_string())).await?;
Ok(())
}
async fn send_typing(&mut self, typing: bool) -> Result<()> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
let msg = json!({
"channel": "typing",
"data": typing
});
ws.send(Message::Text(msg.to_string())).await?;
Ok(())
}
async fn request_people_online(&mut self) -> Result<()> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
let msg = json!({
"channel": "peopleOnline"
});
ws.send(Message::Text(msg.to_string())).await?;
Ok(())
}
async fn next_event(&mut self) -> Result<Option<OmegleMessage>> {
let ws = self.ws.as_mut().ok_or_else(|| anyhow!("Not connected"))?;
while let Some(msg) = ws.next().await {
match msg? {
Message::Text(text) => {
let omegle_msg: OmegleMessage = serde_json::from_str(&text)?;
return Ok(Some(omegle_msg));
}
Message::Binary(_) => {}
Message::Ping(_) | Message::Pong(_) => {}
Message::Close(_) => return Ok(None),
Message::Frame(_) => {}
}
}
Ok(None)
}
}

17
bot/src/omegle/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
pub mod protocol;
pub mod client;
use anyhow::Result;
use async_trait::async_trait;
#[async_trait]
pub trait OmegleProvider: Send + Sync {
async fn connect(&mut self, url: &str) -> Result<()>;
async fn disconnect(&mut self) -> Result<()>;
async fn disconnect_peer(&mut self) -> Result<()>;
async fn request_match(&mut self, prefer_same_country: bool, interests: Vec<String>) -> Result<()>;
async fn send_message(&mut self, text: &str) -> Result<()>;
async fn send_typing(&mut self, typing: bool) -> Result<()>;
async fn request_people_online(&mut self) -> Result<()>;
async fn next_event(&mut self) -> Result<Option<protocol::OmegleMessage>>;
}

View File

@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OmegleMessage {
pub channel: String,
#[serde(default)]
pub data: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CountryData {
pub country: String,
#[serde(rename = "countryName")]
pub country_name: String,
}

113
bot/src/state/db.rs Normal file
View File

@@ -0,0 +1,113 @@
use crate::state::models::{RoomState, UserConfig};
use anyhow::Result;
use rusqlite::{params, Connection};
use std::sync::{Arc, Mutex};
pub struct Db {
pub conn: Arc<Mutex<Connection>>,
}
impl Db {
pub fn new(path: &str) -> Result<Self> {
let conn = Connection::open(path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS room_state (
room_id TEXT PRIMARY KEY,
is_connected INTEGER DEFAULT 0,
active_user_id TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS user_config (
user_id TEXT PRIMARY KEY,
autoskip_countries TEXT,
interests TEXT
)",
[],
)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
pub fn get_room_state(&self, room_id: &str) -> Result<RoomState> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT room_id, is_connected, active_user_id FROM room_state WHERE room_id = ?",
)?;
let state = stmt.query_row(params![room_id], |row| {
Ok(RoomState {
room_id: row.get(0)?,
is_connected: row.get::<_, i32>(1)? != 0,
active_user_id: row.get(2)?,
})
});
match state {
Ok(s) => Ok(s),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(RoomState {
room_id: room_id.to_string(),
is_connected: false,
active_user_id: None,
}),
Err(e) => Err(e.into()),
}
}
pub fn update_room_state(&self, state: &RoomState) -> Result<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT OR REPLACE INTO room_state (room_id, is_connected, active_user_id) VALUES (?, ?, ?)",
params![state.room_id, state.is_connected as i32, state.active_user_id],
)?;
Ok(())
}
pub fn get_user_config(&self, user_id: &str) -> Result<UserConfig> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT user_id, autoskip_countries, interests FROM user_config WHERE user_id = ?",
)?;
let config = stmt.query_row(params![user_id], |row| {
let countries_str: String = row.get(1).unwrap_or_default();
let autoskip_countries = if countries_str.is_empty() {
Vec::new()
} else {
countries_str.split(',').map(|s| s.to_string()).collect()
};
let interests_str: String = row.get(2).unwrap_or_default();
let interests = if interests_str.is_empty() {
Vec::new()
} else {
interests_str.split(',').map(|s| s.to_string()).collect()
};
Ok(UserConfig {
user_id: row.get(0)?,
autoskip_countries,
interests,
})
});
match config {
Ok(c) => Ok(c),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(UserConfig {
user_id: user_id.to_string(),
autoskip_countries: Vec::new(),
interests: Vec::new(),
}),
Err(e) => Err(e.into()),
}
}
pub fn update_user_config(&self, config: &UserConfig) -> Result<()> {
let conn = self.conn.lock().unwrap();
let countries_str = config.autoskip_countries.join(",");
let interests_str = config.interests.join(",");
conn.execute(
"INSERT OR REPLACE INTO user_config (user_id, autoskip_countries, interests) VALUES (?, ?, ?)",
params![config.user_id, countries_str, interests_str],
)?;
Ok(())
}
}

2
bot/src/state/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod db;
pub mod models;

15
bot/src/state/models.rs Normal file
View File

@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomState {
pub room_id: String,
pub is_connected: bool,
pub active_user_id: Option<String>, // User ID whose config is active in this room
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserConfig {
pub user_id: String,
pub autoskip_countries: Vec<String>,
pub interests: Vec<String>,
}

13
bot/src/utils/flags.rs Normal file
View File

@@ -0,0 +1,13 @@
pub fn country_code_to_flag(code: &str) -> String {
if code.len() != 2 {
return "🏳".to_string();
}
code.to_uppercase()
.chars()
.map(|c| {
// regional indicator symbols start at 0x1F1E6 ('A')
char::from_u32(0x1F1E6 + (c as u32 - 'A' as u32)).unwrap_or('?')
})
.collect()
}

1
bot/src/utils/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod flags;