feat: Some quality of life improvements
This commit is contained in:
@@ -4,7 +4,10 @@ MATRIX_USERNAME=@omeglebot:jzitnik.dev
|
||||
MATRIX_PASSWORD=
|
||||
|
||||
# Selenium Grid configuration
|
||||
GRID_DASHBOARD_PORT=4444 # Bare in mind that by default there is no password for the VNC connection. So this shouldn't be exposed to the public.
|
||||
GRID_DASHBOARD_PORT=4444 # Bare in mind that by default there is no password for the VNC connection.
|
||||
|
||||
# The URL of the Selenium Grid hub. This is used in a link for the user that sends `!connect` command to manually pass the Cloudflare challange.
|
||||
SELENIUM_GRID_URL=http://localhost:4444
|
||||
|
||||
# Miscellaneous settings
|
||||
CF_WAIT_TIME=60 # The time you have to manually pass the Cloudflare challenge in seconds.
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct MatrixConfig {
|
||||
pub struct OmegleConfig {
|
||||
#[serde(default)]
|
||||
pub websocket_url: String,
|
||||
#[serde(default)]
|
||||
pub selenium_url: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -33,11 +35,13 @@ impl Config {
|
||||
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();
|
||||
let selenium_url = env::var("SELENIUM_URL").unwrap_or_default();
|
||||
|
||||
if !homeserver.is_empty()
|
||||
|| !username.is_empty()
|
||||
|| !password.is_empty()
|
||||
|| !websocket_url.is_empty()
|
||||
|| !selenium_url.is_empty()
|
||||
{
|
||||
return Ok(Config {
|
||||
matrix: MatrixConfig {
|
||||
@@ -45,7 +49,10 @@ impl Config {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
omegle: OmegleConfig { websocket_url },
|
||||
omegle: OmegleConfig {
|
||||
websocket_url,
|
||||
selenium_url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ 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,
|
||||
Pause { reply_to: OwnedEventId },
|
||||
Disconnect { reply_to: OwnedEventId },
|
||||
SendMessage(String),
|
||||
SendTyping(bool),
|
||||
}
|
||||
@@ -78,6 +78,24 @@ async fn edit_status(room: &Room, event_id: OwnedEventId, text: &str, emoji: &st
|
||||
Ok(resp.event_id)
|
||||
}
|
||||
|
||||
async fn send_reply(room: &Room, event_id: OwnedEventId, text: &str) -> Result<OwnedEventId> {
|
||||
let mut content = RoomMessageEventContent::text_plain(text);
|
||||
content.relates_to = Some(Relation::Reply {
|
||||
in_reply_to: matrix_sdk::ruma::events::relation::InReplyTo::new(event_id),
|
||||
});
|
||||
let resp = room.send(content).await?;
|
||||
Ok(resp.event_id)
|
||||
}
|
||||
|
||||
async fn send_reply_html(room: &Room, event_id: OwnedEventId, text: &str, html: &str) -> Result<OwnedEventId> {
|
||||
let mut content = RoomMessageEventContent::text_html(text, html);
|
||||
content.relates_to = Some(Relation::Reply {
|
||||
in_reply_to: matrix_sdk::ruma::events::relation::InReplyTo::new(event_id),
|
||||
});
|
||||
let resp = room.send(content).await?;
|
||||
Ok(resp.event_id)
|
||||
}
|
||||
|
||||
|
||||
impl MatrixBot {
|
||||
pub async fn new(config: Config, db: Arc<Db>) -> Result<Self> {
|
||||
@@ -134,7 +152,7 @@ impl MatrixBot {
|
||||
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;
|
||||
handle_command(body, &original.sender, &room, &db, &config, &handlers, &client, original.event_id).await;
|
||||
} else if let Some(tx) = handlers.get(&room_id) {
|
||||
let _ = tx.send(BotCommand::SendMessage(body.to_string())).await;
|
||||
}
|
||||
@@ -175,6 +193,7 @@ async fn handle_command(
|
||||
config: &Config,
|
||||
handlers: &Arc<DashMap<String, mpsc::Sender<BotCommand>>>,
|
||||
client: &Client,
|
||||
reply_to: OwnedEventId,
|
||||
) {
|
||||
let parts: Vec<&str> = body.split_whitespace().collect();
|
||||
let cmd = parts[0];
|
||||
@@ -185,24 +204,28 @@ async fn handle_command(
|
||||
"!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>!connect</code> - Connect to Omegle 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>!disconnect</code> - Disconnect from Omegle WebSocket<br/>\
|
||||
<code>!autoskip add/remove/list <CC></code> - Manage your automatic skipping list (global)<br/>\
|
||||
<code>!interests add/remove/list/clear <interest></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;
|
||||
let _ = send_reply_html(room, reply_to, plain, help_text).await;
|
||||
}
|
||||
"!connect" => {
|
||||
if handlers.contains_key(&room_id) {
|
||||
let _ = send_info(room, "Already connected").await;
|
||||
let _ = send_reply(room, reply_to, "Already connected").await;
|
||||
return;
|
||||
}
|
||||
|
||||
let msg_id = match send_info(room, "🔄 Connecting to Omgle...").await {
|
||||
let connecting_msg = if config.omegle.selenium_url.is_empty() {
|
||||
"🔄 Connecting to Omegle...".to_string()
|
||||
} else {
|
||||
format!("🔄 Connecting to Omegle... (If needed, complete bot check at {})", config.omegle.selenium_url)
|
||||
};
|
||||
let msg_id = match send_reply(room, reply_to, &connecting_msg).await {
|
||||
Ok(id) => id,
|
||||
Err(_) => return,
|
||||
};
|
||||
@@ -217,29 +240,29 @@ async fn handle_command(
|
||||
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 mut omegle_client = WsOmegleClient::new();
|
||||
if let Err(e) = omegle_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;
|
||||
let _ = omegle_client.request_people_online().await;
|
||||
|
||||
handle_omgle_session(omgle_client, rx, room_clone, db_struct_clone, room_id.clone(), handlers_clone, client_clone).await;
|
||||
handle_omegle_session(omegle_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 {
|
||||
let msg_id = match send_reply(room, reply_to, "🔍 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;
|
||||
let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket. Use <code>!connect</code> first.").await;
|
||||
}
|
||||
}
|
||||
"!interests" => {
|
||||
@@ -251,49 +274,49 @@ async fn handle_command(
|
||||
config.interests.push(i.to_string());
|
||||
}
|
||||
db.update_user_config(&config).unwrap();
|
||||
let _ = send_info(room, &format!("✅ Added interests: {:?}", &parts[2..])).await;
|
||||
let _ = send_reply(room, reply_to, &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;
|
||||
let _ = send_reply(room, reply_to, &format!("🗑️ Removed interests: {:?}", &parts[2..])).await;
|
||||
}
|
||||
"list" => {
|
||||
let _ = send_info(room, &format!("📝 Your interests: {:?}", config.interests)).await;
|
||||
let _ = send_reply(room, reply_to, &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_reply(room, reply_to, "✨ Interests cleared").await;
|
||||
}
|
||||
_ => {
|
||||
let _ = send_info(room, "❌ Invalid <code>!interests</code> subcommand.").await;
|
||||
let _ = send_reply(room, reply_to, "❌ Invalid <code>!interests</code> subcommand.").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
"!skip" => {
|
||||
if let Some(tx) = handlers.get(&room_id) {
|
||||
let msg_id = match send_info(room, "⏩ Skipping...").await {
|
||||
let msg_id = match send_reply(room, reply_to, "⏩ 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;
|
||||
let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await;
|
||||
}
|
||||
}
|
||||
"!stop" => {
|
||||
if let Some(tx) = handlers.get(&room_id) {
|
||||
let _ = tx.send(BotCommand::Pause).await;
|
||||
let _ = tx.send(BotCommand::Pause { reply_to }).await;
|
||||
} else {
|
||||
let _ = send_info(room, "❌ Not connected to WebSocket.").await;
|
||||
let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await;
|
||||
}
|
||||
}
|
||||
"!disconnect" => {
|
||||
if let Some(tx) = handlers.get(&room_id) {
|
||||
let _ = tx.send(BotCommand::Disconnect).await;
|
||||
let _ = tx.send(BotCommand::Disconnect { reply_to }).await;
|
||||
} else {
|
||||
let _ = send_info(room, "❌ Not connected to WebSocket.").await;
|
||||
let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await;
|
||||
}
|
||||
}
|
||||
"!autoskip" => {
|
||||
@@ -305,29 +328,29 @@ async fn handle_command(
|
||||
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;
|
||||
let _ = send_reply(room, reply_to, &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;
|
||||
let _ = send_reply(room, reply_to, &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_reply(room, reply_to, &format!("📝 Your auto-skip countries: {:?}", config.autoskip_countries)).await;
|
||||
}
|
||||
_ => {
|
||||
let _ = send_info(room, "❌ Invalid <code>!autoskip</code> subcommand.").await;
|
||||
let _ = send_reply(room, reply_to, "❌ 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;
|
||||
let _ = send_reply(room, reply_to, &format!("❌ Invalid command: <code>{}</code>. Type <code>!help</code> for a list of commands.", cmd)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_omgle_session(
|
||||
mut omgle: WsOmegleClient,
|
||||
async fn handle_omegle_session(
|
||||
mut omegle: WsOmegleClient,
|
||||
mut rx: mpsc::Receiver<BotCommand>,
|
||||
room: Room,
|
||||
db: Arc<Db>,
|
||||
@@ -350,17 +373,17 @@ async fn handle_omgle_session(
|
||||
cmd = rx.recv() => {
|
||||
match cmd {
|
||||
Some(BotCommand::Connect { msg_id }) => {
|
||||
let _ = edit_info(&room, msg_id, "✅ Connected to Omgle").await;
|
||||
let _ = edit_info(&room, msg_id, "✅ Connected to Omegle").await;
|
||||
},
|
||||
Some(BotCommand::Match { prefer_same_country, user_id, msg_id }) => {
|
||||
if peer_connected {
|
||||
let _ = omgle.disconnect_peer().await;
|
||||
let _ = omegle.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;
|
||||
let _ = omegle.request_match(prefer_same_country, user_config.interests).await;
|
||||
|
||||
peer_connected = false;
|
||||
local_typing_active = false;
|
||||
@@ -377,14 +400,14 @@ async fn handle_omgle_session(
|
||||
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;
|
||||
let _ = omegle.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 _ = omegle.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);
|
||||
@@ -393,44 +416,44 @@ async fn handle_omgle_session(
|
||||
let _ = edit_info(&room, msg_id, "❌ No stranger to skip.").await;
|
||||
}
|
||||
},
|
||||
Some(BotCommand::Pause) => {
|
||||
Some(BotCommand::Pause { reply_to }) => {
|
||||
if peer_connected {
|
||||
let _ = send_info(&room, "⏸️ Paused (Skipped peer)").await;
|
||||
let _ = omgle.disconnect_peer().await;
|
||||
let _ = send_reply(&room, reply_to, "⏸️ Paused (Skipped peer)").await;
|
||||
let _ = omegle.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;
|
||||
let _ = send_reply(&room, reply_to, "❌ No stranger to pause.").await;
|
||||
}
|
||||
},
|
||||
Some(BotCommand::Disconnect) => {
|
||||
let _ = omgle.disconnect().await;
|
||||
Some(BotCommand::Disconnect { reply_to }) => {
|
||||
let _ = omegle.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;
|
||||
let _ = send_reply(&room, reply_to, "🔌 Disconnected from Omegle").await;
|
||||
handlers.remove(&room_id);
|
||||
return;
|
||||
},
|
||||
Some(BotCommand::SendMessage(text)) => {
|
||||
if peer_connected {
|
||||
local_typing_active = false;
|
||||
let _ = omgle.send_message(&text).await;
|
||||
let _ = omegle.send_message(&text).await;
|
||||
message_count += 1;
|
||||
}
|
||||
},
|
||||
Some(BotCommand::SendTyping(typing)) => {
|
||||
if peer_connected {
|
||||
local_typing_active = typing;
|
||||
let _ = omgle.send_typing(typing).await;
|
||||
let _ = omegle.send_typing(typing).await;
|
||||
}
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
ev = omgle.next_event() => {
|
||||
ev = omegle.next_event() => {
|
||||
match ev {
|
||||
Ok(Some(msg)) => {
|
||||
match msg.channel.as_str() {
|
||||
@@ -454,14 +477,14 @@ async fn handle_omgle_session(
|
||||
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;
|
||||
let _ = omegle.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;
|
||||
let _ = omegle.request_match(last_prefer_same_country, config.interests).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,7 +519,7 @@ async fn handle_omgle_session(
|
||||
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;
|
||||
let _ = omegle.request_match(last_prefer_same_country, config.interests).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,11 +538,11 @@ async fn handle_omgle_session(
|
||||
}
|
||||
|
||||
if local_typing_active && peer_connected {
|
||||
let _ = omgle.send_typing(true).await;
|
||||
let _ = omegle.send_typing(true).await;
|
||||
}
|
||||
|
||||
if last_people_online_request.elapsed().as_secs() >= 60 {
|
||||
let _ = omgle.request_people_online().await;
|
||||
let _ = omegle.request_people_online().await;
|
||||
last_people_online_request = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,18 @@ impl WsOmegleClient {
|
||||
impl OmegleProvider for WsOmegleClient {
|
||||
async fn connect(&mut self, url: &str) -> Result<()> {
|
||||
let (ws_stream, _) = connect_async(url).await?;
|
||||
self.ws = Some(ws_stream);
|
||||
|
||||
let mut ws = ws_stream;
|
||||
let msg = ws.next().await.ok_or_else(|| anyhow!("Connection closed before receiving SUCCESS"))??;
|
||||
|
||||
match msg {
|
||||
Message::Text(text) if text == "SUCCESS" => {}
|
||||
Message::Text(text) => return Err(anyhow!("Unexpected message: {}", text)),
|
||||
Message::Close(_) => return Err(anyhow!("Server closed connection")),
|
||||
_ => return Err(anyhow!("Unexpected message type")),
|
||||
}
|
||||
|
||||
self.ws = Some(ws);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
# Selenium Grid with Chromium (or Chrome)
|
||||
selenium:
|
||||
image: selenium/standalone-chromium:4.43.0-20260404
|
||||
build: ./selenium-patch
|
||||
container_name: selenium
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
@@ -40,6 +40,7 @@ services:
|
||||
- MATRIX_USERNAME=${MATRIX_USERNAME}
|
||||
- MATRIX_PASSWORD=${MATRIX_PASSWORD}
|
||||
- OMEGLE_WEBSOCKET_URL=ws://omegle-proxy:8765
|
||||
- SELENIUM_URL=${SELENIUM_GRID_URL}
|
||||
- DB_PATH=/data/omegle.db
|
||||
volumes:
|
||||
- omegle-bot-data:/data
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
RUN pip install --no-cache-dir setuptools selenium websockets
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY main.py .
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
|
||||
0
proxy/__init__.py
Normal file
0
proxy/__init__.py
Normal file
11
proxy/config.py
Normal file
11
proxy/config.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
TARGET_URL = os.getenv("TARGET_URL", "https://omegleweb.io/")
|
||||
TARGET_WS_URL = os.getenv(
|
||||
"TARGET_WS_URL", "wss://omegleweb.io:8443/socket.io/?EIO=4&transport=websocket"
|
||||
)
|
||||
LOCAL_HOST = os.getenv("LOCAL_HOST", "0.0.0.0")
|
||||
LOCAL_PORT = int(os.getenv("LOCAL_PORT", "8765"))
|
||||
HEADLESS = os.getenv("HEADLESS", "false").lower() in ("true", "1", "yes")
|
||||
CF_WAIT_TIME = int(os.getenv("CF_WAIT_TIME", "60"))
|
||||
SELENIUM_URL = os.getenv("SELENIUM_URL", "http://localhost:4444")
|
||||
186
proxy/main.py
186
proxy/main.py
@@ -1,182 +1,24 @@
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import websockets
|
||||
from datetime import datetime
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
TARGET_URL = os.getenv("TARGET_URL", "https://omegleweb.io/")
|
||||
TARGET_WS_URL = os.getenv("TARGET_WS_URL", "wss://omegleweb.io:8443/socket.io/?EIO=4&transport=websocket")
|
||||
LOCAL_HOST = os.getenv("LOCAL_HOST", "0.0.0.0")
|
||||
LOCAL_PORT = int(os.getenv("LOCAL_PORT", "8765"))
|
||||
HEADLESS = os.getenv("HEADLESS", "false").lower() in ("true", "1", "yes")
|
||||
CF_WAIT_TIME = int(os.getenv("CF_WAIT_TIME", "60"))
|
||||
SELENIUM_URL = os.getenv("SELENIUM_URL", "http://localhost:4444")
|
||||
from config import LOCAL_HOST, LOCAL_PORT
|
||||
from websocket_client import handle_connection
|
||||
|
||||
credentials = {
|
||||
"user_agent": None,
|
||||
"cookies": None
|
||||
}
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
|
||||
def stealth_js(driver):
|
||||
driver.execute_script("""
|
||||
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||||
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
|
||||
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
|
||||
window.chrome = {runtime: {}};
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({state: Notification.permission}) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
Object.defineProperty(navigator, 'permissions', {get: () => window.navigator.permissions});
|
||||
""")
|
||||
|
||||
def extract_credentials():
|
||||
print("\n" + "="*50)
|
||||
print("[*] PHASE 1: SELENIUM EXTRACTION")
|
||||
print("="*50)
|
||||
print("[*] Launching Chrome browser...")
|
||||
async def main():
|
||||
await websockets.serve(handle_connection, LOCAL_HOST, LOCAL_PORT)
|
||||
await asyncio.Future()
|
||||
|
||||
options = Options()
|
||||
if HEADLESS:
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-software-rasterizer")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_argument("--disable-setuid-sandbox")
|
||||
options.add_argument("--disable-background-networking")
|
||||
options.add_argument("--disable-default-apps")
|
||||
options.add_argument("--disable-sync")
|
||||
options.add_argument("--disable-translate")
|
||||
options.add_argument("--metrics-recording-only")
|
||||
options.add_argument("--mute-audio")
|
||||
options.add_argument("--no-first-run")
|
||||
options.add_argument("--safebrowsing-disable-auto-update")
|
||||
options.add_argument("--ignore-certificate-errors")
|
||||
options.add_argument("--ignore-ssl-errors")
|
||||
options.add_argument("--user-data-dir=/tmp/chrome-data")
|
||||
options.add_argument("--disable-features=IsolateOrigins,site-per-process")
|
||||
|
||||
driver = webdriver.Remote(
|
||||
command_executor=SELENIUM_URL + "/wd/hub",
|
||||
options=options
|
||||
)
|
||||
|
||||
stealth_js(driver)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print("[*] Navigating to OmegleWeb homepage...")
|
||||
driver.get(TARGET_URL)
|
||||
|
||||
wait = WebDriverWait(driver, CF_WAIT_TIME)
|
||||
wait.until(EC.presence_of_element_located((By.ID, "logo")))
|
||||
|
||||
print("[*] Time's up! Extracting the goods...")
|
||||
user_agent = driver.execute_script("return navigator.userAgent;")
|
||||
selenium_cookies = driver.get_cookies()
|
||||
cookie_string = "; ".join([f"{c['name']}={c['value']}" for c in selenium_cookies])
|
||||
|
||||
credentials["user_agent"] = user_agent
|
||||
credentials["cookies"] = cookie_string
|
||||
|
||||
print("[+] Extraction successful!")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[!] Extraction failed: {e}")
|
||||
return False
|
||||
finally:
|
||||
print("[*] Closing browser...")
|
||||
driver.quit()
|
||||
|
||||
async def start_bridge():
|
||||
async def bridge_handler(local_client):
|
||||
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] [+] Local client connected to the bridge!")
|
||||
|
||||
while True:
|
||||
headers = {
|
||||
"User-Agent": credentials["user_agent"],
|
||||
"Cookie": credentials["cookies"]
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"[*] Attempting tunnel to OmegleWeb...")
|
||||
async with websockets.connect(TARGET_WS_URL, additional_headers=headers) as target_server:
|
||||
print("[+] Tunnel established! Relaying messages...")
|
||||
|
||||
async def forward_local_to_target():
|
||||
async for message in local_client:
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] [Local -> Omegle]: {message}")
|
||||
await target_server.send(message)
|
||||
|
||||
async def forward_target_to_local():
|
||||
async for message in target_server:
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] [Omegle -> Local]: {message}")
|
||||
await local_client.send(message)
|
||||
|
||||
t1 = asyncio.create_task(forward_local_to_target())
|
||||
t2 = asyncio.create_task(forward_target_to_local())
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[t1, t2],
|
||||
return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if t1 in done and t1.exception() is None:
|
||||
print("[-] Local client disconnected.")
|
||||
break
|
||||
|
||||
print("[-] OmegleWeb connection closed.")
|
||||
break
|
||||
|
||||
except websockets.exceptions.InvalidStatusCode as e:
|
||||
if e.status_code == 403:
|
||||
print(f"[!] HTTP 403 Forbidden: Cookies likely expired. Refreshing...")
|
||||
loop = asyncio.get_running_loop()
|
||||
success = await loop.run_in_executor(None, extract_credentials)
|
||||
if not success:
|
||||
print("[!] Failed to refresh cookies. Retrying in 5s...")
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
else:
|
||||
print(f"[!] Bridge error (Status {e.status_code}): {e}")
|
||||
break
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
print("[-] OmegleWeb disconnected.")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[!] Bridge error: {type(e).__name__}: {e}")
|
||||
break
|
||||
|
||||
print(f"[*] Starting local WebSocket proxy on ws://{LOCAL_HOST}:{LOCAL_PORT}")
|
||||
async with websockets.serve(bridge_handler, LOCAL_HOST, LOCAL_PORT):
|
||||
await asyncio.Future()
|
||||
|
||||
def main():
|
||||
if not extract_credentials():
|
||||
print("[!] Initial extraction failed. Exiting.")
|
||||
return
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("[*] PHASE 2: LAUNCH THE WEBSOCKET PROXY")
|
||||
print("="*50)
|
||||
|
||||
try:
|
||||
asyncio.run(start_bridge())
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n[*] Shutting down...")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
logging.info("Shutting down...")
|
||||
|
||||
2
proxy/requirements.txt
Normal file
2
proxy/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
selenium==4.43.0
|
||||
websockets==16.0
|
||||
86
proxy/selenium_utils.py
Normal file
86
proxy/selenium_utils.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from config import HEADLESS, CF_WAIT_TIME, SELENIUM_URL, TARGET_URL
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_chrome_options() -> Options:
|
||||
options = Options()
|
||||
if HEADLESS:
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-software-rasterizer")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_argument("--disable-setuid-sandbox")
|
||||
options.add_argument("--disable-background-networking")
|
||||
options.add_argument("--disable-default-apps")
|
||||
options.add_argument("--disable-sync")
|
||||
options.add_argument("--disable-translate")
|
||||
options.add_argument("--metrics-recording-only")
|
||||
options.add_argument("--mute-audio")
|
||||
options.add_argument("--no-first-run")
|
||||
options.add_argument("--safebrowsing-disable-auto-update")
|
||||
options.add_argument("--ignore-certificate-errors")
|
||||
options.add_argument("--ignore-ssl-errors")
|
||||
options.add_argument("--user-data-dir=/tmp/chrome-data")
|
||||
options.add_argument("--disable-features=IsolateOrigins,site-per-process")
|
||||
return options
|
||||
|
||||
|
||||
def apply_stealth_scripts(driver: webdriver.Remote) -> None:
|
||||
driver.execute_script("""
|
||||
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||||
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
|
||||
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
|
||||
window.chrome = {runtime: {}};
|
||||
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications'
|
||||
? Promise.resolve({state: Notification.permission})
|
||||
: originalQuery(parameters)
|
||||
);
|
||||
Object.defineProperty(navigator, 'permissions', {get: () => window.navigator.permissions});
|
||||
""")
|
||||
|
||||
|
||||
def fetch_fresh_credentials() -> tuple[str, str] | None:
|
||||
log.info("Starting Selenium to fetch fresh credentials...")
|
||||
|
||||
driver = None
|
||||
try:
|
||||
driver = webdriver.Remote(
|
||||
command_executor=f"{SELENIUM_URL}/wd/hub",
|
||||
options=get_chrome_options(),
|
||||
)
|
||||
apply_stealth_scripts(driver)
|
||||
driver.get(TARGET_URL)
|
||||
|
||||
wait = WebDriverWait(driver, CF_WAIT_TIME)
|
||||
wait.until(EC.presence_of_element_located((By.ID, "logo")))
|
||||
|
||||
user_agent = driver.execute_script("return navigator.userAgent;")
|
||||
selenium_cookies = driver.get_cookies()
|
||||
cookies = "; ".join(
|
||||
f"{c['name']}={c['value']}"
|
||||
for c in selenium_cookies
|
||||
)
|
||||
log.info("Successfully extracted credentials via Selenium")
|
||||
return (user_agent, cookies)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Failed to extract credentials: {e}")
|
||||
return None
|
||||
|
||||
finally:
|
||||
if driver is not None:
|
||||
driver.quit()
|
||||
85
proxy/websocket_client.py
Normal file
85
proxy/websocket_client.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
from config import TARGET_WS_URL, LOCAL_HOST, LOCAL_PORT
|
||||
from selenium_utils import fetch_fresh_credentials
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def handle_connection(local_client):
|
||||
log.info("Local client connected, fetching credentials...")
|
||||
|
||||
result = await asyncio.get_running_loop().run_in_executor(
|
||||
None, fetch_fresh_credentials
|
||||
)
|
||||
if result is None:
|
||||
log.error("Could not fetch credentials")
|
||||
await local_client.close(1011, "Failed to bypass Cloudflare")
|
||||
return
|
||||
|
||||
user_agent, cookies = result
|
||||
headers = {
|
||||
"User-Agent": user_agent,
|
||||
"Cookie": cookies,
|
||||
}
|
||||
|
||||
try:
|
||||
log.info("Connecting to OmegleWeb...")
|
||||
async with websockets.connect(
|
||||
TARGET_WS_URL,
|
||||
additional_headers=headers,
|
||||
) as target_server:
|
||||
log.info("OmegleWeb connected! Notifying client...")
|
||||
await local_client.send("SUCCESS")
|
||||
log.info("Starting relay")
|
||||
await relay_bidirectional(local_client, target_server)
|
||||
|
||||
except websockets.exceptions.InvalidStatusCode as e:
|
||||
log.error(f"Omegle connection failed (Status {e.status_code})")
|
||||
await local_client.close(1011, f"Connection failed: {e.status_code}")
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
log.info("OmegleWeb disconnected")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Connection error: {type(e).__name__}: {e}")
|
||||
await local_client.close(1011, str(e))
|
||||
|
||||
|
||||
async def relay_bidirectional(local_ws, target_ws):
|
||||
async def forward_local():
|
||||
try:
|
||||
async for message in local_ws:
|
||||
log.debug(f"[Local -> Omegle]: {message}")
|
||||
await target_ws.send(message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
log.info("Local client disconnected")
|
||||
except Exception as e:
|
||||
log.error(f"Forward local error: {e}")
|
||||
|
||||
async def forward_remote():
|
||||
try:
|
||||
async for message in target_ws:
|
||||
log.debug(f"[Omegle -> Local]: {message}")
|
||||
await local_ws.send(message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
log.info("OmegleWeb disconnected")
|
||||
except Exception as e:
|
||||
log.error(f"Forward remote error: {e}")
|
||||
|
||||
await asyncio.gather(
|
||||
forward_local(),
|
||||
forward_remote(),
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
log.info(f"Starting WebSocket proxy on ws://{LOCAL_HOST}:{LOCAL_PORT}")
|
||||
await websockets.serve(handle_connection, LOCAL_HOST, LOCAL_PORT)
|
||||
await asyncio.Future()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
14
selenium-patch/Dockerfile
Normal file
14
selenium-patch/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM selenium/standalone-chromium:4.43.0-20260404
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update && apt-get install -y python3-pip
|
||||
RUN pip3 install undetected-chromedriver setuptools --break-system-packages
|
||||
|
||||
RUN echo 'from undetected_chromedriver.patcher import Patcher\n\
|
||||
Patcher(executable_path="/usr/bin/chromedriver").patch()\n\
|
||||
print("Chromedriver patched inside Docker successfully!")' > /tmp/patch.py
|
||||
|
||||
RUN python3 /tmp/patch.py
|
||||
|
||||
USER seluser
|
||||
Reference in New Issue
Block a user