Commit 388b43de authored by nextime's avatar nextime

chat html improvements

parent 93089ca1
#!/usr/bin/env python3
import obsws_python as obs
import time
# load conn info from config.toml
cl = obs.EventClient(host='192.168.42.115', port=4455, password='motorol4')
cr = obs.ReqClient(host='192.168.42.115', port=4455, password='motorol4')
def on_scene_item_enable_state_changed(data):
print("scscene_item_enable_state_changed")
print(data.attrs())
print(data.scene_item_enabled, data.scene_item_id, data.scene_name, data.scene_uuid)
cl.callback.register(on_scene_item_enable_state_changed)
# returns a list of currently registered events
print(cl.callback.get())
# You may also deregister a callback
#cl.callback.deregister(on_input_mute_state_changed)
now=time.time()-30
while True:
if (time.time() - now) > 30:
now = time.time()
cr.broadcast_custom_event({'eventData': {'eventType': 'Ping', 'time': time.time()}})
time.sleep(.1)
[General]
log_file = /tmp/streaming_control.log
log_level = INFO
[Web]
stream_url = https://192.168.42.1/HLS/record/Live.m3u8
rtmp_url = rtmp://192.168.42.1/record/Live
port = 5000
[Tkinter]
window_title = SHM Cam Studio
button_width = 20
button_height = 2
font_size = 12
#[OBS:mdma]
#host = 192.168.42.115
#port = 4455
#password = motorol4
[OBS:slut]
host = 192.168.42.125
port = 4455
password = motorol4
[OBS:leeloo]
host = 192.168.42.111
port = 4455
password = motorol4
# OUTPUT are shenes on OBS sending to a specific stream, can be a rtmp stream with
# OBS multistream plugin or a virtual cam, both the included in OBS or the ones
# from a plugin.
[OUTPUT:smleeloo]
obs = leeloo
scene = LIVE SFW
status.open.disable = 5,4,3
status.close.disable = 3
status.close.enable = 5,4
status.tease.disable = 4
status.tease.enable = 5,3
[OUTPUT:smstefy]
obs = slut
scene = live SFW
status.open.disable = 5,4,3
status.close.disable = 3
status.close.enable = 5,4
status.tease.disable = 4
status.tease.enable = 5,3
[OUTPUT:shine]
obs = slut
scene = SHINE
status.open.disable = 4,2,3
status.close.disable = 3
status.close.enable = 4,2
status.tease.disable = 2
status.tease.enable = 4,3
#[OUTPUT:livejasmin]
#obs = slut
#scene = LIVE JM
#status.open.disable = 2,3,4,5
##status.close.disable = 2
#status.close.enable = 3,4,5
#status.tease.disable = 3
#status.tease.enable = 2,4,5
# BUTTON:<rownum>:<button_name>
[BUTTON:1:private_shine]
title = Private Shine
action = private_shine
color = maroon
[BUTTON:1:private_stefy]
title = Private Stefy
action = private_stefy
color = maroon
[BUTTON:1:private_leelo]
title = Private Leeloo
action = private_leeloo
color = maroon
[BUTTON:1:open_all]]
title = Open ALL
action = open_all
color = green
#[BUTTON:1:private_jasmine]
#title = Private Jasmine
#action = private_jasmine
#color = green
[BUTTON:2:shine_openclose]
title = Open/Close Shine
action = shine_openclose
output = shine
color.close = red
color.open = green
color.tease = orange
color = grey
[BUTTON:2:stefy_openclose]
title = Open/Close Stefy
action = stefy_openclose
output = smstefy
color.close = red
color.open = green
color.tease = orange
color = grey
[BUTTON:2:leelo_openclose]
title = Open/Close Leeloo
action = leelo_openclose
output = smleeloo
color.close = red
color.open = green
color.tease = orange
color = grey
[BUTTON:2:spotify]
title = MUSIC PAUSE
action = spotify_pause
color = blue
#[BUTTON:2:leelo_livejasmine]
#title = Open/Close JASM
#action = jasmine_openclose
[BUTTON:3:tease_all]
title = SCENE Tease ALL
action = tease_all
color = teal
color.active = turquoise
feedback = scene
[BUTTON:3:tease]
title = SCENE Tease
action = tease
color = teal
color.active = turquoise
feedback = scene
[BUTTON:3:scene_all_open]
title = SCENE ALL OPEN
action = open_all
color = teal
color.active = turquoise
feedback = scene
[BUTTON:3:scene_shile_always_open]
title = SCENE SHINE ALWAYS OPEN
action = open_all_shine_open
color = teal
color.active = turquoise
feedback = scene
[BUTTON:3:scene_manual]
title = MANUAL
action = scene_manual
color = blue
color.manual = blue
color.automatic = red
title.manual = MANUAL
title.automatic = AUTO
feedback = status
[ACTION:scene_manual]
setstatus = change
[ACTION:spotify_pause]
execute = /home/nextime/bin/spotifypause
[ACTION:private_shine]
execute = /usr/local/bin/smblur_private
[ACTION:private_stefy]
execute = /usr/local/bin/smblur_private_stefy
[ACTION:private_leeloo]
execute = /usr/local/bin/smblur_private_leeloo
#[ACTION:private_jasmine]
#execute = /usr/local/bin/smblur_private_jasmin
[ACTION:leelo_openclose]
execute = /usr/local/bin/smblur_leeloo
[ACTION:shine_openclose]
execute = /usr/local/bin/smblur_shine
[ACTION:stefy_openclose]
execute = /usr/local/bin/smblur_stefy
#[ACTION:jasmine_openclose]
#execute = /usr/local/bin/smblur_jasmin
[ACTION:tease_all]
execute = /usr/local/bin/smblur_teaseall
setscene = tease_all
[ACTION:tease]
execute = /usr/local/bin/smblur_tease
setscene = tease
[ACTION:open_all]
execute = /usr/local/bin/smblur_clean
setscene = open_all
[ACTION:open_all_shine_open]
execute = /usr/local/bin/smblur_clean
setscene = open_all_shine_open
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
align-items: center; align-items: center;
padding: 0 20px; padding: 0 20px;
gap: 10px; gap: 10px;
border-bottom: 2px solid #3b82f6;
} }
.top-bar img { .top-bar img {
height: 40px; height: 40px;
...@@ -261,13 +261,13 @@ ...@@ -261,13 +261,13 @@
border-radius: 10px 10px 10px 0px; border-radius: 10px 10px 10px 0px;
} }
.message.notify-system { .message.notify-system {
background: linear-gradient(to right, #1f2937, #2a1b3d); background: linear-gradient(to right, #964a5f, #71143f);
margin: 8px 10%; margin: 8px 10%;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
} }
.message.notify-platform { .message.notify-platform {
background: linear-gradient(to right, #1f2937, #2a1b3d); background: linear-gradient(to right, #b54d23, #813316);
margin: 8px 10%; margin: 8px 10%;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
...@@ -287,7 +287,7 @@ ...@@ -287,7 +287,7 @@
cursor: pointer; cursor: pointer;
} }
.message .content { .message .content {
color: #a3bffa; color: #dfe2ea;
} }
.message img { .message img {
max-width: 100px; max-width: 100px;
...@@ -423,7 +423,7 @@ ...@@ -423,7 +423,7 @@
background: #2a2a2a; background: #2a2a2a;
border: 1px solid #3b82f6; border: 1px solid #3b82f6;
padding: 10px; padding: 10px;
z-index: 100; z-index: 1000;
display: none; display: none;
border-radius: 8px; border-radius: 8px;
} }
...@@ -440,9 +440,13 @@ ...@@ -440,9 +440,13 @@
border-radius: 20px; border-radius: 20px;
font-size: 14px; font-size: 14px;
} }
.context-menu a:hover, .context-menu button:hover { .context-menu a:hover, .context-menu button:hover:not([disabled]) {
background: #3b82f6; background: #3b82f6;
} }
.context-menu button[disabled] {
color: #aaa;
cursor: default;
}
/* Earnings table */ /* Earnings table */
.earnings-table { .earnings-table {
...@@ -488,6 +492,163 @@ ...@@ -488,6 +492,163 @@
background: #b91c1c; background: #b91c1c;
} }
/* Private chats list styles */
.private-chats-list {
margin-top: 10px;
}
.private-chat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: #1e293b;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.private-chat-item:hover {
background: #2a3b53;
}
.private-chat-item-name {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-chat-item-close {
color: #aaa;
font-size: 16px;
font-weight: bold;
cursor: pointer;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
}
.private-chat-item-close:hover {
color: #fff;
}
.no-private-chats {
text-align: center;
padding: 20px;
color: #aaa;
font-style: italic;
}
/* Private chat window styles */
.private-chat-window-container {
display: none;
position: fixed;
top: 100px;
right: 100px;
width: 350px;
z-index: 990;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
border-radius: 8px;
resize: both;
overflow: hidden;
min-width: 300px;
min-height: 310px;
}
.private-chat-content {
background: linear-gradient(to bottom, #2a2a2a, #1f1f1f);
padding: 7px;
padding-top: 0px;
/* border: 1px solid #3b82f6; */
border-radius: 8px;
height: 100%;
display: flex;
flex-direction: column;
}
.private-chat-header {
display: flex;
background: linear-gradient(to right, #3b4d66, #6a1b9a);
justify-content: space-between;
align-items: center;
padding: 10px;
margin-bottom: 10px;
cursor: move; /* Indicate draggable */
}
.private-chat-title {
font-size: 16px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-chat-controls {
display: flex;
gap: 5px;
}
.minimize-chat,
.close-chat {
color: #aaa;
font-size: 18px;
font-weight: bold;
cursor: pointer;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
}
.minimize-chat:hover,
.close-chat:hover {
color: #fff;
}
.private-chat-messages {
flex: 1;
background: #121212;
border-radius: 8px;
padding: 10px;
overflow-y: auto;
margin-bottom: 10px;
min-height: 200px;
}
.private-chat-input {
display: flex;
gap: 5px;
}
.private-chat-input textarea {
flex: 1;
height: 40px;
padding: 8px 10px;
border: none;
border-radius: 20px;
background: #1e293b;
color: #fff;
resize: none;
font-size: 14px;
}
.private-chat-input button {
height: 40px;
padding: 8px 15px;
background: #3b82f6;
border: none;
border-radius: 20px;
color: #fff;
cursor: pointer;
font-weight: 500;
font-size: 14px;
}
/* Mobile layout */ /* Mobile layout */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
...@@ -611,11 +772,18 @@ ...@@ -611,11 +772,18 @@
</div> </div>
</div> </div>
<div class="right-column"> <div class="right-column">
<div class="user-list-header"> <div class="tabs">
<span>Userlist</span> <div class="tab active" data-tab="userlist">Userlist (<span id="total-users">0</span>)</div>
<span id="total-users">0</span> <div class="tab" data-tab="private-chats">Private Chats (<span id="private-chats-count">0</span>)</div>
</div>
<div class="tab-content active" id="userlist">
<div class="user-list" id="user-list"></div>
</div>
<div class="tab-content" id="private-chats">
<div class="private-chats-list" id="private-chats-list">
<div class="no-private-chats">No private chats open</div>
</div>
</div> </div>
<div class="user-list" id="user-list"></div>
</div> </div>
</div> </div>
<div class="context-menu" id="context-menu"> <div class="context-menu" id="context-menu">
...@@ -625,6 +793,9 @@ ...@@ -625,6 +793,9 @@
<button id="context-kick">Kick User</button> <button id="context-kick">Kick User</button>
<button id="context-private">Private Chat</button> <button id="context-private">Private Chat</button>
</div> </div>
<!-- Container for multiple private chat windows -->
<div id="private-chat-windows-container"></div>
<script> <script>
// Fake data for messages, users, earnings, status, and RTSP URLs // Fake data for messages, users, earnings, status, and RTSP URLs
...@@ -800,6 +971,9 @@ ...@@ -800,6 +971,9 @@
e.preventDefault(); e.preventDefault();
showContextMenu(e, msg.sender); showContextMenu(e, msg.sender);
}); });
senderSpan.addEventListener('dblclick', () => {
openPrivateChat(msg.sender);
});
div.appendChild(senderSpan); div.appendChild(senderSpan);
div.appendChild(document.createTextNode(': ')); div.appendChild(document.createTextNode(': '));
} }
...@@ -811,7 +985,10 @@ ...@@ -811,7 +985,10 @@
if (senderSpan && msg.sender !== 'me' && !msg.type?.startsWith('notify')) { if (senderSpan && msg.sender !== 'me' && !msg.type?.startsWith('notify')) {
senderSpan.addEventListener('contextmenu', (e) => { senderSpan.addEventListener('contextmenu', (e) => {
e.preventDefault(); e.preventDefault();
showContextMenu(e, senderSpan.dataset.sender); showContextMenu(e, senderSpan.dataset.sender, false);
});
senderSpan.addEventListener('dblclick', () => {
openPrivateChat(senderSpan.dataset.sender);
}); });
} }
chatWindow.appendChild(div); chatWindow.appendChild(div);
...@@ -910,7 +1087,7 @@ ...@@ -910,7 +1087,7 @@
// Context menu // Context menu
const contextMenu = document.getElementById('context-menu'); const contextMenu = document.getElementById('context-menu');
function showContextMenu(e, sender) { function showContextMenu(e, sender, isFromPrivateChat = false) {
e.preventDefault(); e.preventDefault();
const parts = sender.split('@'); const parts = sender.split('@');
if (parts.length !== 2) { if (parts.length !== 2) {
...@@ -933,11 +1110,29 @@ ...@@ -933,11 +1110,29 @@
console.error(`User not found: ${username} in ${platform}.${account}`); console.error(`User not found: ${username} in ${platform}.${account}`);
return; return;
} }
// Set profile link
contextMenu.querySelector('#context-profile').href = `https://${platform}.com/profile/${username}`; contextMenu.querySelector('#context-profile').href = `https://${platform}.com/profile/${username}`;
contextMenu.querySelector('#context-tokens').textContent = `Tokens: ${user.tokens}`;
// Change tokens to non-clickable text
const tokensElement = contextMenu.querySelector('#context-tokens');
tokensElement.textContent = `Tokens: ${user.tokens}`;
tokensElement.style.cursor = 'default';
tokensElement.setAttribute('disabled', 'disabled');
// Set other button actions
contextMenu.querySelector('#context-ban').onclick = () => alert(`Ban ${sender}`); contextMenu.querySelector('#context-ban').onclick = () => alert(`Ban ${sender}`);
contextMenu.querySelector('#context-kick').onclick = () => alert(`Kick ${sender}`); contextMenu.querySelector('#context-kick').onclick = () => alert(`Kick ${sender}`);
contextMenu.querySelector('#context-private').onclick = () => alert(`Open private chat with ${sender}`);
// Hide or show private chat button based on context
const privateButton = contextMenu.querySelector('#context-private');
if (isFromPrivateChat) {
privateButton.style.display = 'none';
} else {
privateButton.style.display = 'block';
privateButton.onclick = () => openPrivateChat(sender);
}
contextMenu.style.display = 'block'; contextMenu.style.display = 'block';
contextMenu.style.left = `${e.pageX}px`; contextMenu.style.left = `${e.pageX}px`;
contextMenu.style.top = `${e.pageY}px`; contextMenu.style.top = `${e.pageY}px`;
...@@ -971,7 +1166,10 @@ ...@@ -971,7 +1166,10 @@
`; `;
userDiv.addEventListener('contextmenu', (e) => { userDiv.addEventListener('contextmenu', (e) => {
e.preventDefault(); e.preventDefault();
showContextMenu(e, userDiv.dataset.sender); showContextMenu(e, userDiv.dataset.sender, false);
});
userDiv.addEventListener('dblclick', () => {
openPrivateChat(userDiv.dataset.sender);
}); });
usersDiv.appendChild(userDiv); usersDiv.appendChild(userDiv);
}); });
...@@ -1037,12 +1235,383 @@ ...@@ -1037,12 +1235,383 @@
renderEarnings(); renderEarnings();
}); });
// Private Chat functionality
const privateChatWindowsContainer = document.getElementById('private-chat-windows-container');
// Store private chat messages and states
const privateChats = {}; // Store chat messages by user
const openPrivateChats = new Set(); // Track open private chats
const activeChatWindows = {}; // Store references to active chat windows
// Track dragging state for each window
const dragStates = {};
// Private chats list
const privateChatsListElement = document.getElementById('private-chats-list');
function renderPrivateChatslist() {
privateChatsListElement.innerHTML = '';
// Update the private chats count in the tab title
const privateChatsCount = document.getElementById('private-chats-count');
privateChatsCount.textContent = openPrivateChats.size;
if (openPrivateChats.size === 0) {
const noChatsDiv = document.createElement('div');
noChatsDiv.className = 'no-private-chats';
noChatsDiv.textContent = 'No private chats open';
privateChatsListElement.appendChild(noChatsDiv);
return;
}
openPrivateChats.forEach(sender => {
const chatItem = document.createElement('div');
chatItem.className = 'private-chat-item';
chatItem.dataset.sender = sender;
const nameSpan = document.createElement('div');
nameSpan.className = 'private-chat-item-name';
nameSpan.textContent = sender;
const closeButton = document.createElement('div');
closeButton.className = 'private-chat-item-close';
closeButton.innerHTML = '&times;';
closeButton.title = 'Close chat';
chatItem.appendChild(nameSpan);
chatItem.appendChild(closeButton);
// Click on chat item to open/restore the chat
chatItem.addEventListener('click', (e) => {
if (e.target !== closeButton) {
openPrivateChat(sender);
}
});
// Right-click on chat item to show context menu
chatItem.addEventListener('contextmenu', (e) => {
if (e.target !== closeButton) {
e.preventDefault();
showContextMenu(e, sender, false);
}
});
// Click on close button to close the chat
closeButton.addEventListener('click', (e) => {
e.stopPropagation();
closePrivateChat(sender);
});
privateChatsListElement.appendChild(chatItem);
});
}
// Create a new private chat window
function createPrivateChatWindow(sender) {
// Create window container
const chatWindow = document.createElement('div');
chatWindow.className = 'private-chat-window-container';
chatWindow.id = `private-chat-window-${sender.replace(/[@.]/g, '-')}`;
chatWindow.dataset.sender = sender;
// Position windows in a cascading manner
const openWindowsCount = document.querySelectorAll('.private-chat-window-container').length;
const offsetX = 50 + (openWindowsCount * 20);
const offsetY = 100 + (openWindowsCount * 20);
chatWindow.style.top = `${offsetY}px`;
chatWindow.style.right = `${offsetX}px`;
// Create window content
chatWindow.innerHTML = `
<div class="private-chat-content">
<div class="private-chat-header" data-sender="${sender}">
<div class="private-chat-title">Private Chat with <span class="private-chat-username">${sender}</span></div>
<div class="private-chat-controls">
<span class="minimize-chat" title="Minimize">_</span>
<span class="close-chat" title="Close">&times;</span>
</div>
</div>
<div class="private-chat-messages"></div>
<div class="private-chat-input">
<textarea placeholder="Type a private message..."></textarea>
<button class="send-private-message">Send</button>
</div>
</div>
`;
// Add to DOM
privateChatWindowsContainer.appendChild(chatWindow);
// Setup dragging for this window
setupDraggableWindow(chatWindow);
// Setup event listeners for this window
setupChatWindowEvents(chatWindow, sender);
// Store reference to the window
activeChatWindows[sender] = chatWindow;
return chatWindow;
}
// Setup dragging functionality for a chat window
function setupDraggableWindow(chatWindow) {
const header = chatWindow.querySelector('.private-chat-header');
const sender = header.dataset.sender;
// Initialize drag state for this window
dragStates[sender] = {
isDragging: false,
offsetX: 0,
offsetY: 0
};
header.addEventListener('mousedown', (e) => {
const state = dragStates[sender];
state.isDragging = true;
state.offsetX = e.clientX - chatWindow.getBoundingClientRect().left;
state.offsetY = e.clientY - chatWindow.getBoundingClientRect().top;
header.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
const state = dragStates[sender];
if (state && state.isDragging) {
const x = e.clientX - state.offsetX;
const y = e.clientY - state.offsetY;
// Keep window within viewport bounds
const maxX = window.innerWidth - chatWindow.offsetWidth;
const maxY = window.innerHeight - chatWindow.offsetHeight;
chatWindow.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
chatWindow.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
}
});
// Add mouseup event to stop dragging
document.addEventListener('mouseup', () => {
const state = dragStates[sender];
if (state) {
state.isDragging = false;
header.style.cursor = 'move';
}
});
}
// Setup event listeners for a chat window
function setupChatWindowEvents(chatWindow, sender) {
const closeBtn = chatWindow.querySelector('.close-chat');
const minimizeBtn = chatWindow.querySelector('.minimize-chat');
const sendBtn = chatWindow.querySelector('.send-private-message');
const textarea = chatWindow.querySelector('textarea');
const header = chatWindow.querySelector('.private-chat-header');
// Close button
closeBtn.addEventListener('click', () => {
closePrivateChat(sender);
});
// Minimize button
minimizeBtn.addEventListener('click', () => {
minimizePrivateChat(sender);
});
// Right-click on header to show context menu
header.addEventListener('contextmenu', (e) => {
e.preventDefault();
showContextMenu(e, sender, true);
});
// Send button
sendBtn.addEventListener('click', () => {
sendPrivateMessage(sender);
});
// Enter key to send
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendPrivateMessage(sender);
}
});
}
// Open a private chat window
function openPrivateChat(sender) {
// Initialize private chat if it doesn't exist
if (!privateChats[sender]) {
privateChats[sender] = [];
}
// Add to open chats if not already there
if (!openPrivateChats.has(sender)) {
openPrivateChats.add(sender);
renderPrivateChatslist();
}
// Get or create the chat window
let chatWindow = activeChatWindows[sender];
if (!chatWindow) {
chatWindow = createPrivateChatWindow(sender);
}
// Show and restore the window
chatWindow.style.display = 'block';
// Render messages
renderPrivateMessages(sender);
// Focus the input
chatWindow.querySelector('textarea').focus();
}
// Close a private chat window
function closePrivateChat(sender) {
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
chatWindow.remove();
delete activeChatWindows[sender];
delete dragStates[sender];
}
openPrivateChats.delete(sender);
renderPrivateChatslist();
}
// Minimize a private chat window
function minimizePrivateChat(sender) {
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
chatWindow.style.display = 'none';
}
}
// Render messages in a private chat window
function renderPrivateMessages(sender) {
const chatWindow = activeChatWindows[sender];
if (!chatWindow) return;
const messagesContainer = chatWindow.querySelector('.private-chat-messages');
messagesContainer.innerHTML = '';
if (privateChats[sender] && privateChats[sender].length > 0) {
privateChats[sender].forEach(msg => {
const div = document.createElement('div');
div.className = `message ${msg.sender === 'me' ? 'me' : 'other'}`;
if (msg.sender === 'me') {
const senderSpan = document.createElement('span');
senderSpan.className = 'sender';
senderSpan.textContent = 'me:';
senderSpan.style.color = '#93c5fd';
div.appendChild(senderSpan);
div.appendChild(document.createTextNode(' '));
} else {
const senderSpan = document.createElement('span');
senderSpan.className = 'sender';
senderSpan.textContent = msg.sender;
div.appendChild(senderSpan);
div.appendChild(document.createTextNode(': '));
}
const contentSpan = document.createElement('span');
contentSpan.className = 'content';
contentSpan.textContent = msg.content;
div.appendChild(contentSpan);
messagesContainer.appendChild(div);
});
} else {
const div = document.createElement('div');
div.className = 'message notify-system';
div.textContent = 'Start a private conversation...';
messagesContainer.appendChild(div);
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Send a private message
function sendPrivateMessage(sender) {
const chatWindow = activeChatWindows[sender];
if (!chatWindow) return;
const textarea = chatWindow.querySelector('textarea');
const content = textarea.value.trim();
if (content) {
// Add message to private chat
if (!privateChats[sender]) {
privateChats[sender] = [];
}
privateChats[sender].push({
sender: 'me',
content: content,
timestamp: new Date().toISOString()
});
// Simulate receiving a response after a short delay
setTimeout(() => {
privateChats[sender].push({
sender: sender,
content: `Thanks for your message: "${content}"`,
timestamp: new Date().toISOString()
});
if (activeChatWindows[sender] && activeChatWindows[sender].style.display === 'block') {
renderPrivateMessages(sender);
}
}, 1000);
renderPrivateMessages(sender);
textarea.value = '';
}
}
// Initialize // Initialize
populateVideoSources(); populateVideoSources();
renderMessages(fakeMessages); renderMessages(fakeMessages);
renderUserList(); renderUserList();
renderEarnings(); renderEarnings();
updateStatus(); updateStatus();
// Create a test private chat for demonstration
const testSender = "alice@C4.sexhackme";
privateChats[testSender] = [
{
sender: 'me',
content: 'Hello Alice!',
timestamp: new Date().toISOString()
},
{
sender: testSender,
content: 'Hi there! How can I help you?',
timestamp: new Date().toISOString()
}
];
openPrivateChats.add(testSender);
renderPrivateChatslist();
// Handle window resize to keep chat windows within viewport
window.addEventListener('resize', () => {
for (const sender in activeChatWindows) {
const chatWindow = activeChatWindows[sender];
if (chatWindow) {
// Keep window within viewport bounds
const maxX = window.innerWidth - chatWindow.offsetWidth;
const maxY = window.innerHeight - chatWindow.offsetHeight;
const currentX = parseInt(chatWindow.style.left) || 0;
const currentY = parseInt(chatWindow.style.top) || 0;
chatWindow.style.left = `${Math.max(0, Math.min(currentX, maxX))}px`;
chatWindow.style.top = `${Math.max(0, Math.min(currentY, maxY))}px`;
}
}
});
</script> </script>
</body> </body>
</html> </html>
# -*- coding: utf-8 -*-
from playwright.async_api import async_playwright, Browser, BrowserContext
import json
import shutil
import errno
from pathlib import Path
import asyncio
import uuid
from typing import List, Optional
# Base directories for storing profile data and storage states (cross-platform)
BASE_PROFILE_DIR = Path("chromium_profiles") # Directory for user data/profile directories
BASE_STATE_DIR = Path("browser_states") # Directory for storage state JSON files
def copytree_ignore_errors(src: Path, dst: Path, ignore_patterns: list = ["SingletonLock", "SingletonCookie", "SingletonSocket"]):
"""Copy a directory tree, ignoring specified files and handling errors gracefully."""
# Define a function to ignore files matching specified patterns (e.g., Singleton* files)
def ignore_files(directory, files):
return [f for f in files if any(pattern in f for pattern in ignore_patterns)]
try:
# Copy the directory tree, ignoring specified files
shutil.copytree(src, dst, ignore=ignore_files, dirs_exist_ok=False)
except shutil.Error as e:
# Log non-critical copy errors (e.g., permission issues)
print(f"Non-critical errors during copy: {e}")
except OSError as e:
# Handle specific OS errors (e.g., file not found, device busy) gracefully
if e.errno not in (errno.ENOENT, errno.EAGAIN, errno.EBUSY):
raise
print(f"Skipped problematic file during copy: {e}")
class SimulatedBrowser:
"""Simulates a Playwright Browser object using either launch_persistent_context or browser.launch."""
def __init__(self, playwright, base_profile_dir: Path, use_persistent_context: bool, extension_paths: List[Path] = None):
"""Initialize the simulated browser."""
# Store Playwright instance for browser operations
self._playwright = playwright
# Base directory for profile data
self._base_profile_dir = base_profile_dir
# Flag to choose between launch_persistent_context and browser.launch
self._use_persistent_context = use_persistent_context
# List of extension paths to copy to each context (converted to Path objects)
self._extension_paths = [Path(p) for p in (extension_paths or [])]
# Dictionary to store context_id -> BrowserContext
self._contexts = {}
# Browser instance for browser.launch mode
self._browser = None
# Flag to track if the browser is closed
self._is_closed = False
async def launch(self):
"""Simulate browser.launch() by preparing the environment."""
# Create base profile directory if it doesn't exist
self._base_profile_dir.mkdir(parents=True, exist_ok=True)
# If using browser.launch mode, launch a single Chromium instance
if not self._use_persistent_context:
self._browser = await self._playwright.chromium.launch(headless=False)
print("Simulated browser launched.")
return self
async def new_context(self, context_id: str = None, **kwargs) -> BrowserContext:
"""Create a new context with its own persistent state."""
# Check if the browser is closed
if self._is_closed:
raise RuntimeError("Browser is closed.")
# Generate a unique context ID if not provided
context_id = context_id or str(uuid.uuid4())
# Create or restore profile directory for this context
profile_dir = self._get_profile_dir(context_id)
if not profile_dir.exists():
restore_profile_dir(context_id, self._base_profile_dir)
create_profile_dir(context_id, self._base_profile_dir)
# Create context based on mode
if self._use_persistent_context:
# Use launch_persistent_context for native persistence via user_data_dir
context = await self._playwright.chromium.launch_persistent_context(
user_data_dir=profile_dir,
headless=False,
viewport=kwargs.get("viewport", {"width": 1280, "height": 720}),
**{k: v for k, v in kwargs.items() if k != "extension_paths"} # Filter out extension_paths
)
else:
# Use browser.launch with manual persistence
if not self._browser:
raise RuntimeError("Browser not launched.")
context = await self._browser.new_context(
viewport=kwargs.get("viewport", {"width": 1280, "height": 720}),
**{k: v for k, v in kwargs.items() if k != "extension_paths"}
)
# Load storage state manually if it exists
state_file = BASE_STATE_DIR / f"browser_state_{context_id}.json"
if state_file.exists():
with open(state_file, 'r', encoding='utf-8') as f:
storage_state = json.load(f)
# Load cookies from storage state
if "cookies" in storage_state:
await context.add_cookies(storage_state["cookies"])
# Load local storage from storage state
if "origins" in storage_state:
for origin in storage_state["origins"]:
for item in origin.get("localStorage", []):
await context.evaluate(
"""
({name, value}) => {
window.localStorage.setItem(name, value);
}
""",
{"name": item["name"], "value": item["value"]}
)
print(f"Loaded browser state for context {context_id} from {state_file}")
# Store the context
self._contexts[context_id] = context
print(f"Created new context with ID {context_id} using profile {profile_dir}")
# Install pre-set extensions for this context
for ext_path in self._extension_paths:
await install_extension(context, ext_path, context_id)
return context
async def close(self):
"""Close all contexts and mark the browser as closed."""
if not self._is_closed:
for context_id, context in self._contexts.items():
# Save storage state before closing
await save_browser_state(context, context_id)
# Close context to release Singleton* files
await context.close()
# Backup profile directory after closing (only for persistent context mode)
if self._use_persistent_context:
backup_profile_dir(context_id, self._base_profile_dir)
# Close the browser if in browser.launch mode
if self._browser:
await self._browser.close()
self._contexts.clear()
self._is_closed = True
print("Simulated browser closed.")
def _get_profile_dir(self, context_id: str) -> Path:
"""Get the profile directory for a context."""
return self._base_profile_dir / f"chromium_profile_{context_id}"
async def save_browser_state(context: BrowserContext, context_id: str):
"""Save the browser's storage state for a specific context."""
state_file = BASE_STATE_DIR / f"browser_state_{context_id}.json"
await context.storage_state(path=state_file)
print(f"Saved browser state for context {context_id} to {state_file}")
def create_profile_dir(context_id: str, base_profile_dir: Path):
"""Create a profile directory for a specific context if it doesn't exist."""
profile_dir = base_profile_dir / f"chromium_profile_{context_id}"
profile_dir.mkdir(parents=True, exist_ok=True)
print(f"Created profile directory for context {context_id} at {profile_dir}")
def backup_profile_dir(context_id: str, base_profile_dir: Path):
"""Copy the profile directory for a specific context to a backup location."""
profile_dir = base_profile_dir / f"chromium_profile_{context_id}"
backup_dir = base_profile_dir / f"chromium_profile_backup_{context_id}"
if profile_dir.exists():
if backup_dir.exists():
shutil.rmtree(backup_dir) # Remove old backup
copytree_ignore_errors(profile_dir, backup_dir)
print(f"Backed up profile directory for context {context_id} to {backup_dir}")
def restore_profile_dir(context_id: str, base_profile_dir: Path):
"""Restore the profile directory for a specific context from the backup."""
profile_dir = base_profile_dir / f"chromium_profile_{context_id}"
backup_dir = base_profile_dir / f"chromium_profile_backup_{context_id}"
if backup_dir.exists():
if profile_dir.exists():
shutil.rmtree(profile_dir) # Remove current profile dir
copytree_ignore_errors(backup_dir, profile_dir)
print(f"Restored profile directory for context {context_id} from {backup_dir}")
else:
print(f"No backup profile directory found for context {context_id}.")
async def install_extension(context: BrowserContext, extension_path: Path, context_id: str):
"""Load an extension for a specific context."""
extension_path = Path(extension_path)
profile_dir = BASE_PROFILE_DIR / f"chromium_profile_{context_id}"
if extension_path.exists():
# Copy extension to the user data directory's Extensions folder
extension_dest = profile_dir / "Extensions" / extension_path.name
if not extension_dest.exists():
shutil.copytree(extension_path, extension_dest)
print(f"Copied extension to {extension_dest} for context {context_id}")
# Add a script to simulate extension interaction (optional)
await context.add_init_script(
script="""chrome.runtime.sendMessage({ message: 'Extension loaded' });"""
)
print(f"Extension loaded from {extension_path} for context {context_id}")
else:
print(f"Extension path {extension_path} does not exist for context {context_id}.")
async def run_context(simulated_browser: SimulatedBrowser, context_id: str, extension_paths: List[Path]):
"""Run a single browser context with its own persistent state."""
# Create a new context with the pre-set extensions
context = await simulated_browser.new_context(
context_id=context_id,
extension_paths=extension_paths
)
# Open a new page in the context
page = await context.new_page()
# Navigate to a sample website
await page.goto("https://example.com")
# Print the page title as an example action
print(f"Page title for context {context_id}: {await page.title()}")
async def run(playwright, num_contexts=3, use_persistent_context=True):
"""Run multiple browser contexts with persistent states."""
# Ensure base directories exist
BASE_PROFILE_DIR.mkdir(parents=True, exist_ok=True)
BASE_STATE_DIR.mkdir(parents=True, exist_ok=True)
# Define pre-set extensions to be copied to all contexts
EXTENSION_PATHS = [
Path("./my_extension1"), # Example: use forward slashes for cross-platform compatibility
Path("./my_extension2") # Replace with actual paths to unpacked extension directories
]
# Create simulated browser with the chosen mode and extensions
simulated_browser = SimulatedBrowser(playwright, BASE_PROFILE_DIR, use_persistent_context, EXTENSION_PATHS)
await simulated_browser.launch()
# Create tasks for multiple contexts
tasks = []
for _ in range(num_contexts):
context_id = str(uuid.uuid4()) # Unique ID for each context
tasks.append(run_context(simulated_browser, context_id, EXTENSION_PATHS))
# Run all contexts concurrently
await asyncio.gather(*tasks)
while True:
asyncio.sleep(0.1)
# Close the simulated browser, saving states and backing up profiles
await simulated_browser.close()
async def main():
"""Main entry point to test both modes."""
"""
HOW TO USE
----------
This script provides a unified solution for managing multiple Playwright browser contexts with persistent states (session data, extensions, cache) using two approaches:
1. launch_persistent_context mode: Uses Playwright's launch_persistent_context() for native persistence via user data directories.
2. browser.launch mode: Uses Playwright's browser.launch() with manual persistence via storage state files and profile directories.
Prerequisites:
- Install Playwright: Run "pip install playwright" in your terminal.
- Install Chromium: Run "playwright install chromium" to download the Chromium browser used by Playwright.
- Ensure Python 3.7+ is installed to support async/await syntax.
Setting Up Extensions:
- Update the EXTENSION_PATHS list in the run() function with paths to unpacked extension directories, each containing a manifest.json file.
- Example:
EXTENSION_PATHS = [
Path("./my_extension1"),
Path("./my_extension2")
]
- To obtain unpacked extensions:
- On Linux/macOS: Extract a .crx file using "unzip <extension>.crx".
- On Windows: Use 7-Zip to extract a .crx file (it is a ZIP archive).
- Alternatively, pre-install extensions in a Chromium browser and copy its profile directory (e.g., ~/.config/chromium on Linux, ~/Library/Application Support/Chromium on macOS, %LocalAppData%\\Chromium\\User Data on Windows) to chromium_profile_<context_id> for each context.
- Note: Extensions load natively in launch_persistent_context mode but may not load dynamically in browser.launch mode without additional setup (e.g., automating chrome://extensions/).
Running the Script:
- Save the script as script.py.
- Run it from the terminal: "python script.py".
- The script will:
- Create chromium_profiles and browser_states directories in the script's working directory.
- Run two tests:
1. launch_persistent_context mode: Creates 3 contexts, each with a unique user data directory (chromium_profile_<context_id>) and storage state file (browser_state_<context_id>.json).
2. browser.launch mode: Creates 3 contexts using a single browser instance, with manual persistence.
- For each context:
- Copies all extensions from EXTENSION_PATHS to the context's profile directory (chromium_profile_<context_id>/Extensions).
- Loads session data (cookies, local storage) from browser_state_<context_id>.json if it exists.
- Navigates to https://example.com and prints the page title as a sample action.
- Saves session data and backs up the profile directory (in launch_persistent_context mode) when closing.
- Output will include logs for directory creation, extension copying, state loading/saving, and page titles.
Subsequent Runs:
- The script restores profile directories and session states for each context using unique context IDs (UUIDs), ensuring persistence across runs.
- Backed-up profile directories (chromium_profile_backup_<context_id>) are used to restore chromium_profile_<context_id> if needed.
- Session data persists in browser_state_<context_id>.json files.
Customization:
- Change the number of contexts by modifying num_contexts in run().
- Switch modes by setting use_persistent_context=True (launch_persistent_context) or False (browser.launch) in run().
- Update EXTENSION_PATHS with your extension directories.
- Add context options (e.g., user_agent, locale) by passing them to new_context(**kwargs) in run_context().
DEBUGGING TIPS
--------------
If you encounter issues while running the script, consider the following debugging strategies:
Extension Not Loading:
- Verify each path in EXTENSION_PATHS points to a valid unpacked extension directory containing a manifest.json file.
- Check the chromium_profile_<context_id>/Extensions directory to ensure extensions were copied correctly.
- In launch_persistent_context mode:
- Open the browser (since headless=False) and navigate to chrome://extensions/ to confirm extensions are loaded.
- Ensure extensions are compatible with Chromium and have a valid manifest.
- In browser.launch mode:
- Extensions may not load dynamically due to Playwright's limitations.
- To enable extensions, pre-install them in a Chromium browser and copy the profile to chromium_profile_<context_id>.
- Alternatively, automate extension installation by navigating to chrome://extensions/ and enabling developer mode (not implemented in this script).
- If extensions fail to load, check console logs for errors related to install_extension().
Session Data Issues:
- Ensure browser_state_<context_id>.json files in the browser_states directory contain valid JSON.
- Open these files to verify cookies and local storage data are correctly formatted.
- Check console output for messages like "Loaded browser state..." or "Saved browser state..." to confirm state handling.
- If session data does not persist, verify write permissions for the browser_states directory.
- Test by logging into a website in one run and checking if the login persists in the next run.
Backup Errors:
- The copytree_ignore_errors function prevents errors related to SingletonLock, SingletonCookie, and SingletonSocket files by ignoring them during backups.
- If backup issues persist, check console logs for messages like "Skipped problematic file..." or "Non-critical errors during copy...".
- Ensure no other processes (e.g., lingering Chromium instances) are accessing chromium_profile_<context_id> directories.
- Verify write permissions for the chromium_profiles directory.
- If backups fail, manually inspect chromium_profile_backup_<context_id> to ensure critical files (e.g., extensions, cache) were copied.
Indentation Errors:
- The code uses consistent 4-space indentation per PEP 8. If syntax errors occur, ensure no tabs are mixed with spaces.
- Copy the code directly to avoid indentation issues introduced by text editors or copy-paste operations.
- Use an editor with Python linting (e.g., VS Code with Pylance) to detect indentation problems.
Resource Usage:
- In launch_persistent_context mode, each context runs a separate Chromium instance, increasing memory and CPU usage. Monitor system resources if using many contexts.
- In browser.launch mode, a single browser instance is used, but extension/cache persistence is limited.
- If performance is an issue, reduce num_contexts or switch to browser.launch mode.
Other Errors:
- If you encounter runtime errors (e.g., network issues, Playwright exceptions), check the full traceback printed by the script.
- Share the error message and traceback for specific debugging assistance.
- Common issues include:
- Network errors: Ensure internet connectivity and that https://example.com is accessible.
- Playwright errors: Verify Playwright and Chromium are installed correctly.
- File permission errors: Run the script with appropriate permissions (e.g., as administrator on Windows if needed).
- To isolate issues, try running with num_contexts=1 and a single extension.
Logging:
- The script includes verbose logging for key actions (e.g., directory creation, extension copying, state loading/saving).
- Review console output to trace execution and identify where errors occur.
- Add custom print statements in functions like new_context() or install_extension() for deeper debugging.
Testing:
- Test with a small number of contexts (e.g., num_contexts=1) to verify basic functionality.
- Test each mode separately by commenting out one call in main().
- Test with and without extensions to isolate extension-related issues.
- Run on different platforms (Linux, macOS, Windows) to confirm cross-platform compatibility.
"""
async with async_playwright() as playwright:
try:
# Test with launch_persistent_context mode
print("Running with launch_persistent_context...")
await run(playwright, num_contexts=3, use_persistent_context=True)
# Test with browser.launch mode
print("\nRunning with browser.launch...")
await run(playwright, num_contexts=3, use_persistent_context=False)
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
asyncio.run(main())
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