15
15
from selenium .webdriver .common .alert import Alert
16
16
from selenium .webdriver .common .by import By
17
17
from selenium .webdriver .common .keys import Keys
18
+ from selenium .webdriver .support import expected_conditions as EC
19
+ from selenium .webdriver .support .ui import WebDriverWait
18
20
19
- #How long to wait after performing any browser action, for the webpage to load its response
20
- BROWSER_ACTION_DELAY = 2
21
+ #How long to wait maximum for a condition to be true in the browser
22
+ BROWSER_WAIT_TIMEOUT = 30
21
23
22
24
#How long to wait between sending messages
23
25
SEND_MESSAGE_COOLDOWN = 3
52
54
53
55
class ChatCommand ():
54
56
"""A chat command, internal use only"""
55
- def __init__ (self , name , actor , cooldown = BROWSER_ACTION_DELAY , amount_cents = 0 , exclusive = False , allowed_badges = ["subscriber" ], whitelist_badges = ["moderator" ], target = None ):
57
+ def __init__ (self , name , actor , cooldown = SEND_MESSAGE_COOLDOWN , amount_cents = 0 , exclusive = False , allowed_badges = ["subscriber" ], whitelist_badges = ["moderator" ], target = None ):
56
58
"""name: The !name of the command
57
59
actor: The RumleChatActor host object
58
60
amount_cents: The minimum cost of the command. Defaults to free
59
61
exclusive: If this command can only be run by users with allowed badges. Defaults to False
60
- allowed_badges: Badges that are allowed to run this command (if it is exclusive). Defaults to subscribers, admin is added internally.
62
+ allowed_badges: Badges that are allowed to run this command (if it is exclusive).
63
+ Defaults to subscribers, admin is added internally.
61
64
whitelist_badges: Badges which if borne give the user free-of-charge command access
62
- target: The function(message, actor) to call on successful command usage . Defaults to self.run"""
65
+ target: The command function(message, actor) to call. Defaults to self.run"""
63
66
assert " " not in name , "Name cannot contain spaces"
64
67
self .name = name
65
68
self .actor = actor
66
- assert cooldown >= BROWSER_ACTION_DELAY , f"Cannot set a cooldown shorter than { BROWSER_ACTION_DELAY } "
69
+ assert cooldown >= SEND_MESSAGE_COOLDOWN , \
70
+ f"Cannot set a cooldown shorter than { SEND_MESSAGE_COOLDOWN } "
71
+
67
72
self .cooldown = cooldown
68
73
self .amount_cents = amount_cents #Cost of the command
69
74
self .exclusive = exclusive
@@ -75,19 +80,31 @@ def __init__(self, name, actor, cooldown = BROWSER_ACTION_DELAY, amount_cents =
75
80
def call (self , message ):
76
81
"""The command was called"""
77
82
#this command is exclusive, and the user does not have the required badge
78
- if self .exclusive and not (True in [badge .slug in self .allowed_badges for badge in message .user .badges ]):
79
- self .actor .send_message (f"@{ message .user .username } That command is exclusive to: " + ", " .join (self .allowed_badges ))
83
+ if self .exclusive and \
84
+ not (True in [badge .slug in self .allowed_badges for badge in message .user .badges ]):
85
+
86
+ self .actor .send_message (f"@{ message .user .username } That command is exclusive to: " +
87
+ ", " .join (self .allowed_badges )
88
+ )
89
+
80
90
return
81
91
82
92
#The command is still on cooldown
83
93
if (curtime := time .time ()) - self .last_use_time < self .cooldown :
84
- self .actor .send_message (f"@{ message .user .username } That command is still on cooldown. Try again in { int (self .last_use_time + self .cooldown - curtime + 0.5 )} seconds." )
94
+ self .actor .send_message (
95
+ f"@{ message .user .username } That command is still on cooldown. " +
96
+ f"Try again in { int (self .last_use_time + self .cooldown - curtime + 0.5 )} seconds."
97
+ )
85
98
86
99
return
87
100
88
101
#the user did not pay enough for the command and they do not have a free pass
89
- if message .rant_price_cents < self .amount_cents and not (True in [badge .slug in self .whitelist_badges for badge in message .user .badges ]):
90
- self .actor .send_message (f"@{ message .user .username } That command costs ${ self .amount_cents / 100 :.2f} ." )
102
+ if message .rant_price_cents < self .amount_cents and \
103
+ not (True in [badge .slug in self .whitelist_badges for badge in message .user .badges ]):
104
+
105
+ self .actor .send_message ("@" + message .user .username +
106
+ f"That command costs ${ self .amount_cents / 100 :.2f} ."
107
+ )
91
108
return
92
109
93
110
#the command was called successfully
@@ -103,7 +120,9 @@ def run(self, message):
103
120
return
104
121
105
122
#Run method was never defined
106
- self .actor .send_message (f"@{ message .user .username } Hello, this command never had a target defined. :-)" )
123
+ self .actor .send_message ("@" + message .user .username +
124
+ "Hello, this command never had a target defined. :-)"
125
+ )
107
126
108
127
class RumbleChatActor ():
109
128
"""Actor that interacts with Rumble chat"""
@@ -116,7 +135,7 @@ def __init__(self, stream_id = None, init_message = "Hello, Rumble world!", prof
116
135
streamer_username: The username of the person streaming
117
136
streamer_channel: The channel doing the livestream
118
137
is_channel_stream: If the livestream is on a channel or not
119
- ignore_users: List of usernames, will ignore all their messages, useful if you are using TheRumbleBot """
138
+ ignore_users: List of usernames, will ignore all their messages"""
120
139
121
140
#The info of the person streaming
122
141
self .__streamer_username = streamer_username
@@ -133,7 +152,8 @@ def __init__(self, stream_id = None, init_message = "Hello, Rumble world!", prof
133
152
if stream_id :
134
153
self .stream_id , self .stream_id_b10 = utils .stream_id_36_and_10 (stream_id )
135
154
136
- #It is not our livestream or we have no Live Stream API, LS API functions are not available
155
+ #It is not our livestream or we have no Live Stream API,
156
+ #so LS API functions are not available
137
157
if not self .rum_api or self .stream_id not in self .rum_api .livestreams :
138
158
self .api_stream = None
139
159
@@ -173,17 +193,29 @@ def __init__(self, stream_id = None, init_message = "Hello, Rumble world!", prof
173
193
#We have credentials
174
194
if username and password :
175
195
sign_in_buttn .click ()
176
- time .sleep (BROWSER_ACTION_DELAY )
177
- self .browser .find_element (By .ID , "login-username" ).send_keys (username + Keys .RETURN )
196
+ WebDriverWait (self .browser , BROWSER_WAIT_TIMEOUT ).until (
197
+ EC .visibility_of_element_located (By .ID , "login-username" ),
198
+ "Timed out waiting for sign-in dialouge"
199
+ )
200
+
201
+ uname_field = self .browser .find_element (By .ID , "login-username" )
202
+ uname_field .send_keys (username + Keys .RETURN )
178
203
self .browser .find_element (By .ID , "login-password" ).send_keys (password + Keys .RETURN )
179
- break #We only need to do that once
180
204
181
205
#We do not have credentials, ask for manual sign in
182
206
self .browser .maximize_window ()
183
207
input ("Please log in at the browser, then press enter here." )
184
208
185
- #Wait for signed in loading to complete
186
- time .sleep (BROWSER_ACTION_DELAY )
209
+ #Wait for signed in loading to complete
210
+ WebDriverWait (self .browser , BROWSER_WAIT_TIMEOUT ).until (
211
+ EC .invisibility_of_element (uname_field ),
212
+ "Timeout waiting for username field to disappear"
213
+ )
214
+
215
+ WebDriverWait (self .browser , BROWSER_WAIT_TIMEOUT ).until (
216
+ EC .visibility_of_element_located (By .ID , "chat-message-text-input" ),
217
+ "Timed out waiting for chat message field to appear"
218
+ )
187
219
188
220
#Find our username
189
221
if username :
@@ -198,9 +230,6 @@ def __init__(self, stream_id = None, init_message = "Hello, Rumble world!", prof
198
230
#Ignore these users when processing messages
199
231
self .ignore_users = ignore_users
200
232
201
- #Wait for potential further page load?
202
- time .sleep (BROWSER_ACTION_DELAY )
203
-
204
233
#History of the bot's messages so they do not get loop processed
205
234
self .sent_messages = []
206
235
@@ -224,9 +253,11 @@ def __init__(self, stream_id = None, init_message = "Hello, Rumble world!", prof
224
253
while (m := self .ssechat .get_message ()).user .username != self .username :
225
254
pass
226
255
227
- assert "moderator" in m .user .badges or "admin" in m .user .badges , "Actor cannot function without being a moderator"
256
+ assert "moderator" in m .user .badges or "admin" in m .user .badges , \
257
+ "Actor cannot function without being a moderator"
228
258
229
- #Functions that are to be called on each message, must return False if the message was deleted
259
+ #Functions that are to be called on each message,
260
+ #must return False if the message was deleted
230
261
self .message_actions = []
231
262
232
263
#Instances of RumbleChatCommand, by name
@@ -252,7 +283,10 @@ def streamer_channel(self):
252
283
#We are the ones streaming, and the API URL is under the channel
253
284
if self .api_stream and self .rum_api .channel_name :
254
285
self .__streamer_channel = self .rum_api .channel_name
255
- #We are not the ones streaming, or the API URL was not under our channel, and we can confirm this is a channel stream
286
+
287
+ #We are not the ones streaming,
288
+ #or the API URL was not under our channel,
289
+ #and we are sure this is a channel stream
256
290
else :
257
291
self .__streamer_channel = input ("Enter the channel of the person streaming: " )
258
292
@@ -307,7 +341,9 @@ def _sender_loop(self):
307
341
308
342
def __send_message (self , text ):
309
343
"""Send a message in chat"""
310
- assert len (text ) < MAX_MESSAGE_LEN , f"Message with prefix cannot be longer than { MAX_MESSAGE_LEN } characters"
344
+ assert len (text ) < MAX_MESSAGE_LEN , \
345
+ f"Message with prefix cannot be longer than { MAX_MESSAGE_LEN } characters"
346
+
311
347
self .sent_messages .append (text )
312
348
self .last_message_send_time = time .time ()
313
349
self .browser .find_element (By .ID , "chat-message-text-input" ).send_keys (text + Keys .RETURN )
@@ -327,12 +363,20 @@ def open_moderation_menu(self, message):
327
363
#Find the message by ID
328
364
elif isinstance (message , int ):
329
365
message_id = message
330
- message_li = self .browser .find_element (By .XPATH , f"//li[@class='chat-history--row js-chat-history-item'][@data-message-id='{ message_id } ']" )
366
+ message_li = self .browser .find_element (
367
+ By .XPATH ,
368
+ "//li[@class='chat-history--row js-chat-history-item']" +
369
+ f"[@data-message-id='{ message_id } ']"
370
+ )
331
371
332
372
#The message has a message ID attribute
333
373
elif hasattr (message , "message_id" ):
334
374
message_id = message .message_id
335
- message_li = self .browser .find_element (By .XPATH , f"//li[@class='chat-history--row js-chat-history-item'][@data-message-id='{ message_id } ']" )
375
+ message_li = self .browser .find_element (
376
+ By .XPATH ,
377
+ "//li[@class='chat-history--row js-chat-history-item']" +
378
+ f"[@data-message-id='{ message_id } ']"
379
+ )
336
380
337
381
#Not a valid message type
338
382
else :
@@ -341,7 +385,10 @@ def open_moderation_menu(self, message):
341
385
#Hover over the message
342
386
self .hover_element (message_li )
343
387
#Find the moderation menu
344
- menu_bttn = self .browser .find_element (By .XPATH , f"//li[@class='chat-history--row js-chat-history-item'][@data-message-id='{ message_id } ']/button[@class='js-moderate-btn chat-history--kebab-button']" )
388
+ menu_bttn = message_li .find_element (
389
+ By .XPATH ,
390
+ ".//button[@class='js-moderate-btn chat-history--kebab-button']"
391
+ )
345
392
#Click the moderation menu button
346
393
menu_bttn .click ()
347
394
@@ -350,23 +397,40 @@ def open_moderation_menu(self, message):
350
397
def delete_message (self , message ):
351
398
"""Delete a message in the chat"""
352
399
m_id = self .open_moderation_menu (message )
353
- del_bttn = self .browser .find_element (By .XPATH , f"//button[@class='cmi js-btn-delete-current'][@data-message-id='{ m_id } ']" )
400
+ del_bttn = self .browser .find_element (
401
+ By .XPATH ,
402
+ f"//button[@class='cmi js-btn-delete-current'][@data-message-id='{ m_id } ']"
403
+ )
404
+
354
405
del_bttn .click ()
355
- time .sleep (BROWSER_ACTION_DELAY )
406
+
407
+ #Wait for the confirmation to appear
408
+ WebDriverWait (self .browser , BROWSER_WAIT_TIMEOUT ).until (
409
+ EC .alert_is_present (),
410
+ "Timed out waiting for deletion confirmation dialouge to appear"
411
+ )
356
412
357
413
#Confirm the confirmation dialog
358
414
Alert (self .browser ).accept ()
359
415
360
416
def mute_by_message (self , message , mute_level = "5" ):
361
417
"""Mute a user by message"""
362
418
self .open_moderation_menu (message )
363
- timeout_bttn = self .browser .find_element (By .XPATH , f"//button[@class='{ MUTE_LEVELS [mute_level ]} ']" )
419
+ timeout_bttn = self .browser .find_element (
420
+ By .XPATH ,
421
+ f"//button[@class='{ MUTE_LEVELS [mute_level ]} ']"
422
+ )
423
+
364
424
timeout_bttn .click ()
365
425
366
426
def mute_by_appearname (self , name , mute_level = "5" ):
367
427
"""Mute a user by the name they are appearing with"""
368
428
#Find any chat message by this user
369
- message_li = self .browser .find_element (By .XPATH , f"//li[@class='chat-history--row js-chat-history-item'][@data-username='{ name } ']" )
429
+ message_li = self .browser .find_element (
430
+ By .XPATH ,
431
+ f"//li[@class='chat-history--row js-chat-history-item'][@data-username='{ name } ']"
432
+ )
433
+
370
434
self .mute_by_message (message = message_li , mute_level = mute_level )
371
435
372
436
def pin_message (self , message ):
@@ -378,7 +442,11 @@ def pin_message(self, message):
378
442
def unpin_message (self ):
379
443
"""Unpin the currently pinned message"""
380
444
try :
381
- unpin_bttn = self .browser .find_element (By .XPATH , "//button[@data-js='remove_pinned_message_button']" )
445
+ unpin_bttn = self .browser .find_element (
446
+ By .XPATH ,
447
+ "//button[@data-js='remove_pinned_message_button']"
448
+ )
449
+
382
450
except selenium .common .exceptions .NoSuchElementException :
383
451
return False #No message was pinned
384
452
@@ -412,7 +480,8 @@ def register_command(self, command, name = None):
412
480
"""Register a command"""
413
481
#Is a ChatCommand instance
414
482
if isinstance (command , ChatCommand ):
415
- assert not name or name == command .name , "ChatCommand instance has different name than one passed"
483
+ assert not name or name == command .name , \
484
+ "ChatCommand instance has different name than one passed"
416
485
self .chat_commands [command .name ] = command
417
486
418
487
#Is a callable
@@ -435,7 +504,7 @@ def __process_message(self, message):
435
504
if message .text in self .sent_messages :
436
505
return
437
506
438
- #If the message is from the same account as us, reset our message cooldown from the message send time if it is newer
507
+ #If the message is from the same account as us, consider it in message send cooldown
439
508
if message .user .username == self .username :
440
509
self .last_message_send_time = max ((self .last_message_send_time , message .time ))
441
510
@@ -444,7 +513,8 @@ def __process_message(self, message):
444
513
return
445
514
446
515
for action in self .message_actions :
447
- if message .message_id in self .ssechat .deleted_message_ids or not action (message , self ): #The message got deleted, possibly by this action
516
+ #The message got deleted, possibly by this action
517
+ if message .message_id in self .ssechat .deleted_message_ids or not action (message , self ):
448
518
return
449
519
450
520
self .__run_if_command (message )
0 commit comments