added initial browser control

parent 815adc2c
......@@ -69,6 +69,7 @@ from shmcs.webpanel import run_flask_app
from shmcs.panel import create_panel_gui
from shmcs.studio import run_camstudio
from shmcs.obs import run_obs_controller
from shmcs.browser import run_browser
def main():
# Setup argument parser
......@@ -80,6 +81,8 @@ def main():
parser.add_argument('--port', type=int, default=5000,
help='Port for the web interface (default: 5000)')
parser.add_argument('--browser', action="store_true",
help='launch local web browser')
# Parse arguments
args = parser.parse_args()
......@@ -100,6 +103,15 @@ def main():
)
obs_thread.start()
if args.browser:
browser_thread = threading.Thread(
target=run_browser,
kwargs={},
daemon=True
)
logger.info("Browser launch requested")
browser_thread.start()
# Daemon mode for web interface
if args.nogui or args.daemon:
run_camstudio(args.daemon)
......
import platform
import asyncio
from playwright.async_api import async_playwright
import argparse
import os
from datetime import datetime
from pathlib import Path
import logging
logging.getLogger(__name__)
async def monitor_stream(log_path, domain):
async with async_playwright() as p:
try:
browser = await p.chromium.connect_over_cdp("http://localhost:9222")
except:
user_data_dir = './user-data'
cont = await p.chromium.launch_persistent_context(user_data_dir, headless=False, args=["--disable-infobars", "--disable-features=Antaniu"], ignore_default_args=["--enable-automation", "--no-sandbox"],)
pages = cont.pages
if pages:
page = pages[0]
else:
page = await cont.new_page()
logger.info(page.url)
await page.goto('http://localhost:5000/chat')
logger.info("Browser session retained")
#await browser.close()
browser = cont.browser
while True:
browser = cont.browser
if browser:
contexts = browser.contexts
else:
contexts = [cont]
for context in contexts:
for page in context.pages:
if domain in page.url:
print(f"Found Streamate tab")
await monitor_requests(page, log_path)
await asyncio.sleep(5) # Check every 5 seconds
async def monitor_requests(page, log_path):
async def log_request(request):
if request.resource_type not in ['document', 'image', 'font', 'stylesheet']:
log_entry = f"[{datetime.now()}] Request: {request.method} {request.url}\n"
with open(log_path, 'a') as f:
f.write(log_entry)
async def log_response(response):
#print(await response.body())
#print(response.request.resource_type)
#if response.request.resource_type not in ['document', 'image', 'font', 'stylesheet']:
if response.request.resource_type in ['xhr'] and not response.url.endswith(".mp3"):
log_entry = f"[{datetime.now()}] Response: {response.status} {response.url} -> BODY: "
log_body = await response.body()
with open(log_path, 'a') as f:
f.write(log_entry)
print(log_body)
f.write(str(log_body))
f.write("\n\n")
async def log_websocket(ws):
def writelog(payload, origin, url):
logging.info(origin+": "+str(payload)+"\n")
log_entry = f"[{datetime.now()}] WS: {origin} {url} -> Message: "
with open(log_path, 'a') as f:
f.write(log_entry)
f.write(origin+": ")
f.write(str(payload)+"\n\n")
logging.info(f"WebSocket opened: {ws.url}")
ws.on("framesent", lambda payload: writelog(payload, 'framesent', ws.url))
ws.on("framereceived", lambda payload: writelog( payload, 'framereceived', ws.url))
ws.on("close", lambda payload: print("WebSocket closed"))
page.on("request", log_request)
page.on("response", log_response)
page.on("websocket", log_websocket)
while True:
await asyncio.sleep(1)
def run_browser():
lpath=str(Path(Path.home(), Path('streamon.log')))
#parser = argparse.ArgumentParser(description="Monitor Streamate requests")
#parser.add_argument("--log_path", type=str, default=lpath,
# help="Path to the log file")
#parser.add_argument("--domain", type=str, default="performerclient.streamatemodels.com",
# help="URL to monitor")
#args = parser.parse_args()
logging.info("Starting browser...")
log_dir = os.path.dirname(lpath)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
asyncio.run(monitor_stream(lpath, "performerclient.streamatemodels.com"))
......@@ -147,6 +147,9 @@ def stream():
return render_template('stream.html', stream_url=stream_url)
@flask_app.route("/chat")
def chat():
return render_template('chat.html')
@socketio.event
def my_event(message):
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Responsive Chat with Context Menus and Fixed Private Chat</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
/* Reset and base */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Poppins', sans-serif;
background: #121212;
color: #e0e0e0;
display: flex;
min-height: 100vh;
user-select: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Container Layout */
.container {
display: grid;
grid-template-columns: 1fr 280px;
grid-template-rows: 1fr;
gap: 0;
width: 100%;
height: 100vh;
background: #1f1f1f;
overflow: hidden;
}
/* User List Panel */
.user-list {
background: #20232a;
border-left: 1px solid #2e2e3a;
display: flex;
flex-direction: column;
user-select: none;
min-width: 280px;
}
.user-list-header {
padding: 16px;
font-weight: 700;
font-size: 1.2rem;
border-bottom: 1px solid #2e2e3a;
flex-shrink: 0;
}
.users {
flex-grow: 1;
overflow-y: auto;
padding: 12px 8px;
-webkit-overflow-scrolling: touch;
}
.user-item {
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: background-color 0.3s ease;
user-select: none;
}
.user-item:hover,
.user-item:focus {
background-color: #393d4d;
outline: none;
}
.user-avatar {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #0984e3);
color: white;
font-weight: 700;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.user-name {
flex-grow: 1;
font-size: 0.95rem;
color: #dcdde1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
}
.user-status {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #44bd32;
box-shadow: 0 0 4px #44bd32aa;
flex-shrink: 0;
}
/* Main Chat Panel */
.chat-panel {
display: flex;
flex-direction: column;
background: #181a20;
user-select: none;
min-width: 0;
}
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid #2e2e3a;
font-weight: 700;
font-size: 1.3rem;
background: linear-gradient(135deg, #6c5ce7, #0984e3);
color: white;
user-select: none;
flex-shrink: 0;
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
padding: 20px 24px;
scrollbar-width: thin;
scrollbar-color: #394264 transparent;
background: #121217;
user-select: text;
-webkit-overflow-scrolling: touch;
min-height: 0;
}
.chat-messages::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-thumb {
background-color: #394264;
border-radius: 4px;
}
.message {
max-width: 60%;
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.4;
font-size: 0.95rem;
word-wrap: break-word;
box-shadow: 0 2px 5px rgb(0 0 0 / 0.3);
user-select: text;
display: flex;
align-items: center;
gap: 8px;
}
.message.user {
background: linear-gradient(135deg, #0984e3, #6c5ce7);
color: white;
align-self: flex-end;
border-bottom-right-radius: 2px;
justify-content: flex-end;
}
.message.other {
background: #2d2f3a;
color: #ddd;
align-self: flex-start;
border-bottom-left-radius: 2px;
}
.msg-username {
font-weight: 700;
cursor: pointer;
user-select: text;
color: #a2d2ff;
}
.chat-input-container {
display: flex;
padding: 12px 24px;
border-top: 1px solid #2e2e3a;
background: #20232a;
align-items: center;
user-select: none;
flex-shrink: 0;
}
.chat-input {
flex-grow: 1;
padding: 10px 16px;
font-size: 1rem;
border-radius: 24px;
border: none;
background-color: #2d2f3a;
color: #eee;
outline: none;
transition: background-color 0.2s ease;
}
.chat-input::placeholder {
color: #888;
}
.chat-input:focus {
background-color: #424559;
}
.send-btn {
margin-left: 12px;
background: linear-gradient(135deg, #6c5ce7, #0984e3);
border: none;
padding: 10px 18px;
color: white;
font-weight: 700;
font-size: 1rem;
border-radius: 24px;
cursor: pointer;
user-select: none;
transition: background-color 0.3s ease;
}
.send-btn:hover {
background: linear-gradient(135deg, #5353c3, #0761c7);
}
/* Private Chat Popup */
.private-chat-popup {
position: fixed;
bottom: 16px;
right: 16px;
width: 320px;
max-height: 480px;
background: #292b37;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.7);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1000;
font-size: 0.9rem;
animation: fadeInUp 0.3s ease forwards;
user-select: none;
resize: both;
overflow: auto;
}
.private-chat-header {
background: linear-gradient(135deg, #6c5ce7, #0984e3);
padding: 12px 16px;
font-weight: 700;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
cursor: move;
}
.private-chat-close {
cursor: pointer;
font-weight: 700;
font-size: 1.2rem;
line-height: 0;
user-select: none;
transition: color 0.3s ease;
background: none;
border: none;
color: white;
}
.private-chat-close:hover {
color: #ffd32a;
}
.private-chat-messages {
flex-grow: 1;
padding: 12px 16px;
overflow-y: auto;
background: #20212a;
scrollbar-width: thin;
scrollbar-color: #52526a transparent;
-webkit-overflow-scrolling: touch;
user-select: text;
min-height: 120px;
max-height: 320px;
}
.private-chat-messages::-webkit-scrollbar {
width: 6px;
}
.private-chat-messages::-webkit-scrollbar-thumb {
background-color: #52526a;
border-radius: 4px;
}
.private-message {
margin-bottom: 12px;
background: #3a3c4d;
padding: 10px 14px;
border-radius: 10px;
color: #e4e6f0;
max-width: 90%;
word-wrap: break-word;
}
.private-message.user {
background: linear-gradient(135deg, #0984e3, #6c5ce7);
color: white;
align-self: flex-end;
border-bottom-right-radius: 2px;
}
.private-message.other {
background: #3a3c4d;
color: #ddd;
align-self: flex-start;
border-bottom-left-radius: 2px;
}
.private-chat-input-container {
display: flex;
padding: 10px 14px;
border-top: 1px solid #44475a;
background: #2b2e42;
align-items: center;
flex-shrink: 0;
}
.private-chat-input {
flex-grow: 1;
padding: 8px 14px;
font-size: 0.95rem;
border-radius: 18px;
border: none;
background-color: #3d3f56;
color: #eee;
outline: none;
transition: background-color 0.2s ease;
}
.private-chat-input::placeholder {
color: #bbb;
}
.private-chat-input:focus {
background-color: #56577b;
}
.private-send-btn {
margin-left: 10px;
background: linear-gradient(135deg, #6c5ce7, #0984e3);
border: none;
padding: 8px 20px;
color: white;
font-weight: 700;
font-size: 0.95rem;
border-radius: 18px;
cursor: pointer;
user-select: none;
transition: background-color 0.3s ease;
}
.private-send-btn:hover {
background: linear-gradient(135deg, #5353c3, #0761c7);
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Context Menu Styling */
.context-menu {
position: absolute;
background: #2a2c38;
border-radius: 8px;
padding: 8px 0;
width: 180px;
box-shadow: 0 6px 15px rgba(0,0,0,0.4);
z-index: 2000;
display: none;
user-select: none;
}
.context-menu.visible {
display: block;
}
.context-menu-item {
padding: 10px 16px;
font-size: 0.9rem;
color: #e0e0e0;
cursor: pointer;
transition: background-color 0.25s ease;
}
.context-menu-item:hover,
.context-menu-item:focus {
background: #5353c3;
color: white;
outline: none;
}
/* Responsive */
@media (max-width: 768px) {
body {
display: block;
height: auto;
}
.container {
height: auto;
display: flex;
flex-direction: column;
}
.chat-panel {
order: 2;
height: 70vh;
min-width: 100%;
}
.user-list {
order: 1;
height: 30vh;
min-width: 100%;
border-left: none !important;
border-top: 1px solid #2e2e3a;
}
.chat-messages {
height: 100%;
min-height: 0;
}
.chat-input-container {
padding: 8px 16px;
}
.chat-input {
font-size: 0.9rem;
padding: 8px 12px;
}
.send-btn {
padding: 8px 12px;
font-size: 0.9rem;
}
.user-list-header, .chat-header {
padding: 12px 16px;
font-size: 1.1rem;
}
.user-item {
padding: 8px 10px;
}
.user-avatar {
width: 28px;
height: 28px;
font-size: 0.9rem;
}
.user-name {
font-size: 0.9rem;
}
.private-chat-popup {
width: 90vw;
max-height: 50vh;
bottom: 8px;
right: 5vw;
font-size: 0.85rem;
}
.private-chat-messages {
max-height: 200px;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Main Chat -->
<section class="chat-panel" aria-label="Public chat messages">
<header class="chat-header">Global Chat</header>
<div class="chat-messages" id="chatMessages" role="log" aria-live="polite" aria-relevant="additions"></div>
<form id="chatForm" class="chat-input-container" aria-label="Send message">
<input type="text" id="chatInput" class="chat-input" placeholder="Enter message..." autocomplete="off" aria-required="true" />
<button type="submit" class="send-btn" aria-label="Send message">Send</button>
</form>
</section>
<!-- User List -->
<aside class="user-list" aria-label="User list">
<div class="user-list-header">Online Users</div>
<div class="users" id="userList" role="list" tabindex="0" aria-live="polite"></div>
</aside>
</div>
<!-- Private Chat Popup Template -->
<template id="privateChatTemplate">
<section class="private-chat-popup" role="dialog" aria-modal="true" aria-label="Private chat with user">
<header class="private-chat-header">
<span class="private-chat-user"></span>
<button class="private-chat-close" aria-label="Close private chat">&times;</button>
</header>
<div class="private-chat-messages" role="log" aria-live="polite" aria-relevant="additions"></div>
<form class="private-chat-input-container" aria-label="Send private message">
<input type="text" class="private-chat-input" placeholder="Enter private message..." autocomplete="off" aria-required="true" />
<button type="submit" class="private-send-btn" aria-label="Send private message">Send</button>
</form>
</section>
</template>
<!-- Context Menu -->
<nav id="contextMenu" class="context-menu" role="menu" aria-hidden="true" tabindex="-1" aria-label="User actions">
<div class="context-menu-item" data-action="openProfile" role="menuitem" tabindex="0">View Profile</div>
<div class="context-menu-item" data-action="privateChat" role="menuitem" tabindex="0">Open Private Chat</div>
<div class="context-menu-item" data-action="ban" role="menuitem" tabindex="0">Ban User</div>
<div class="context-menu-item" data-action="kick" role="menuitem" tabindex="0">Kick User</div>
</nav>
<script>
const users = [
{ id: 'u1', name: 'Alice' },
{ id: 'u2', name: 'Bob' },
{ id: 'u3', name: 'Charlie' },
{ id: 'u4', name: 'David' }
];
const userListElem = document.getElementById('userList');
const chatMessagesElem = document.getElementById('chatMessages');
const chatForm = document.getElementById('chatForm');
const chatInput = document.getElementById('chatInput');
const privateChats = new Map();
const contextMenu = document.getElementById('contextMenu');
let contextMenuTargetUser = null;
let contextMenuTargetType = null;
function renderUserList() {
userListElem.innerHTML = '';
users.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
userItem.setAttribute('role', 'listitem');
userItem.tabIndex = 0;
userItem.dataset.userid = user.id;
const avatar = document.createElement('div');
avatar.className = 'user-avatar';
avatar.textContent = user.name.charAt(0).toUpperCase();
const name = document.createElement('div');
name.className = 'user-name';
name.textContent = user.name;
const status = document.createElement('div');
status.className = 'user-status';
status.title = 'Online';
userItem.appendChild(avatar);
userItem.appendChild(name);
userItem.appendChild(status);
userItem.addEventListener('click', () => openPrivateChat(user));
userItem.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPrivateChat(user); } });
userItem.addEventListener('contextmenu', e => {
e.preventDefault();
showContextMenu(e, user, 'userlist');
});
userListElem.appendChild(userItem);
});
}
function addPublicMessage(text, senderId = null, senderName = null) {
const msg = document.createElement('div');
msg.className = senderId === "me" ? 'message user' : 'message other';
const usernameSpan = document.createElement('span');
usernameSpan.className = 'msg-username';
if(!senderName) senderName = senderId ? (users.find(u => u.id === senderId)?.name || 'Someone') : 'Someone';
if(!senderId) senderId = 'someone';
usernameSpan.textContent = senderName;
usernameSpan.tabIndex = 0;
usernameSpan.dataset.userid = senderId;
// Important: Use mousedown event to open context menu instantly on right click on usernames in main chat
usernameSpan.addEventListener('contextmenu', e => {
e.preventDefault();
const user = users.find(u => u.id === senderId) || {id: 'someone', name: senderName};
showContextMenu(e, user, 'chat');
});
usernameSpan.addEventListener('keydown', (e) => {
if ((e.shiftKey && e.key === "F10") || e.key === "ContextMenu") {
e.preventDefault();
const user = users.find(u => u.id === senderId) || {id: 'someone', name: senderName};
showContextMenu(e, user, 'chat');
}
});
msg.appendChild(usernameSpan);
const textNode = document.createTextNode(`: ${text}`);
msg.appendChild(textNode);
chatMessagesElem.appendChild(msg);
chatMessagesElem.scrollTop = chatMessagesElem.scrollHeight;
}
chatForm.addEventListener('submit', e => {
e.preventDefault();
const msg = chatInput.value.trim();
if (!msg) return;
addPublicMessage(msg, "me", "You");
chatInput.value = '';
chatInput.focus();
});
function openPrivateChat(user) {
if (privateChats.has(user.id)) {
const chatPopup = privateChats.get(user.id);
chatPopup.element.style.display = 'flex';
chatPopup.input.focus();
return;
}
const template = document.getElementById('privateChatTemplate');
const clone = template.content.cloneNode(true);
const popup = clone.querySelector('.private-chat-popup');
const headerUser = clone.querySelector('.private-chat-user');
const closeBtn = clone.querySelector('.private-chat-close');
const msgsElem = clone.querySelector('.private-chat-messages');
const form = clone.querySelector('form');
const input = clone.querySelector('input');
headerUser.textContent = user.name;
closeBtn.addEventListener('click', () => {
popup.style.display = 'none';
});
function addPrivateMessage(text, sender = 'other') {
const msg = document.createElement('div');
msg.className = `private-message ${sender}`;
msg.textContent = text;
msgsElem.appendChild(msg);
msgsElem.scrollTop = msgsElem.scrollHeight;
}
form.addEventListener('submit', e => {
e.preventDefault();
const message = input.value.trim();
if (!message) return;
addPrivateMessage(message, 'user');
input.value = '';
input.focus();
});
popup.style.display = 'flex';
document.body.appendChild(popup);
privateChats.set(user.id, {
element: popup,
input: input,
addMessage: addPrivateMessage
});
input.focus();
}
function showContextMenu(event, user, type) {
contextMenuTargetUser = user;
contextMenuTargetType = type;
const menuWidth = 180;
const menuHeight = 150;
let x = event.clientX;
let y = event.clientY;
if (x + menuWidth > window.innerWidth) {
x = window.innerWidth - menuWidth - 8;
}
if (y + menuHeight > window.innerHeight) {
y = window.innerHeight - menuHeight - 8;
}
contextMenu.style.left = x + 'px';
contextMenu.style.top = y + 'px';
contextMenu.classList.add('visible');
contextMenu.setAttribute('aria-hidden', 'false');
const firstItem = contextMenu.querySelector('.context-menu-item');
if (firstItem) firstItem.focus();
document.addEventListener('click', outsideClickHandler);
document.addEventListener('keydown', keyboardHandler);
}
function hideContextMenu() {
contextMenu.classList.remove('visible');
contextMenu.setAttribute('aria-hidden', 'true');
contextMenuTargetUser = null;
contextMenuTargetType = null;
document.removeEventListener('click', outsideClickHandler);
document.removeEventListener('keydown', keyboardHandler);
}
function outsideClickHandler(event) {
if (!contextMenu.contains(event.target)) {
hideContextMenu();
}
}
function keyboardHandler(event) {
if (!contextMenu.classList.contains('visible')) return;
const items = [...contextMenu.querySelectorAll('.context-menu-item')];
const currentIndex = items.indexOf(document.activeElement);
switch(event.key) {
case 'Escape':
hideContextMenu();
break;
case 'ArrowDown':
event.preventDefault();
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex = (currentIndex - 1 + items.length) % items.length;
items[prevIndex].focus();
break;
case 'Enter':
event.preventDefault();
if (document.activeElement.classList.contains('context-menu-item')) {
document.activeElement.click();
}
break;
}
}
contextMenu.addEventListener('click', e => {
if (!e.target.classList.contains('context-menu-item')) return;
const action = e.target.dataset.action;
if (!contextMenuTargetUser) return;
switch (action) {
case 'openProfile':
alert(`Open profile for ${contextMenuTargetUser.name} (ID: ${contextMenuTargetUser.id})`);
break;
case 'privateChat':
openPrivateChat(contextMenuTargetUser);
break;
case 'ban':
alert(`Ban user ${contextMenuTargetUser.name}`);
break;
case 'kick':
alert(`Kick user ${contextMenuTargetUser.name}`);
break;
default:
break;
}
hideContextMenu();
});
renderUserList();
addPublicMessage('Welcome to the chat!', 'someone', 'Someone');
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment