Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
S
SHMCamStudio
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
SexHackMe
SHMCamStudio
Commits
ee7ebecd
Commit
ee7ebecd
authored
Jun 23, 2025
by
nextime
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Client base web interface ready
parent
388b43de
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
1465 additions
and
139 deletions
+1465
-139
webpanel.py
shmcs/webpanel.py
+417
-9
chat.html
templates/chat.html
+836
-130
panel.html
templates/panel.html
+212
-0
No files found.
shmcs/webpanel.py
View file @
ee7ebecd
...
...
@@ -13,10 +13,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from
flask
import
Flask
,
render_template
,
request
,
session
from
flask
import
Flask
,
render_template
,
request
,
session
,
jsonify
from
utils
import
check_port_available
,
run_command
,
create_daemon
,
run_action
import
sys
import
os
import
json
import
time
import
threading
from
datetime
import
datetime
from
guiutils
import
get_buttons
import
flask_restful
as
restful
...
...
@@ -97,8 +101,7 @@ class AppData(restful.Resource):
return
{
'content'
:
content
}
@
flask_app
.
route
(
'/'
)
def
index
():
def
prepare_panel_page
():
#buttons, numrows = get_buttons()
row
=
1
style_rows
=
""
...
...
@@ -107,7 +110,7 @@ def index():
while
row
<=
numrows
:
bspan
=
int
(
120
/
len
(
buttons
[
row
]
.
keys
()))
style_rows
=
style_rows
+
"
\n
.button_row"
+
str
(
row
)
+
" {
\n
grid-column: span "
+
str
(
bspan
)
+
";
\n
}
\n
"
col
=
0
htmlbuttons
=
htmlbuttons
+
"<div class='button_row'>"
for
b
in
buttons
[
row
]
.
keys
():
...
...
@@ -123,14 +126,23 @@ def index():
htmlbuttons
=
htmlbuttons
+
"""<button style="color:white;background-color:"""
+
color
+
""";"
class="button private button_row"""
+
str
(
row
)
+
" "
+
pollclass
+
""" " onclick="executeCommand('"""
+
command
+
"""')">
"""
+
buttons
[
row
][
b
][
'title'
]
+
"""
</button>"""
</button>"""
htmlbuttons
=
htmlbuttons
+
"</div>"
row
=
row
+
1
return
style_rows
,
htmlbuttons
@
flask_app
.
route
(
'/'
)
def
index
():
style_rows
,
htmlbuttons
=
prepare_panel_page
()
return
render_template
(
'index.html'
,
style_rows
=
style_rows
,
htmlbuttons
=
htmlbuttons
)
@
flask_app
.
route
(
'/panel'
)
def
panel
():
style_rows
,
htmlbuttons
=
prepare_panel_page
()
return
render_template
(
'panel.html'
,
style_rows
=
style_rows
,
htmlbuttons
=
htmlbuttons
)
@
flask_app
.
route
(
'/execute'
,
methods
=
[
'POST'
])
def
execute
():
command_key
=
request
.
form
.
get
(
'command'
)
...
...
@@ -147,21 +159,392 @@ def stream():
return
render_template
(
'stream.html'
,
stream_url
=
stream_url
)
# Fake data for the chat interface
fake_messages
=
[
{
"sender"
:
"me"
,
"content"
:
"Hello! 😊"
,
"timestamp"
:
"2025-06-21 20:00"
},
{
"sender"
:
"alice@C4.sexhackme"
,
"content"
:
"Hi! Check this: https://example.com"
,
"timestamp"
:
"2025-06-21 20:01"
},
{
"sender"
:
"bob@SC.spora"
,
"content"
:
"<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>"
,
"timestamp"
:
"2025-06-21 20:02"
},
{
"sender"
:
"system"
,
"content"
:
"system: *** Server maintenance scheduled at 1 AM"
,
"type"
:
"notify-system"
,
"timestamp"
:
"2025-06-21 20:03"
},
{
"sender"
:
"platform@SC.spora"
,
"content"
:
"platform: *** User joined the room"
,
"type"
:
"notify-platform"
,
"timestamp"
:
"2025-06-21 20:04"
},
{
"sender"
:
"bob@SC.spora"
,
"content"
:
"<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>"
,
"type"
:
"tip"
,
"timestamp"
:
"2025-06-21 20:05"
}
]
fake_users
=
{
"C4.sexhackme"
:
[
{
"username"
:
"alice"
,
"status"
:
"online"
,
"tokens"
:
50
},
{
"username"
:
"charlie"
,
"status"
:
"offline"
,
"tokens"
:
20
}
],
"SC.spora"
:
[
{
"username"
:
"bob"
,
"status"
:
"online"
,
"tokens"
:
30
}
]
}
fake_earnings
=
[
{
"platform"
:
"C4.sexhackme"
,
"lastSession"
:
100
,
"today"
:
250
,
"lastHour"
:
50
,
"sess"
:
100
},
{
"platform"
:
"SC.spora"
,
"lastSession"
:
80
,
"today"
:
200
,
"lastHour"
:
30
,
"sess"
:
80
}
]
fake_status
=
{
"C4.sexhackme"
:
"online"
,
"SC.spora"
:
"offline"
}
fake_rtsp_urls
=
[
{
"id"
:
"rtsp1"
,
"url"
:
"rtsp://example.com/stream1"
},
{
"id"
:
"rtsp2"
,
"url"
:
"rtsp://example.com/stream2"
}
]
# Test private chat data
test_private_chat
=
{
"alice@C4.sexhackme"
:
[
{
"sender"
:
"me"
,
"content"
:
"Hello Alice!"
,
"timestamp"
:
datetime
.
now
()
.
isoformat
()
},
{
"sender"
:
"alice@C4.sexhackme"
,
"content"
:
"Hi there! How can I help you?"
,
"timestamp"
:
datetime
.
now
()
.
isoformat
()
}
]
}
# Track unread messages in private chats
unread_private_messages
=
{
"alice@C4.sexhackme"
:
1
# Number of unread messages
}
# Store the last update timestamp for each data type
last_updates
=
{
"messages"
:
time
.
time
(),
"users"
:
time
.
time
(),
"earnings"
:
time
.
time
(),
"status"
:
time
.
time
(),
"rtsp_urls"
:
time
.
time
(),
"private_chats"
:
time
.
time
()
}
# Store the last request timestamp for each client
client_last_request
=
{}
@
flask_app
.
route
(
"/chat"
)
def
chat
():
return
render_template
(
'chat.html'
)
@
flask_app
.
route
(
"/api/chat"
)
def
get_chat_data
():
"""
Consolidated API endpoint for chat data with non-blocking long polling.
This implements a hybrid approach:
1. SocketIO for real-time push notifications when data changes
2. Long polling as a fallback mechanism with non-blocking behavior
The client will receive immediate updates via SocketIO when available,
and will fall back to polling if SocketIO is not working.
"""
client_id
=
request
.
args
.
get
(
'client_id'
,
str
(
time
.
time
()))
# Initialize client's last request time if it's a new client
if
client_id
not
in
client_last_request
:
# New client - set initial timestamp to 0 to ensure they get data
client_last_request
[
client_id
]
=
0
# Get the stored last request time for this client
client_last_time
=
client_last_request
[
client_id
]
# Check if any data has been updated since the client's last request
def
is_data_updated
():
# For new clients (last_time=0), always return true
if
client_last_time
==
0
:
return
True
# For existing clients, check if any data has been updated
return
(
last_updates
[
"messages"
]
>
client_last_time
or
last_updates
[
"users"
]
>
client_last_time
or
last_updates
[
"earnings"
]
>
client_last_time
or
last_updates
[
"status"
]
>
client_last_time
or
last_updates
[
"rtsp_urls"
]
>
client_last_time
or
last_updates
[
"private_chats"
]
>
client_last_time
)
# If data is already updated or this is a new client, return immediately
if
is_data_updated
():
current_time
=
time
.
time
()
# Update the client's last request time
client_last_request
[
client_id
]
=
current_time
# Log the data update for debugging
logging
.
info
(
f
"Sending updated data to client {client_id}"
)
return
jsonify
({
"messages"
:
fake_messages
,
"users"
:
fake_users
,
"earnings"
:
fake_earnings
,
"status"
:
fake_status
,
"rtsp_urls"
:
fake_rtsp_urls
,
"private_chats"
:
test_private_chat
,
"unread_private_messages"
:
unread_private_messages
,
"timestamp"
:
current_time
})
# For long polling, return a response that will be processed by the client
# This avoids blocking the server thread
return
jsonify
({
"no_update"
:
True
,
"retry_after"
:
1000
,
# Retry after 1 second
"timestamp"
:
time
.
time
()
})
# Keep these routes for backward compatibility if needed
@
flask_app
.
route
(
"/api/messages"
)
def
get_messages
():
last_updates
[
"messages"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'messages'
})
return
jsonify
(
fake_messages
)
@
flask_app
.
route
(
"/api/users"
)
def
get_users
():
last_updates
[
"users"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'users'
})
return
jsonify
(
fake_users
)
@
flask_app
.
route
(
"/api/earnings"
)
def
get_earnings
():
last_updates
[
"earnings"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'earnings'
})
return
jsonify
(
fake_earnings
)
@
flask_app
.
route
(
"/api/status"
)
def
get_status
():
last_updates
[
"status"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'status'
})
return
jsonify
(
fake_status
)
@
flask_app
.
route
(
"/api/rtsp_urls"
)
def
get_rtsp_urls
():
last_updates
[
"rtsp_urls"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'rtsp_urls'
})
return
jsonify
(
fake_rtsp_urls
)
@
flask_app
.
route
(
"/api/private_chats"
)
def
get_private_chats
():
last_updates
[
"private_chats"
]
=
time
.
time
()
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'private_chats'
})
return
jsonify
(
test_private_chat
)
@
flask_app
.
route
(
"/api/send_message"
,
methods
=
[
"POST"
])
def
send_message
():
data
=
request
.
json
if
not
data
or
"content"
not
in
data
:
return
jsonify
({
"error"
:
"Invalid message data"
}),
400
# Get client_id from request if available
client_id
=
request
.
args
.
get
(
'client_id'
,
None
)
new_message
=
{
"sender"
:
"me"
,
"content"
:
data
[
"content"
],
"timestamp"
:
datetime
.
now
()
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M"
)
}
# Add to fake messages
fake_messages
.
append
(
new_message
)
# Update the last update timestamp for messages
current_time
=
time
.
time
()
last_updates
[
"messages"
]
=
current_time
# Reset all client timestamps to force data refresh on next poll
# But set them to slightly before current time to avoid full refresh
for
cid
in
client_last_request
:
# Don't reset the current client's timestamp if we know who it is
if
client_id
and
cid
==
client_id
:
continue
# Set to a value that will trigger an update but not to 0
client_last_request
[
cid
]
=
current_time
-
1
# Notify clients about the update
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'messages'
})
return
jsonify
({
"success"
:
True
,
"message"
:
new_message
})
@
flask_app
.
route
(
"/api/send_private_message"
,
methods
=
[
"POST"
])
def
send_private_message
():
data
=
request
.
json
if
not
data
or
"content"
not
in
data
or
"recipient"
not
in
data
:
return
jsonify
({
"error"
:
"Invalid message data"
}),
400
# Get client_id from request if available
client_id
=
request
.
args
.
get
(
'client_id'
,
None
)
recipient
=
data
[
"recipient"
]
content
=
data
[
"content"
]
# Create new message
new_message
=
{
"sender"
:
"me"
,
"content"
:
content
,
"timestamp"
:
datetime
.
now
()
.
isoformat
()
}
# Add to private chat
if
recipient
not
in
test_private_chat
:
test_private_chat
[
recipient
]
=
[]
test_private_chat
[
recipient
]
.
append
(
new_message
)
# Simulate response
response_message
=
{
"sender"
:
recipient
,
"content"
:
f
"Thanks for your message:
\"
{content}
\"
"
,
"timestamp"
:
datetime
.
now
()
.
isoformat
()
}
test_private_chat
[
recipient
]
.
append
(
response_message
)
# Increment unread message count for the recipient's response
if
recipient
not
in
unread_private_messages
:
unread_private_messages
[
recipient
]
=
0
unread_private_messages
[
recipient
]
+=
1
# Update the last update timestamp for private chats
current_time
=
time
.
time
()
last_updates
[
"private_chats"
]
=
current_time
# Reset all client timestamps to force data refresh on next poll
for
cid
in
client_last_request
:
# Don't reset the current client's timestamp if we know who it is
if
client_id
and
cid
==
client_id
:
continue
# Set to a value that will trigger an update but not to 0
client_last_request
[
cid
]
=
current_time
-
1
# Notify clients about the update with additional data
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'private_chats'
,
'sender'
:
recipient
,
'has_unread'
:
True
})
return
jsonify
({
"success"
:
True
,
"messages"
:
test_private_chat
[
recipient
],
"unread_count"
:
unread_private_messages
[
recipient
]
})
@
flask_app
.
route
(
"/api/mark_messages_read"
,
methods
=
[
"POST"
])
def
mark_messages_read
():
data
=
request
.
json
if
not
data
or
"sender"
not
in
data
:
return
jsonify
({
"error"
:
"Invalid data"
}),
400
# Get client_id from request if available
client_id
=
request
.
args
.
get
(
'client_id'
,
None
)
sender
=
data
[
"sender"
]
# Reset unread count for this sender
if
sender
in
unread_private_messages
:
unread_private_messages
[
sender
]
=
0
# Update the last update timestamp for private chats
current_time
=
time
.
time
()
last_updates
[
"private_chats"
]
=
current_time
# Reset all client timestamps to force data refresh on next poll
for
cid
in
client_last_request
:
# Don't reset the current client's timestamp if we know who it is
if
client_id
and
cid
==
client_id
:
continue
# Set to a value that will trigger an update but not to 0
client_last_request
[
cid
]
=
current_time
-
1
# Notify clients about the update with specific information
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'private_chats'
,
'sender'
:
sender
,
'has_unread'
:
False
})
return
jsonify
({
"success"
:
True
,
"sender"
:
sender
,
"unread_count"
:
0
})
@
flask_app
.
route
(
"/api/reset_session"
,
methods
=
[
"POST"
])
def
reset_session
():
# Reset session earnings
for
earning
in
fake_earnings
:
earning
[
"sess"
]
=
0
# Update the last update timestamp for earnings
last_updates
[
"earnings"
]
=
time
.
time
()
# Notify clients about the update
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'earnings'
})
return
jsonify
({
"success"
:
True
})
@
flask_app
.
route
(
"/api/update_status"
,
methods
=
[
"POST"
])
def
update_status
():
data
=
request
.
json
if
not
data
:
return
jsonify
({
"error"
:
"Invalid status data"
}),
400
# Update all platforms
if
"all"
in
data
and
data
[
"all"
]
in
[
"online"
,
"offline"
]:
new_status
=
data
[
"all"
]
for
platform
in
fake_status
:
fake_status
[
platform
]
=
new_status
# Update specific platform
elif
"platform"
in
data
and
"status"
in
data
:
platform
=
data
[
"platform"
]
new_status
=
data
[
"status"
]
if
platform
in
fake_status
:
fake_status
[
platform
]
=
new_status
# Update the last update timestamp for status
last_updates
[
"status"
]
=
time
.
time
()
# Notify clients about the update
socketio
.
emit
(
'chat_update'
,
{
'type'
:
'status'
})
return
jsonify
({
"success"
:
True
,
"status"
:
fake_status
})
@
socketio
.
event
def
my_event
(
message
):
session
[
'receive_count'
]
=
session
.
get
(
'receive_count'
,
0
)
+
1
emit
(
'my_response'
,
{
'data'
:
message
[
'data'
],
'count'
:
session
[
'receive_count'
]})
@
socketio
.
event
def
my_ping
():
emit
(
'my_pong'
)
@
socketio
.
event
def
connect
():
"""Handle client connection"""
client_id
=
request
.
sid
logging
.
info
(
f
"Client connected: {client_id}"
)
# Initialize new client with timestamp 0 to ensure they get all data
client_last_request
[
client_id
]
=
0
# Emit an immediate update event to trigger data fetch
emit
(
'chat_update'
,
{
'type'
:
'initial_connect'
})
@
socketio
.
event
def
disconnect
():
"""Handle client disconnection"""
client_id
=
request
.
sid
logging
.
info
(
f
"Client disconnected: {client_id}"
)
if
client_id
in
client_last_request
:
del
client_last_request
[
client_id
]
@
socketio
.
event
def
get_queue
():
...
...
@@ -179,7 +562,7 @@ def get_queue():
logging
.
info
(
'CHANGE THE COLOR OF THE WEB BUTTON FOR '
+
str
(
data
[
'output'
]))
bcfg
=
outputs
[
data
[
'output'
]][
'cfg'
]
if
'color.'
+
str
(
data
[
'status'
])
in
bcfg
.
keys
():
emit
(
'change_output'
,
{
'button'
:
data
[
'output'
],
'color'
:
bcfg
[
'color.'
+
str
(
data
[
'status'
])]
},
broadcast
=
True
)
emit
(
'change_output'
,
{
'button'
:
data
[
'output'
],
'color'
:
bcfg
[
'color.'
+
str
(
data
[
'status'
])]
},
broadcast
=
True
)
if
'title.'
+
str
(
data
[
'status'
])
in
bcfg
.
keys
():
emit
(
'change_output'
,
{
'button'
:
data
[
'output'
],
'title'
:
bcfg
[
'title.'
+
str
(
data
[
'status'
])]
},
broadcast
=
True
)
if
event
==
'INPUTSTATUSCHANGE'
:
...
...
@@ -202,6 +585,27 @@ def get_queue():
# Function to clean up stale client entries
def
cleanup_stale_clients
():
"""Remove clients that haven't made a request in the last 10 minutes"""
while
True
:
try
:
current_time
=
time
.
time
()
stale_threshold
=
current_time
-
600
# 10 minutes
# Create a copy of the keys to avoid modifying during iteration
client_ids
=
list
(
client_last_request
.
keys
())
for
client_id
in
client_ids
:
if
client_last_request
[
client_id
]
<
stale_threshold
:
logging
.
info
(
f
"Removing stale client: {client_id}"
)
del
client_last_request
[
client_id
]
except
Exception
as
e
:
logging
.
error
(
f
"Error in cleanup thread: {e}"
)
# Sleep for 5 minutes before next cleanup
time
.
sleep
(
300
)
def
run_flask_app
(
port
=
5000
,
daemon_mode
=
False
):
"""Run Flask app with optional daemon mode"""
if
not
check_port_available
(
port
):
...
...
@@ -213,6 +617,10 @@ def run_flask_app(port=5000, daemon_mode=False):
if
daemon_mode
and
sys
.
platform
!=
'win32'
:
create_daemon
()
# Start the cleanup thread
cleanup_thread
=
threading
.
Thread
(
target
=
cleanup_stale_clients
,
daemon
=
True
)
cleanup_thread
.
start
()
flask_api
.
add_resource
(
PollAPI
,
'/update'
)
flask_api
.
add_resource
(
AppData
,
'/data'
)
#flask_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
...
...
templates/chat.html
View file @
ee7ebecd
...
...
@@ -48,6 +48,29 @@
.top-bar
a
:hover
{
text-decoration
:
underline
;
}
/* Auto-open toggle in top bar */
.auto-open-toggle
{
display
:
flex
;
align-items
:
center
;
margin-right
:
15px
;
background
:
rgba
(
0
,
0
,
0
,
0.2
);
padding
:
5px
10px
;
border-radius
:
20px
;
}
.auto-open-toggle
label
{
display
:
flex
;
align-items
:
center
;
cursor
:
pointer
;
font-size
:
14px
;
color
:
#fff
;
}
.auto-open-toggle
input
[
type
=
"checkbox"
]
{
margin-right
:
8px
;
cursor
:
pointer
;
}
/* Desktop layout: 3 columns */
.container
{
...
...
@@ -101,6 +124,17 @@
/* Video window */
.video-container
{
position
:
relative
;
.tab[data-tab="session"]
{
background
:
#2a2a2a
;
border-radius
:
8px
8px
0
0
;
margin-right
:
5px
;
font-size
:
14px
;
white-space
:
nowrap
;
}
.tab
[
data-tab
=
"session"
]
.active
{
background
:
#3b82f6
;
}
background
:
#000
;
border-radius
:
8px
;
overflow
:
hidden
;
...
...
@@ -152,17 +186,22 @@
}
/* Online/offline button */
.status-button-container
{
position
:
relative
;
display
:
flex
;
align-items
:
center
;
margin
:
10px
0
;
flex-shrink
:
0
;
}
.status-button
{
width
:
100%
;
flex
:
1
;
padding
:
12px
;
margin
:
10px
0
;
font-size
:
18px
;
font-weight
:
600
;
border
:
none
;
border-radius
:
20px
;
border-radius
:
20px
0
0
20px
;
cursor
:
pointer
;
text-align
:
center
;
flex-shrink
:
0
;
}
.status-button.online
{
background
:
#dc3545
;
...
...
@@ -170,28 +209,67 @@
.status-button.offline
{
background
:
#28a745
;
}
.status-toggle
{
width
:
40px
;
height
:
100%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
0
,
0
,
0
,
0.2
);
border
:
none
;
border-radius
:
0
20px
20px
0
;
cursor
:
pointer
;
font-size
:
16px
;
color
:
#fff
;
}
.status-toggle
:hover
{
background
:
rgba
(
0
,
0
,
0
,
0.3
);
}
.status-menu
{
display
:
none
;
position
:
absolute
;
top
:
100%
;
right
:
0
;
width
:
300px
;
background
:
#2a2a2a
;
border
:
1px
solid
#3b82f6
;
padding
:
10px
;
z-index
:
100
;
border-radius
:
8px
;
margin-top
:
5px
;
box-shadow
:
0
4px
8px
rgba
(
0
,
0
,
0
,
0.2
);
}
.status-menu
button
{
display
:
block
;
display
:
flex
;
justify-content
:
space-between
;
width
:
100%
;
padding
:
8px
;
background
:
none
;
background
:
#135f2a8
a
;
border
:
none
;
color
:
#fff
;
text-align
:
left
;
cursor
:
pointer
;
border-radius
:
20px
;
margin-bottom
:
5px
;
}
.status-menu
button
:hover
{
background
:
#3b82f6
;
.status-menu
button
[
data-status
=
"online"
]
{
background
:
#794c4c
;
}
.status-menu
button
:last-child
{
margin-bottom
:
0
;
}
.status-menu
button
.platform-status
{
display
:
inline-block
;
width
:
12px
;
height
:
12px
;
border-radius
:
50%
;
margin-left
:
8px
;
}
.status-menu
button
.platform-status.online
{
background
:
#28a745
;
}
.status-menu
button
.platform-status.offline
{
background
:
#dc3545
;
}
/* Tabs */
...
...
@@ -492,6 +570,23 @@
background
:
#b91c1c
;
}
/* Notification indicator */
.notification-indicator
{
width
:
10px
;
height
:
10px
;
border-radius
:
50%
;
background-color
:
#ff3860
;
display
:
inline-block
;
margin-right
:
8px
;
animation
:
blink
1s
infinite
;
}
@keyframes
blink
{
0
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0.3
;
}
100
%
{
opacity
:
1
;
}
}
/* Private chats list styles */
.private-chats-list
{
margin-top
:
10px
;
...
...
@@ -555,7 +650,11 @@
resize
:
both
;
overflow
:
hidden
;
min-width
:
300px
;
min-height
:
310px
;
min-height
:
310px
;
}
.private-chat-window-container.active
{
z-index
:
991
;
}
.private-chat-content
{
...
...
@@ -579,6 +678,17 @@
cursor
:
move
;
/* Indicate draggable */
}
.private-chat-title-container
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.private-chat-header
.notification-indicator
{
display
:
none
;
/* Hidden by default */
margin-right
:
8px
;
}
.private-chat-title
{
font-size
:
16px
;
font-weight
:
600
;
...
...
@@ -708,6 +818,13 @@
<div
class=
"top-bar"
>
<img
src=
"https://www.sexhack.me/content/uploads/2022/06/cropped-sexhack-300x99.png"
alt=
"SexHack Logo"
>
<span>
SHM CamStudio by
<a
href=
"https://www.sexhack.me"
>
sexhack.me
</a></span>
<div
style=
"flex: 1;"
></div>
<div
class=
"auto-open-toggle"
>
<label
for=
"auto-open-chats"
title=
"Auto-open private chat windows"
>
<input
type=
"checkbox"
id=
"auto-open-chats"
checked
>
<span>
Auto-open chats
</span>
</label>
</div>
</div>
<div
class=
"container"
>
<div
class=
"left-column"
>
...
...
@@ -722,17 +839,33 @@
Bitrate: 2 Mbps | Quality: HD | Audio:
<span
id=
"audio-bar"
>
████
</span>
</div>
</div>
<button
class=
"status-button offline"
id=
"status-button"
>
GO ONLINE
</button>
<div
class=
"status-menu"
id=
"status-menu"
>
<button
data-platform=
"C4.sexhackme"
>
C4.sexhackme: Offline
</button>
<button
data-platform=
"SC.spora"
>
SC.spora: Offline
</button>
<div
class=
"status-button-container"
>
<button
class=
"status-button offline"
id=
"status-button"
>
GO ONLINE
</button>
<button
class=
"status-toggle"
id=
"status-toggle"
>
⋮
</button>
<div
class=
"status-menu"
id=
"status-menu"
>
<button
data-platform=
"C4.sexhackme"
>
C4.sexhackme
<span
class=
"platform-status offline"
></span>
</button>
<button
data-platform=
"SC.spora"
>
SC.spora
<span
class=
"platform-status offline"
></span>
</button>
</div>
</div>
<div
class=
"tabs"
>
<div
class=
"tab active"
data-tab=
"panel"
>
Panel
</div>
<div
class=
"tab"
data-tab=
"earnings"
>
Earnings
</div>
<div
class=
"tab"
data-tab=
"session"
>
Sessions
</div>
</div>
<div
class=
"tab-content active"
id=
"panel"
>
<iframe
id=
"panel-iframe"
style=
"width:100%;height:300px;border:none;border-radius:8px;"
></iframe>
<div
class=
"panel-container"
style=
"position: relative;"
>
<div
class=
"panel-header"
style=
"display: flex; justify-content: space-between; align-items: center; background: #1e293b; padding: 5px 10px; border-radius: 8px 8px 0 0; margin-bottom: 2px;"
>
<span
style=
"font-size: 14px; font-weight: 500;"
>
Control Panel
</span>
<button
id=
"panel-expand-toggle"
style=
"background: #3b82f6; color: white; border: none; border-radius: 4px; width: 30px; height: 24px; cursor: pointer; font-size: 14px;"
>
⤢
</button>
</div>
<iframe
id=
"panel-iframe"
style=
"width:100%;height:300px;border:none;border-radius:0 0 8px 8px;"
src=
"/panel"
></iframe>
</div>
</div>
<div
class=
"tab-content"
id=
"earnings"
>
<table
class=
"earnings-table"
>
...
...
@@ -774,7 +907,7 @@
<div
class=
"right-column"
>
<div
class=
"tabs"
>
<div
class=
"tab active"
data-tab=
"userlist"
>
Userlist (
<span
id=
"total-users"
>
0
</span>
)
</div>
<div
class=
"tab"
data-tab=
"private-chats"
>
Private Chats (
<span
id=
"private-chats-count"
>
0
</span>
)
</div>
<div
class=
"tab"
data-tab=
"private-chats"
>
<span
class=
"notification-indicator"
id=
"private-chats-tab-indicator"
style=
"display: none;"
></span>
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>
...
...
@@ -797,37 +930,20 @@
<!-- Container for multiple private chat windows -->
<div
id=
"private-chat-windows-container"
></div>
<!-- Include Socket.IO client library -->
<script
src=
"https://cdn.socket.io/4.5.4/socket.io.min.js"
></script>
<script>
// Fake data for messages, users, earnings, status, and RTSP URLs
const
fakeMessages
=
[
{
sender
:
"me"
,
content
:
"Hello! 😊"
,
timestamp
:
"2025-06-21 20:00"
},
{
sender
:
"alice@C4.sexhackme"
,
content
:
"Hi! Check this: https://example.com"
,
timestamp
:
"2025-06-21 20:01"
},
{
sender
:
"bob@SC.spora"
,
content
:
"<img src='https://pbs.twimg.com/media/FsypEu2X0AEtwK3?format=jpg&name=small' alt='Placeholder Image'>"
,
timestamp
:
"2025-06-21 20:02"
},
{
sender
:
"system"
,
content
:
"system: *** Server maintenance scheduled at 1 AM"
,
type
:
"notify-system"
,
timestamp
:
"2025-06-21 20:03"
},
{
sender
:
"platform@SC.spora"
,
content
:
"platform: *** User joined the room"
,
type
:
"notify-platform"
,
timestamp
:
"2025-06-21 20:04"
},
{
sender
:
"bob@SC.spora"
,
content
:
"<span class='sender' data-sender='bob@SC.spora' style='color: #10b981'>bob@SC.spora</span> TIPPED <b>50 TOKENS</b> ($5.00)<div style='text-align: center; color: #6ee7b7'>PM REQUEST</div>"
,
type
:
"tip"
,
timestamp
:
"2025-06-21 20:05"
}
];
const
fakeUsers
=
{
"C4.sexhackme"
:
[
{
username
:
"alice"
,
status
:
"online"
,
tokens
:
50
},
{
username
:
"charlie"
,
status
:
"offline"
,
tokens
:
20
}
],
"SC.spora"
:
[
{
username
:
"bob"
,
status
:
"online"
,
tokens
:
30
}
]
};
const
fakeEarnings
=
[
{
platform
:
"C4.sexhackme"
,
lastSession
:
100
,
today
:
250
,
lastHour
:
50
,
sess
:
100
},
{
platform
:
"SC.spora"
,
lastSession
:
80
,
today
:
200
,
lastHour
:
30
,
sess
:
80
}
];
const
fakeStatus
=
{
"C4.sexhackme"
:
"online"
,
"SC.spora"
:
"offline"
};
const
fakeRtspUrls
=
[
{
id
:
"rtsp1"
,
url
:
"rtsp://example.com/stream1"
},
{
id
:
"rtsp2"
,
url
:
"rtsp://example.com/stream2"
}
];
// Data variables that will be populated from the server
let
messages
=
[];
let
users
=
{};
let
earnings
=
[];
let
statusData
=
{};
let
rtspUrls
=
[];
let
privateChats
=
{};
// Configuration variables
let
autoOpenPrivateChats
=
true
;
// Default to true (enabled)
// Initialize video
const
video
=
document
.
getElementById
(
'video'
);
...
...
@@ -848,7 +964,7 @@
}
catch
(
err
)
{
console
.
error
(
'Error enumerating devices:'
,
err
);
}
fakeR
tspUrls
.
forEach
(
url
=>
{
r
tspUrls
.
forEach
(
url
=>
{
const
option
=
document
.
createElement
(
'option'
);
option
.
value
=
url
.
id
;
option
.
text
=
url
.
url
;
...
...
@@ -866,9 +982,9 @@
if
(
videoSource
.
value
===
'none'
)
{
video
.
srcObject
=
null
;
video
.
src
=
''
;
}
else
if
(
fakeR
tspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
))
{
}
else
if
(
r
tspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
))
{
video
.
srcObject
=
null
;
video
.
src
=
fakeR
tspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
).
url
;
video
.
src
=
r
tspUrls
.
find
(
url
=>
url
.
id
===
videoSource
.
value
).
url
;
}
else
{
navigator
.
mediaDevices
.
getUserMedia
({
video
:
{
deviceId
:
videoSource
.
value
}
})
.
then
(
stream
=>
video
.
srcObject
=
stream
)
...
...
@@ -906,34 +1022,107 @@
// Status button and menu
const
statusButton
=
document
.
getElementById
(
'status-button'
);
const
statusToggle
=
document
.
getElementById
(
'status-toggle'
);
const
statusMenu
=
document
.
getElementById
(
'status-menu'
);
statusButton
.
addEventListener
(
'click'
,
(
e
)
=>
{
// Main status button toggles all platforms at once
statusButton
.
addEventListener
(
'click'
,
()
=>
{
const
isCurrentlyOffline
=
statusButton
.
textContent
===
'GO ONLINE'
;
const
newStatus
=
isCurrentlyOffline
?
'online'
:
'offline'
;
// Send status update to server
fetch
(
'/api/update_status'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({
all
:
newStatus
}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
statusData
=
data
.
status
;
updateStatus
();
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error updating status:'
,
error
);
});
});
// Toggle button shows/hides the platform menu
statusToggle
.
addEventListener
(
'click'
,
(
e
)
=>
{
e
.
stopPropagation
();
// Prevent triggering the main button
statusMenu
.
style
.
display
=
statusMenu
.
style
.
display
===
'block'
?
'none'
:
'block'
;
statusMenu
.
style
.
left
=
`
${
e
.
pageX
}
px`
;
statusMenu
.
style
.
top
=
`
${
e
.
pageY
}
px`
;
});
// Close menu when clicking outside
document
.
addEventListener
(
'click'
,
(
e
)
=>
{
if
(
!
statusToggle
.
contains
(
e
.
target
)
&&
!
statusMenu
.
contains
(
e
.
target
))
{
statusMenu
.
style
.
display
=
'none'
;
}
});
// Individual platform buttons
statusMenu
.
querySelectorAll
(
'button'
).
forEach
(
btn
=>
{
btn
.
addEventListener
(
'click'
,
()
=>
{
btn
.
addEventListener
(
'click'
,
(
e
)
=>
{
e
.
stopPropagation
();
const
platform
=
btn
.
dataset
.
platform
;
fakeStatus
[
platform
]
=
fakeStatus
[
platform
]
===
'online'
?
'offline'
:
'online'
;
updateStatus
();
statusMenu
.
style
.
display
=
'none'
;
const
newStatus
=
statusData
[
platform
]
===
'online'
?
'offline'
:
'online'
;
// Send status update to server
fetch
(
'/api/update_status'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({
platform
:
platform
,
status
:
newStatus
}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
statusData
=
data
.
status
;
updateStatus
();
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error updating status:'
,
error
);
});
});
});
function
updateStatus
()
{
const
isOnline
=
Object
.
values
(
fakeStatus
).
some
(
status
=>
status
===
'online'
);
// Update main button based on any platform being online
const
isOnline
=
Object
.
values
(
statusData
).
some
(
status
=>
status
===
'online'
);
statusButton
.
className
=
`status-button
${
isOnline
?
'online'
:
'offline'
}
`
;
statusButton
.
textContent
=
isOnline
?
'GO OFFLINE'
:
'GO ONLINE'
;
// Update individual platform buttons
statusMenu
.
querySelectorAll
(
'button'
).
forEach
(
btn
=>
{
const
platform
=
btn
.
dataset
.
platform
;
btn
.
textContent
=
`
${
platform
}
:
${
fakeStatus
[
platform
]}
`
;
const
statusDot
=
btn
.
querySelector
(
'.platform-status'
);
const
isOnline
=
statusData
[
platform
]
===
'online'
;
// Update status dot
statusDot
.
className
=
`platform-status
${
statusData
[
platform
]}
`
;
// Update button status attribute for styling
if
(
isOnline
)
{
btn
.
setAttribute
(
'data-status'
,
'online'
);
}
else
{
btn
.
removeAttribute
(
'data-status'
);
}
});
}
// Load panel content
const
panelIframe
=
document
.
getElementById
(
'panel-iframe'
);
panelIframe
.
srcdoc
=
'<p>Control panel content loaded here.</p>'
;
//
const panelIframe = document.getElementById('panel-iframe');
//
panelIframe.srcdoc = '
<
p
>
Control
panel
content
loaded
here
.
<
/p>'
;
// Chat functionality
const
chatWindow
=
document
.
getElementById
(
'chat-window'
);
...
...
@@ -1000,18 +1189,18 @@
let
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
function
getUsernames
()
{
const
users
=
new
Set
();
for
(
const
platform
in
fakeU
sers
)
{
fakeU
sers
[
platform
].
forEach
(
user
=>
{
users
.
add
(
`
${
user
.
username
}
@
${
platform
}
`
);
const
user
name
s
=
new
Set
();
for
(
const
platform
in
u
sers
)
{
u
sers
[
platform
].
forEach
(
user
=>
{
user
name
s
.
add
(
`
${
user
.
username
}
@
${
platform
}
`
);
});
}
fakeM
essages
.
forEach
(
msg
=>
{
m
essages
.
forEach
(
msg
=>
{
if
(
msg
.
sender
!==
'me'
&&
!
msg
.
sender
.
startsWith
(
'system'
)
&&
!
msg
.
sender
.
startsWith
(
'platform@'
))
{
users
.
add
(
msg
.
sender
);
user
name
s
.
add
(
msg
.
sender
);
}
});
return
Array
.
from
(
users
);
return
Array
.
from
(
user
name
s
);
}
chatInput
.
addEventListener
(
'keydown'
,
(
e
)
=>
{
...
...
@@ -1061,8 +1250,30 @@
const
platform
=
platformSelector
.
value
;
if
(
content
)
{
console
.
log
(
`Sending message to
${
platform
}
:
${
content
}
`
);
fakeMessages
.
push
({
sender
:
'me'
,
content
,
timestamp
:
new
Date
().
toISOString
()
});
renderMessages
(
fakeMessages
);
// Send message to server
fetch
(
`/api/send_message?client_id=
${
clientId
}
`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({
content
:
content
,
platform
:
platform
}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
// Add new message to the list
messages
.
push
(
data
.
message
);
renderMessages
(
messages
);
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error sending message:'
,
error
);
});
chatInput
.
value
=
''
;
completionState
=
{
index
:
-
1
,
prefix
:
''
,
original
:
''
,
matches
:
[]
};
}
...
...
@@ -1100,9 +1311,9 @@
console
.
error
(
`Invalid platform.account format:
${
platformAccount
}
`
);
return
;
}
const
userList
=
fakeU
sers
[
`
${
platform
}
.
${
account
}
`
];
const
userList
=
u
sers
[
`
${
platform
}
.
${
account
}
`
];
if
(
!
userList
)
{
console
.
error
(
`Platform not found in
fakeU
sers:
${
platform
}
.
${
account
}
`
);
console
.
error
(
`Platform not found in
u
sers:
${
platform
}
.
${
account
}
`
);
return
;
}
const
user
=
userList
.
find
(
u
=>
u
.
username
===
username
);
...
...
@@ -1149,14 +1360,14 @@
const
totalUsers
=
document
.
getElementById
(
'total-users'
);
let
total
=
0
;
userList
.
innerHTML
=
''
;
for
(
const
platform
in
fakeU
sers
)
{
total
+=
fakeU
sers
[
platform
].
length
;
for
(
const
platform
in
u
sers
)
{
total
+=
u
sers
[
platform
].
length
;
const
platformDiv
=
document
.
createElement
(
'div'
);
platformDiv
.
className
=
'platform'
;
platformDiv
.
innerHTML
=
`<div class="platform-header">
${
platform
}
<span>(
${
fakeU
sers
[
platform
].
length
}
)</span></div>`
;
platformDiv
.
innerHTML
=
`<div class="platform-header">
${
platform
}
<span>(
${
u
sers
[
platform
].
length
}
)</span></div>`
;
const
usersDiv
=
document
.
createElement
(
'div'
);
usersDiv
.
className
=
'users'
;
fakeU
sers
[
platform
].
forEach
(
user
=>
{
u
sers
[
platform
].
forEach
(
user
=>
{
const
userDiv
=
document
.
createElement
(
'div'
);
userDiv
.
className
=
'user'
;
userDiv
.
dataset
.
sender
=
`
${
user
.
username
}
@
${
platform
}
`
;
...
...
@@ -1186,20 +1397,35 @@
}
// Tabs
document
.
querySelectorAll
(
'.tab'
).
forEach
(
tab
=>
{
tab
.
addEventListener
(
'click'
,
()
=>
{
document
.
querySelectorAll
(
'.tab'
).
forEach
(
t
=>
t
.
classList
.
remove
(
'active'
));
document
.
querySelectorAll
(
'.tab-content'
).
forEach
(
c
=>
c
.
classList
.
remove
(
'active'
));
tab
.
classList
.
add
(
'active'
);
document
.
getElementById
(
tab
.
dataset
.
tab
).
classList
.
add
(
'active'
);
});
});
document
.
querySelectorAll
(
'.tabs'
).
forEach
(
tabGroup
=>
{
tabGroup
.
querySelectorAll
(
'.tab'
).
forEach
(
tab
=>
{
tab
.
addEventListener
(
'click'
,
()
=>
{
// Only affect tabs within the same tab group
tabGroup
.
querySelectorAll
(
'.tab'
).
forEach
(
t
=>
t
.
classList
.
remove
(
'active'
));
tab
.
classList
.
add
(
'active'
);
// Only affect tab content elements related to this tab group
const
tabContentId
=
tab
.
dataset
.
tab
;
document
.
getElementById
(
tabContentId
).
classList
.
add
(
'active'
);
// Find all sibling tab contents and hide them
const
tabContents
=
document
.
querySelectorAll
(
'.tab-content'
);
tabContents
.
forEach
(
content
=>
{
if
(
content
.
id
!==
tabContentId
&&
((
tabGroup
.
closest
(
'.left-column'
)
&&
content
.
closest
(
'.left-column'
))
||
(
tabGroup
.
closest
(
'.right-column'
)
&&
content
.
closest
(
'.right-column'
))))
{
content
.
classList
.
remove
(
'active'
);
}
});
});
});
});
// Earnings
function
renderEarnings
()
{
const
tbody
=
document
.
querySelector
(
'.earnings-table tbody'
);
tbody
.
innerHTML
=
''
;
const
totals
=
fakeE
arnings
.
reduce
((
acc
,
e
)
=>
({
const
totals
=
e
arnings
.
reduce
((
acc
,
e
)
=>
({
lastSession
:
acc
.
lastSession
+
e
.
lastSession
,
today
:
acc
.
today
+
e
.
today
,
lastHour
:
acc
.
lastHour
+
e
.
lastHour
,
...
...
@@ -1215,7 +1441,7 @@
<td>$
${
totals
.
sess
}
</td>
`
;
tbody
.
appendChild
(
totalsRow
);
fakeE
arnings
.
forEach
(
e
=>
{
e
arnings
.
forEach
(
e
=>
{
const
[
platform
,
account
]
=
e
.
platform
.
split
(
'.'
);
const
row
=
document
.
createElement
(
'tr'
);
row
.
innerHTML
=
`
...
...
@@ -1231,15 +1457,29 @@
// Reset session button
document
.
getElementById
(
'reset-session'
).
addEventListener
(
'click'
,
()
=>
{
fakeEarnings
.
forEach
(
e
=>
e
.
sess
=
0
);
renderEarnings
();
fetch
(
'/api/reset_session'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
// The long polling will automatically update the earnings data
console
.
log
(
"Session reset successfully"
);
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error resetting session:'
,
error
);
});
});
// Private Chat functionality
const
privateChatWindowsContainer
=
document
.
getElementById
(
'private-chat-windows-container'
);
// Store private chat messages and states
const
privateChats
=
{};
// Store chat messages by user
// Store private chat states
const
openPrivateChats
=
new
Set
();
// Track open private chats
const
activeChatWindows
=
{};
// Store references to active chat windows
...
...
@@ -1269,16 +1509,30 @@
chatItem
.
className
=
'private-chat-item'
;
chatItem
.
dataset
.
sender
=
sender
;
// Create notification indicator
const
notificationIndicator
=
document
.
createElement
(
'span'
);
notificationIndicator
.
className
=
'notification-indicator'
;
// Initially hide the notification indicator
notificationIndicator
.
style
.
display
=
'none'
;
const
nameSpan
=
document
.
createElement
(
'div'
);
nameSpan
.
className
=
'private-chat-item-name'
;
nameSpan
.
textContent
=
sender
;
// Create a container for the indicator and name
const
leftContainer
=
document
.
createElement
(
'div'
);
leftContainer
.
style
.
display
=
'flex'
;
leftContainer
.
style
.
alignItems
=
'center'
;
leftContainer
.
appendChild
(
notificationIndicator
);
leftContainer
.
appendChild
(
nameSpan
);
const
closeButton
=
document
.
createElement
(
'div'
);
closeButton
.
className
=
'private-chat-item-close'
;
closeButton
.
innerHTML
=
'×'
;
closeButton
.
title
=
'Close chat'
;
chatItem
.
appendChild
(
nameSpan
);
chatItem
.
appendChild
(
leftContainer
);
chatItem
.
appendChild
(
closeButton
);
// Click on chat item to open/restore the chat
...
...
@@ -1325,7 +1579,10 @@
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-title-container">
<span class="notification-indicator"></span>
<div class="private-chat-title">Private Chat with <span class="private-chat-username">
${
sender
}
</span></div>
</div>
<div class="private-chat-controls">
<span class="minimize-chat" title="Minimize">_</span>
<span class="close-chat" title="Close">×</span>
...
...
@@ -1423,6 +1680,11 @@
showContextMenu
(
e
,
sender
,
true
);
});
// Set active window on click (for z-index management)
chatWindow
.
addEventListener
(
'mousedown'
,
()
=>
{
setActivePrivateChat
(
sender
);
});
// Send button
sendBtn
.
addEventListener
(
'click'
,
()
=>
{
sendPrivateMessage
(
sender
);
...
...
@@ -1464,6 +1726,81 @@
// Focus the input
chatWindow
.
querySelector
(
'textarea'
).
focus
();
// Mark messages as read
markMessagesAsRead
(
sender
);
// Set as active window
setActivePrivateChat
(
sender
);
}
// Set active private chat window (manage z-index)
function
setActivePrivateChat
(
sender
)
{
// Set all windows to z-index 990
Object
.
keys
(
activeChatWindows
).
forEach
(
key
=>
{
if
(
activeChatWindows
[
key
])
{
activeChatWindows
[
key
].
classList
.
remove
(
'active'
);
}
});
// Set the clicked window to z-index 991
if
(
activeChatWindows
[
sender
])
{
activeChatWindows
[
sender
].
classList
.
add
(
'active'
);
}
}
// Mark messages as read for a specific sender
function
markMessagesAsRead
(
sender
)
{
fetch
(
`/api/mark_messages_read?client_id=
${
clientId
}
`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({
sender
:
sender
}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
// Update the notification indicator in the chat list
const
chatItem
=
document
.
querySelector
(
`.private-chat-item[data-sender="
${
sender
}
"]`
);
if
(
chatItem
)
{
const
indicator
=
chatItem
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
indicator
.
style
.
display
=
'none'
;
}
}
// Also hide the indicator in the chat window header
const
chatWindow
=
activeChatWindows
[
sender
];
if
(
chatWindow
)
{
const
headerIndicator
=
chatWindow
.
querySelector
(
'.private-chat-header .notification-indicator'
);
if
(
headerIndicator
)
{
headerIndicator
.
style
.
display
=
'none'
;
}
}
// Check if any other chats have unread messages
let
anyUnread
=
false
;
document
.
querySelectorAll
(
'.private-chat-item .notification-indicator'
).
forEach
(
ind
=>
{
if
(
ind
.
style
.
display
===
'inline-block'
)
{
anyUnread
=
true
;
}
});
// Update tab indicator
if
(
!
anyUnread
)
{
const
tabIndicator
=
document
.
getElementById
(
'private-chats-tab-indicator'
);
if
(
tabIndicator
)
{
tabIndicator
.
style
.
display
=
'none'
;
}
}
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error marking messages as read:'
,
error
);
});
}
// Close a private chat window
...
...
@@ -1541,59 +1878,428 @@
const
content
=
textarea
.
value
.
trim
();
if
(
content
)
{
// Add message to private chat
if
(
!
privateChats
[
sender
])
{
privateChats
[
sender
]
=
[];
// Send private message to server
fetch
(
`/api/send_private_message?client_id=
${
clientId
}
`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
({
recipient
:
sender
,
content
:
content
}),
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
// Update private chat messages
privateChats
[
sender
]
=
data
.
messages
;
renderPrivateMessages
(
sender
);
}
})
.
catch
(
error
=>
{
console
.
error
(
'Error sending private message:'
,
error
);
});
textarea
.
value
=
''
;
}
}
// Panel expand/collapse functionality
const
panelExpandToggle
=
document
.
getElementById
(
'panel-expand-toggle'
);
const
panelIframe
=
document
.
getElementById
(
'panel-iframe'
);
const
panelContainer
=
document
.
querySelector
(
'.panel-container'
);
let
isPanelExpanded
=
false
;
panelExpandToggle
.
addEventListener
(
'click'
,
()
=>
{
isPanelExpanded
=
!
isPanelExpanded
;
if
(
isPanelExpanded
)
{
// Expand panel
panelContainer
.
style
.
position
=
'fixed'
;
panelContainer
.
style
.
top
=
'10%'
;
panelContainer
.
style
.
left
=
'10%'
;
panelContainer
.
style
.
width
=
'80%'
;
panelContainer
.
style
.
height
=
'80%'
;
panelContainer
.
style
.
zIndex
=
'1010'
;
panelIframe
.
style
.
height
=
'100%'
;
panelExpandToggle
.
innerHTML
=
'⤓'
;
panelContainer
.
style
.
background
=
'#1c1c1c'
;
panelContainer
.
style
.
boxShadow
=
'0 0 20px rgba(0, 0, 0, 0.5)'
;
panelContainer
.
style
.
borderRadius
=
'8px'
;
}
else
{
// Collapse panel
panelContainer
.
style
.
position
=
'relative'
;
panelContainer
.
style
.
top
=
'auto'
;
panelContainer
.
style
.
left
=
'auto'
;
panelContainer
.
style
.
width
=
'100%'
;
panelContainer
.
style
.
height
=
'auto'
;
panelContainer
.
style
.
zIndex
=
'auto'
;
panelIframe
.
style
.
height
=
'300px'
;
panelExpandToggle
.
innerHTML
=
'⤢'
;
panelContainer
.
style
.
background
=
'none'
;
panelContainer
.
style
.
boxShadow
=
'none'
;
}
});
// Client ID for tracking requests
const
clientId
=
`client-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
;
let
lastRequestTime
=
0
;
/**
* Hybrid real-time data synchronization system
*
* This implements a dual approach for real-time updates:
* 1. Primary: SocketIO for immediate push notifications when data changes
* 2. Fallback: Long polling with adaptive backoff for reliability
*
* The combination ensures:
* - Immediate updates when possible (via SocketIO)
* - Reliability in all network conditions (via long polling)
* - Efficient server resource usage (non-blocking on server, adaptive polling on client)
* - Multiple browser windows can open the chat page simultaneously
*/
async
function
fetchData
()
{
try
{
// Initial data load
await
fetchChatData
();
// Ensure UI is updated with initial data
renderMessages
(
messages
);
renderUserList
();
renderEarnings
();
updateStatus
();
renderPrivateChatslist
();
// Start long polling as a fallback mechanism
// SocketIO will be the primary update mechanism
startLongPolling
();
}
catch
(
error
)
{
console
.
error
(
"Error fetching data:"
,
error
);
// Retry after a short delay
setTimeout
(
fetchData
,
5000
);
}
}
// Fetch chat data from the consolidated API endpoint
async
function
fetchChatData
()
{
try
{
const
response
=
await
fetch
(
`/api/chat?client_id=
${
clientId
}
&last_request=
${
lastRequestTime
}
`
);
const
data
=
await
response
.
json
();
// Update last request time for next poll
lastRequestTime
=
data
.
timestamp
;
// If there are no updates, just return the data for retry handling
if
(
data
.
no_update
)
{
return
data
;
}
// Update all data
messages
=
data
.
messages
||
[];
users
=
data
.
users
||
{};
earnings
=
data
.
earnings
||
[];
statusData
=
data
.
status
||
{};
rtspUrls
=
data
.
rtsp_urls
||
[];
privateChats
=
data
.
private_chats
||
{};
// Initialize private chats if they exist in the data
if
(
privateChats
)
{
for
(
const
sender
in
privateChats
)
{
if
(
privateChats
[
sender
]
&&
privateChats
[
sender
].
length
>
0
)
{
// Add to open chats if not already there
if
(
!
openPrivateChats
.
has
(
sender
))
{
openPrivateChats
.
add
(
sender
);
}
}
}
}
// Handle unread private messages
if
(
data
.
unread_private_messages
)
{
updateUnreadIndicators
(
data
.
unread_private_messages
);
// Handle unread messages
for
(
const
sender
in
data
.
unread_private_messages
)
{
if
(
data
.
unread_private_messages
[
sender
]
>
0
&&
privateChats
[
sender
]
&&
privateChats
[
sender
].
length
>
0
)
{
// Add to open chats if not already there
if
(
!
openPrivateChats
.
has
(
sender
))
{
openPrivateChats
.
add
(
sender
);
// Open a new chat window if auto-open is enabled
if
(
autoOpenPrivateChats
&&
!
activeChatWindows
[
sender
])
{
openPrivateChat
(
sender
);
}
else
if
(
!
activeChatWindows
[
sender
]
||
activeChatWindows
[
sender
].
style
.
display
===
'none'
)
{
// Show notification indicator in the chat list
const
chatItem
=
document
.
querySelector
(
`.private-chat-item[data-sender="
${
sender
}
"]`
);
if
(
chatItem
)
{
const
indicator
=
chatItem
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
indicator
.
style
.
display
=
'inline-block'
;
}
}
}
}
}
}
}
// Always render the private chats list
renderPrivateChatslist
();
// Update UI
renderMessages
(
messages
);
renderUserList
();
renderEarnings
();
updateStatus
();
// Update private chats list
renderPrivateChatslist
();
return
data
;
}
catch
(
error
)
{
console
.
error
(
"Error fetching chat data:"
,
error
);
throw
error
;
}
}
// Adaptive polling with exponential backoff
let
pollBackoffDelay
=
1000
;
// Start with 1 second
const
maxPollBackoffDelay
=
10000
;
// Max 10 seconds
// Start long polling for updates
function
startLongPolling
()
{
fetchChatData
()
.
then
(
data
=>
{
if
(
data
&&
data
.
no_update
)
{
// No updates available, use adaptive backoff
pollBackoffDelay
=
Math
.
min
(
pollBackoffDelay
*
1.5
,
maxPollBackoffDelay
);
const
retryAfter
=
data
.
retry_after
||
pollBackoffDelay
;
console
.
log
(
`No updates, next poll in
${
retryAfter
}
ms`
);
setTimeout
(
startLongPolling
,
retryAfter
);
}
else
{
// Got updates, reset backoff and immediately start the next poll
pollBackoffDelay
=
1000
;
startLongPolling
();
}
})
.
catch
(
error
=>
{
console
.
error
(
"Long polling error:"
,
error
);
// Retry after a short delay
setTimeout
(
startLongPolling
,
5000
);
});
}
// Initialize SocketIO connection with reconnection options
const
socket
=
io
({
reconnection
:
true
,
reconnectionAttempts
:
Infinity
,
reconnectionDelay
:
1000
,
reconnectionDelayMax
:
5000
,
timeout
:
20000
});
// Listen for chat updates
socket
.
on
(
'chat_update'
,
(
data
)
=>
{
console
.
log
(
'Received update notification:'
,
data
);
// If it's a private chat update with specific sender info
if
(
data
.
type
===
'private_chats'
&&
data
.
sender
)
{
// Fetch the latest data with priority
fetchChatData
().
then
(
responseData
=>
{
// If this update indicates unread messages
if
(
data
.
has_unread
)
{
const
sender
=
data
.
sender
;
// Add to open chats if not already there
if
(
!
openPrivateChats
.
has
(
sender
))
{
openPrivateChats
.
add
(
sender
);
renderPrivateChatslist
();
}
// Show indicator in the tab title
const
tabIndicator
=
document
.
getElementById
(
'private-chats-tab-indicator'
);
if
(
tabIndicator
)
{
tabIndicator
.
style
.
display
=
'inline-block'
;
}
// Check if we should auto-open or just show notification
const
chatWindow
=
activeChatWindows
[
sender
];
if
(
!
chatWindow
)
{
if
(
autoOpenPrivateChats
)
{
// Create and open a new chat window if auto-open is enabled
openPrivateChat
(
sender
);
}
else
{
// Just show notification in the chat list
const
chatItem
=
document
.
querySelector
(
`.private-chat-item[data-sender="
${
sender
}
"]`
);
if
(
chatItem
)
{
const
indicator
=
chatItem
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
indicator
.
style
.
display
=
'inline-block'
;
}
}
}
}
else
if
(
chatWindow
.
style
.
display
===
'none'
)
{
// Window exists but is minimized, show notification in chat list
const
chatItem
=
document
.
querySelector
(
`.private-chat-item[data-sender="
${
sender
}
"]`
);
if
(
chatItem
)
{
const
indicator
=
chatItem
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
indicator
.
style
.
display
=
'inline-block'
;
}
}
}
else
if
(
!
chatWindow
.
classList
.
contains
(
'active'
))
{
// Window is open but not focused, show indicator in header
const
headerIndicator
=
chatWindow
.
querySelector
(
'.private-chat-header .notification-indicator'
);
if
(
headerIndicator
)
{
headerIndicator
.
style
.
display
=
'inline-block'
;
}
}
// Update the chat window content if it exists
if
(
chatWindow
&&
privateChats
[
sender
])
{
renderPrivateMessages
(
sender
);
}
}
else
if
(
data
.
has_unread
===
false
)
{
// This is a "mark as read" update
const
sender
=
data
.
sender
;
privateChats
[
sender
].
push
({
sender
:
'me'
,
content
:
content
,
timestamp
:
new
Date
().
toISOString
()
// Hide notification indicators
const
chatItem
=
document
.
querySelector
(
`.private-chat-item[data-sender="
${
sender
}
"]`
);
if
(
chatItem
)
{
const
indicator
=
chatItem
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
indicator
.
style
.
display
=
'none'
;
}
}
// Check if any other chats have unread messages
let
anyUnread
=
false
;
document
.
querySelectorAll
(
'.private-chat-item .notification-indicator'
).
forEach
(
ind
=>
{
if
(
ind
.
style
.
display
===
'inline-block'
)
{
anyUnread
=
true
;
}
});
// 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
);
// Update tab indicator
if
(
!
anyUnread
)
{
const
tabIndicator
=
document
.
getElementById
(
'private-chats-tab-indicator'
);
if
(
tabIndicator
)
{
tabIndicator
.
style
.
display
=
'none'
;
}
}
,
1000
);
}
renderPrivateMessages
(
sender
);
textarea
.
value
=
''
;
const
chatWindow
=
activeChatWindows
[
sender
];
if
(
chatWindow
)
{
const
headerIndicator
=
chatWindow
.
querySelector
(
'.private-chat-header .notification-indicator'
);
if
(
headerIndicator
)
{
headerIndicator
.
style
.
display
=
'none'
;
}
}
}
});
}
else
if
(
data
.
type
===
'initial_connect'
)
{
// New connection - fetch all data immediately
fetchChatData
();
}
else
{
// For other updates, just fetch the data
fetchChatData
();
}
});
// Handle connection events
socket
.
on
(
'connect'
,
()
=>
{
console
.
log
(
'SocketIO connected'
);
});
socket
.
on
(
'disconnect'
,
()
=>
{
console
.
log
(
'SocketIO disconnected'
);
});
socket
.
on
(
'connect_error'
,
(
error
)
=>
{
console
.
error
(
'SocketIO connection error:'
,
error
);
});
// Function to update unread message indicators
function
updateUnreadIndicators
(
unreadCounts
)
{
// If unreadCounts is undefined or null, return early
if
(
!
unreadCounts
)
return
;
// First ensure all private chats are in the list
let
hasAnyUnread
=
false
;
for
(
const
sender
in
unreadCounts
)
{
if
(
unreadCounts
[
sender
]
>
0
&&
privateChats
[
sender
]
&&
privateChats
[
sender
].
length
>
0
)
{
// Add to open chats if not already there
if
(
!
openPrivateChats
.
has
(
sender
))
{
openPrivateChats
.
add
(
sender
);
renderPrivateChatslist
();
}
hasAnyUnread
=
true
;
}
}
// Update the tab indicator based on whether any chats have unread messages
const
tabIndicator
=
document
.
getElementById
(
'private-chats-tab-indicator'
);
if
(
tabIndicator
)
{
tabIndicator
.
style
.
display
=
hasAnyUnread
?
'inline-block'
:
'none'
;
}
// Update indicators in the private chats list
document
.
querySelectorAll
(
'.private-chat-item'
).
forEach
(
item
=>
{
const
sender
=
item
.
dataset
.
sender
;
const
indicator
=
item
.
querySelector
(
'.notification-indicator'
);
if
(
indicator
)
{
if
(
unreadCounts
[
sender
]
&&
unreadCounts
[
sender
]
>
0
)
{
// Show indicator in chat list
indicator
.
style
.
display
=
'inline-block'
;
hasAnyUnread
=
true
;
// Also show indicator in chat window header if window exists and is not active
const
chatWindow
=
activeChatWindows
[
sender
];
if
(
chatWindow
)
{
if
(
chatWindow
.
style
.
display
===
'none'
||
!
chatWindow
.
classList
.
contains
(
'active'
))
{
const
headerIndicator
=
chatWindow
.
querySelector
(
'.private-chat-header .notification-indicator'
);
if
(
headerIndicator
)
{
headerIndicator
.
style
.
display
=
'inline-block'
;
}
}
}
}
else
{
// Hide indicator
indicator
.
style
.
display
=
'none'
;
// Also hide in header
const
chatWindow
=
activeChatWindows
[
sender
];
if
(
chatWindow
)
{
const
headerIndicator
=
chatWindow
.
querySelector
(
'.private-chat-header .notification-indicator'
);
if
(
headerIndicator
)
{
headerIndicator
.
style
.
display
=
'none'
;
}
}
}
}
});
}
// Initialize
populateVideoSources
();
renderMessages
(
fakeMessages
);
renderUserList
();
renderEarnings
();
updateStatus
();
fetchData
();
// 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
();
// Auto-open toggle event listener
document
.
getElementById
(
'auto-open-chats'
).
addEventListener
(
'change'
,
function
()
{
autoOpenPrivateChats
=
this
.
checked
;
console
.
log
(
`Auto-open private chats:
${
autoOpenPrivateChats
?
'enabled'
:
'disabled'
}
`
);
// Save preference to localStorage
localStorage
.
setItem
(
'autoOpenPrivateChats'
,
autoOpenPrivateChats
);
});
// Load auto-open preference from localStorage
if
(
localStorage
.
getItem
(
'autoOpenPrivateChats'
)
!==
null
)
{
autoOpenPrivateChats
=
localStorage
.
getItem
(
'autoOpenPrivateChats'
)
===
'true'
;
document
.
getElementById
(
'auto-open-chats'
).
checked
=
autoOpenPrivateChats
;
}
// Handle window resize to keep chat windows within viewport
window
.
addEventListener
(
'resize'
,
()
=>
{
...
...
templates/panel.html
0 → 100644
View file @
ee7ebecd
<!--
Copyright (C) 2023 Stefy Lanza <stefy@nexlab.net> and SexHack.me
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
Streaming Control Panel
</title>
<style>
*
{
box-sizing
:
border-box
;
margin
:
0
;
padding
:
0
;
}
body
,
html
{
height
:
100%
;
font-family
:
Arial
,
sans-serif
;
overflow
:
hidden
;
}
.main-container
{
display
:
flex
;
height
:
100vh
;
width
:
100vw
;
}
.buttons-container
{
display
:
grid
;
grid-template-columns
:
repeat
(
120
,
1
fr
);
grid-template-rows
:
repeat
(
4
,
1
fr
);
height
:
100%
;
width
:
100%
;
}
.button_row
{
display
:
grid
;
grid-template-columns
:
repeat
(
120
,
1
fr
);
width
:
100%
;
grid-column
:
span
120
;
}
{
{style_rows|safe
}
}
.button
{
display
:
flex
;
justify-content
:
center
;
align-items
:
center
;
font-size
:
1.5vw
;
font-weight
:
bold
;
text-align
:
center
;
border
:
2px
solid
white
;
cursor
:
pointer
;
transition
:
all
0.3s
ease
;
text-transform
:
uppercase
;
}
/* Color variations */
.private
{
background-color
:
#4CAF50
;
color
:
white
;
}
.toggle
{
background-color
:
#2196F3
;
color
:
white
;
}
.special
{
background-color
:
#FF9800
;
color
:
white
;
}
.button
:hover
{
opacity
:
0.8
;
transform
:
scale
(
1.05
);
}
.button
:active
{
background-color
:
#45a049
;
transform
:
scale
(
0.95
);
}
/* Responsive adjustments */
@media
(
max-width
:
768px
)
{
.button
{
font-size
:
3vw
;
}
}
</style>
<script
src=
"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
integrity=
"sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg=="
crossorigin=
"anonymous"
></script>
<script
src=
"https://cdn.socket.io/4.7.5/socket.io.min.js"
integrity=
"sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO"
crossorigin=
"anonymous"
></script>
</head>
<body>
<div
class=
"main-container"
>
<!-- Buttons Container -->
<div
class=
"buttons-container"
>
{{htmlbuttons|safe}}
</div>
</div>
<script>
function
executeCommand
(
command
)
{
fetch
(
'/execute'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
,
},
body
:
`command=
${
command
}
`
})
.
then
(
response
=>
response
.
text
())
.
then
(
result
=>
{
console
.
log
(
result
);
// Visual feedback
/*event.target.style.backgroundColor = '#45a049';
setTimeout(() => {
event.target.style.backgroundColor =
event.target.classList.contains('private') ? '#4CAF50' :
event.target.classList.contains('toggle') ? '#2196F3' :
'#FF9800';
}, 300); */
})
.
catch
(
error
=>
{
console
.
error
(
'Error:'
,
error
);
});
}
// Prevent zoom on double-tap for mobile
document
.
addEventListener
(
'touchmove'
,
function
(
event
)
{
if
(
event
.
scale
!==
1
)
{
event
.
preventDefault
();
}
},
{
passive
:
false
});
$
(
document
).
ready
(
function
()
{
// Connect to the Socket.IO server.
// The connection URL has the following format, relative to the current page:
// http[s]://
<
domain
>
:
<
port
>
[
/<namespace>
]
var
socket
=
io
();
// Event handler for new connections.
// The callback function is invoked when a connection with the
// server is established.
socket
.
on
(
'connect'
,
function
()
{
socket
.
emit
(
'my_event'
,
{
data
:
'I
\'
m connected!'
});
});
// Event handler for server sent data.
// The callback function is invoked whenever the server emits data
// to the client. The data is then displayed in the "Received"
// section of the page.
socket
.
on
(
'my_response'
,
function
(
msg
,
cb
)
{
$
(
'#log'
).
append
(
'<br>'
+
$
(
'<div/>'
).
text
(
'Received #'
+
msg
.
count
+
': '
+
msg
.
data
).
html
());
if
(
cb
)
cb
();
});
// Interval function that tests message latency by sending a "ping"
// message. The server then responds with a "pong" message and the
// round trip time is measured.
var
ping_pong_times
=
[];
var
start_time
;
window
.
setInterval
(
function
()
{
start_time
=
(
new
Date
).
getTime
();
$
(
'#transport'
).
text
(
socket
.
io
.
engine
.
transport
.
name
);
socket
.
emit
(
'my_ping'
);
},
1000
);
// Handler for the "pong" message. When the pong is received, the
// time from the ping is stored, and the average of the last 30
// samples is average and displayed.
socket
.
on
(
'my_pong'
,
function
()
{
var
latency
=
(
new
Date
).
getTime
()
-
start_time
;
ping_pong_times
.
push
(
latency
);
ping_pong_times
=
ping_pong_times
.
slice
(
-
30
);
// keep last 30 samples
var
sum
=
0
;
for
(
var
i
=
0
;
i
<
ping_pong_times
.
length
;
i
++
)
sum
+=
ping_pong_times
[
i
];
$
(
'#ping-pong'
).
text
(
Math
.
round
(
10
*
sum
/
ping_pong_times
.
length
)
/
10
);
});
window
.
setInterval
(
function
()
{
socket
.
emit
(
'get_queue'
);
},
200
);
socket
.
on
(
'change_output'
,
function
(
msg
,
cb
)
{
//console.log('CHANGE OUTPUT');
//console.log(msg);
$
(
'.output_'
+
msg
.
button
).
css
(
'background-color'
,
msg
.
color
);
});
socket
.
on
(
'change_input'
,
function
(
msg
,
cb
)
{
$
(
'.input_'
+
msg
.
button
).
css
(
'background-color'
,
msg
.
color
);
});
socket
.
on
(
'change_feedback'
,
function
(
msg
,
cb
)
{
if
(
msg
.
hasOwnProperty
(
"color"
))
$
(
'.feedback_'
+
msg
.
feedback
).
css
(
'background-color'
,
msg
.
color
);
if
(
msg
.
hasOwnProperty
(
"title"
))
$
(
'.feedback_'
+
msg
.
feedback
).
text
(
msg
.
title
);
});
});
</script>
<div
id=
"log"
></div>
<div
id=
"ping-pong"
></div>
ms
</b>
<div
id=
"transport"
></div>
</body>
</html>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment