91ca98302770dc555097e05caef9c29559fee53a
[lolcam] / chatbot / bot.py
1 # __ ____ __
2 # | | | _ | | |
3 # | :_ | '-'| | :_ [ @ m 📸
4 # '----' '----' '----' ````````
5 #
6 # +--------+
7 # | bot.py |
8 # +--------+
9 #
10 # LOLCAM📸 Twitch API chatbot listens for commands in the LOLCAM chat. Commands can be
11 # submitted to control parts of the room seen on camera, for example to drive the robots
12 # in the room.
13 #
14 # +----------+
15 # | COMMANDS |
16 # +----------+
17 #
18 # !halp ....... halp bot is here to halp
19 # !goodboy .... become goodboy
20 # !badboy ..... become badboy
21 # !flip ....... change window scene
22 # !party ...... a light show
23 # !ham ........ flash the ham sign
24 # !lamp ....... turn lamp on/off
25 # !fish ....... activate fish
26 # !fire ....... fireplace
27 # !dance ...... make halpbot dance
28 # !coom ....... keanu's wish
29 # !who ........ who is driving
30 #
31 # +------------------+
32 # | !goodboy/!badboy |
33 # +------------------+
34 #
35 # forward ..... !advance !charge !inch
36 # backward .... !shrink !retreat !scooch
37 # turning ..... !turn !hang !swerve !180 !90 !45
38 # moves ....... !walk !spin !worm !pose !donut !ring !drill !bash !wheels !star !hand
39 # control ..... !swap
40 #
41
42 import os, asyncio, threading, subprocess, time, twitchio, json
43 from twitchio.ext import commands
44
45 bot = commands.Bot(
46 irc_token=os.environ['TMI_TOKEN'],
47 client_id=os.environ['CLIENT_ID'],
48 nick=os.environ['BOT_NICK'],
49 prefix=os.environ['BOT_PREFIX'],
50 initial_channels=[os.environ['CHANNEL']],
51 )
52
53 driver=None
54 goodboy=None
55 badboy=None
56 greendriver=None
57 yellowdriver=None
58 timeLimit=180.0
59
60 @bot.event
61 async def event_ready():
62 'Called once when the bot goes online.'
63 print(f"{os.environ['BOT_NICK']} is online!")
64 ws = bot._ws #only needed to send messages within event_ready
65 await ws.send_privmsg(os.environ['CHANNEL'], f"/me is available to halp └[ ∵ ]┘")
66
67 @bot.event
68 async def event_message(ctx):
69 'Runs every time a message is sent in chat.'
70
71 #make sure the bot ignores itself and streamer
72
73 if ctx.author.name.lower() == os.environ['BOT_NICK'].lower():
74 return
75
76 await bot.handle_commands(ctx)
77
78 @bot.command(name='test')
79 async def test(ctx):
80 await ctx.send('test passed!')
81
82 @bot.command(name='who')
83 async def who(ctx):
84 message = ""
85 if goodboy is None:
86 message += "goodboy is free"
87 else:
88 message += f"{goodboy} is goodboy"
89 message += " and "
90 if badboy is None:
91 message += "badboy is free"
92 else:
93 message += f"{badboy} is badboy"
94 await ctx.send(message)
95
96 @bot.command(name="goodboy")
97 async def be_cum_goodboy(ctx):
98 if ctx.author.name == badboy:
99 await ctx.send(f"you are already badboy")
100 else:
101 await you_can_be_cum(ctx, "goodboy")
102
103 @bot.command(name="badboy")
104 async def be_cum_badboy(ctx):
105 if ctx.author.name == goodboy:
106 await ctx.send(f"you are already goodboy")
107 else:
108 await you_can_be_cum(ctx, "badboy")
109
110 async def you_can_be_cum(ctx, requested):
111 global goodboy
112 global badboy
113 print(f"{ctx.author.name} is requesting {requested}")
114 if requested == "goodboy":
115 current_user = goodboy
116 elif requested == "badboy":
117 current_user = badboy
118 if current_user == ctx.author.name:
119 await ctx.send(f"you are already {requested}")
120 elif current_user == None:
121 if requested == "goodboy":
122 goodboy = ctx.author.name
123 elif requested == "badboy":
124 badboy = ctx.author.name
125 ws = bot._ws
126 print(f"wait for it...")
127 await asyncio.sleep(1)
128 asyncio.create_task(ctx.send(f"{ctx.author.name} is {requested} for {int(timeLimit / 60)} minutes (!halp for commands)"))
129 print(f"wait for it again...")
130 await asyncio.sleep(timeLimit)
131 asyncio.create_task(ctx.send(f"{ctx.author.name} is no longer {requested}"))
132 if requested == "goodboy":
133 goodboy = None
134 elif requested == "badboy":
135 badboy = None
136 print(f"{requested} reset")
137 else:
138 await ctx.send(f"{current_user} is already {requested}")
139
140 @bot.command(name='greenbot')
141 async def greenbot(ctx):
142 global greendriver
143 if greendriver == ctx.author.name:
144 await ctx.send(f"you are already the greenbot")
145 elif greendriver == None:
146 greendriver = ctx.author.name
147 ws=bot._ws
148 print(f"wait for it...")
149 await asyncio.sleep(1)
150 asyncio.create_task(ctx.send(f"{greendriver} is the greenbot for {int(timeLimit / 60)} minutes"))
151 print(f"wait for it again...")
152 await asyncio.sleep(timeLimit)
153 asyncio.create_task(ctx.send(f"{greendriver} is no longer the greenbot"))
154 greendriver = None
155 print(f"greendriver reset")
156 else:
157 await ctx.send(f"{greendriver} is already the greenbot")
158
159 @bot.command(name='yellowbot')
160 async def yellowbot(ctx):
161 global yellowdriver
162 if yellowdriver == ctx.author.name:
163 await ctx.send(f"you are already the yellowbot")
164 elif yellowdriver == None:
165 yellowdriver = ctx.author.name
166 ws=bot._ws
167 print(f"wait for it...")
168 await asyncio.sleep(1)
169 asyncio.create_task(ctx.send(f"{yellowdriver} is the yellowbot for {int(timeLimit / 60)} minutes"))
170 print(f"wait for it again...")
171 await asyncio.sleep(timeLimit)
172 asyncio.create_task(ctx.send(f"{yellowdriver} is no longer the yellowbot"))
173 yellowdriver = None
174 print(f"yellowdriver reset")
175 else:
176 await ctx.send(f"{yellowdriver} is already the yellowbot")
177
178 @bot.command(name='dance')
179 async def dance(ctx):
180 await ctx.send(f"└[∵┌]└[ ∵ ]┘[┐∵]┘")
181
182 @bot.command(name='coom')
183 async def coom(ctx):
184 await ctx.send(f"ok keanu")
185
186 @bot.command(name='poll')
187 async def poll(ctx):
188 poll_status = get_poll_status()
189 author = ctx.author
190 if not poll_status["is_active"]:
191 is_authorized = await is_author_authorized(author, ctx.channel)
192 if is_authorized:
193 poll_status["is_active"] = True
194 poll_status["progress_on_inactive"] = True
195 for key in poll_status["choices"]:
196 poll_status["choices"][key] = 0
197 await write_poll_status(poll_status)
198 await ctx.send(f"[┐∵]┘ send !vote [A/B] to choose path")
199 else:
200 await ctx.send(f"[┐∵]┘ excuse me {author.name}, only admin can start the poll")
201 else:
202 await ctx.send(f"[┐∵]┘ excuse me {author.name}, poll is already in progress")
203
204 def get_poll_status():
205 with open("poll_status.json", "rt") as poll_status_file:
206 return json.load(poll_status_file)
207
208 @bot.command(name='close')
209 async def close_poll(ctx):
210 poll_status = get_poll_status()
211 author = ctx.author
212 if poll_status["is_active"]:
213 is_authorized = await is_author_authorized(author, ctx.channel)
214 if is_authorized:
215 poll_status["is_active"] = False
216 winner = None
217 for key in sorted(poll_status["choices"].keys()):
218 if winner is None or poll_status["choices"][key] > poll_status["choices"][winner]:
219 winner = key
220 await write_poll_status(poll_status)
221 await ctx.send(f"[┐∵]┘ {winner} is the winner of the poll")
222 else:
223 await ctx.send(f"[┐∵]┘ excuse me {author.name}, only admin can start the poll")
224 else:
225 await ctx.send(f"[┐∵]┘ excuse me {author.name}, poll is already closed")
226
227 async def write_poll_status(poll_status):
228 with open("poll_status.json", "wt") as poll_status_file:
229 json.dump(poll_status, poll_status_file, indent=4)
230
231 async def is_author_authorized(author, channel):
232 chatters = await bot.get_chatters(str(channel))
233 return author.name in chatters.broadcaster or author.name in chatters.admins
234
235 @bot.command(name='vote')
236 async def vote(ctx, choice):
237 poll_status = get_poll_status()
238 author = ctx.author.name
239 if not poll_status["is_active"]:
240 await ctx.send(f"[┐∵]┘ excuse me {author}, voting is not active")
241 else:
242 choice = choice.strip().lower()
243 if choice in poll_status["choices"].keys():
244 poll_status["choices"][choice] += 1
245 await write_poll_status(poll_status)
246 print(f"poll status is {poll_status}")
247 else:
248 await ctx.send(f"[┐∵]┘ excuse me {author}, {choice} is not a valid choice")
249
250 @bot.command(name='halp')
251 async def halp(ctx, command_name=""):
252 messages = {
253 "halp": "type !halp to see commands or !halp [command] to get info about a command",
254 "who": "find out who is controlling the robots",
255 "goodboy": (
256 "take control of goodboy and do 🤖 !advance !charge !inch !shrink !retreat !scooch !turn"
257 " !hang !swerve !180 !90 !45 !walk !spin !worm !pose !donut !ring !drill !bash !wheels"
258 " !star !hand !swap"),
259 "badboy": (
260 "take control of badboy and do 🤖 !advance !charge !inch !shrink !retreat !scooch !turn"
261 " !hang !swerve !180 !90 !45 !walk !spin !worm !pose !donut !ring !drill !bash !wheels"
262 " !star !hand !swap"),
263 "flip": "change channel on apartment window monitor",
264 "party": "flash the party lights for 30 seconds",
265 "ham": "flash the ham sign for 30 seconds",
266 "lamp": "turn lamp on/off",
267 "fish": "activate the fish",
268 "dance": "I will do a dance",
269 "advance": "drive forward a bit",
270 "retreat": "drive backward a bit",
271 "turn": "rotate robot 90 degrees clockwise",
272 "hang": "make a right turn",
273 "swerve": "make a left turn",
274 "walk": "slowly walk forward",
275 "spin": "do a 360 spin",
276 "worm": "wiggle the robot",
277 "pose": "turn to every direction and pose briefly",
278 "donut": "spin around aggressively",
279 "ring": "drive a full circle",
280 "drill": "thrust forward and back quickly like a drill",
281 "bash": "drive into something, reverse, and repeat",
282 "wheels": "do a sick wheelie",
283 "star": "do a figure that looks like a star",
284 "hand": "open or close the hand",
285 "swap": "!swap [user] will swap in another user in chat",
286 "vote": "use !vote [first/second] to choose the first or second choice when prompted",
287 }
288 command_name = command_name.strip("!")
289 if command_name == "" or command_name not in messages.keys():
290 if ctx.author.name in (goodboy, badboy):
291 await ctx.send((
292 "!advance !charge !inch !shrink !retreat !scooch !turn !hang !swerve !180 !90 !45 "
293 "!walk !spin !worm !pose !donut !ring !drill !bash !wheels !star !hand !swap 🤖 "
294 "try submitting commands together (like !90 !advance !drill) 📝 !halp [command] for more info"))
295 else:
296 await ctx.send("use !goodboy !badboy !flip !party !ham !dance !who !vote 📝 !halp [command] for more info")
297 else:
298 await ctx.send(messages[command_name])
299
300 @bot.command(name='swap')
301 async def swap(ctx, replacement=None):
302 global goodboy, badboy
303 if ctx.author.name not in (goodboy, badboy):
304 await ctx.send(f"only driver can swap out")
305 elif replacement is None:
306 await ctx.send(f"please specify replacement name")
307 else:
308 replacement = replacement.strip().lower()
309 chatters = await bot.get_chatters(str(ctx.channel))
310 print(chatters.all)
311 if replacement == bot.nick:
312 await ctx.send("lol")
313 elif replacement == ctx.author.name:
314 await ctx.send(f"aren't you {replacement}?")
315 elif replacement not in chatters.all:
316 await ctx.send(f"couldn't find {replacement}")
317 elif goodboy == ctx.author.name:
318 goodboy = replacement
319 await ctx.send(f"{replacement} swapped in for goodboy")
320 elif badboy == ctx.author.name:
321 badboy = replacement
322 await ctx.send(f"{replacement} swapped in for badboy")
323
324 @bot.command(name='fish')
325 async def fish(ctx):
326 # subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","mode","05","out"])
327 # subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","05","1"])
328 # time.sleep(1.0)
329 # subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","05","0"])
330 # await ctx.send(f"fishy go brr")
331 await ctx.send(f"fish not currently available")
332
333 @bot.command(name='flip')
334 async def flip(ctx):
335 print("changing the channel...")
336 subprocess.run(["ssh", "partywagon", "-x", "echo", "flip", ">", "/home/pi/lolcam/babycastles_tv/command.txt"])
337
338 @bot.command(name='party')
339 async def party(ctx):
340 await timed_switch(ctx, 5)
341 #await ctx.send(f"party lights are disabled during sports events")
342
343 @bot.command(name='ham')
344 async def ham(ctx):
345 await timed_switch(ctx, 2)
346
347 async def timed_switch(ctx, switch_id):
348 proc = await asyncio.create_subprocess_shell(
349 "wemo status", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
350 stdout, stderr = await proc.communicate()
351 print(stdout)
352 for line in stdout.split(b"\n"):
353 fields = line.split()
354 if len(fields) == 3:
355 heading, switch_name, status = [field.decode() for field in fields]
356 if switch_name.endswith(str(switch_id)):
357 if status == "0":
358 print(f"turning on {switch_name}")
359 await asyncio.create_subprocess_shell(f"wemo switch {switch_name} on")
360 await asyncio.sleep(30)
361 print(f"turning off {switch_name}")
362 await asyncio.create_subprocess_shell(f"wemo switch {switch_name} off")
363 else:
364 print(f"turning off {switch_name}")
365 await asyncio.create_subprocess_shell(f"wemo switch {switch_name} off")
366
367 #
368 # BEAR COMMANDS:
369 #
370 # must become a bear using !goodboy or !badboy to use these commands
371 #
372
373 @bot.command(name='star')
374 async def star(ctx):
375 await robot(ctx, "star")
376
377 @bot.command(name='wheels')
378 async def wheels(ctx):
379 await robot(ctx, "wheels")
380
381 @bot.command(name='bash')
382 async def bash(ctx):
383 await robot(ctx, "bash")
384
385 @bot.command(name='drill')
386 async def drill(ctx):
387 await robot(ctx, "drill")
388
389 @bot.command(name='ring')
390 async def ring(ctx):
391 await robot(ctx, "ring")
392
393 @bot.command(name='donut')
394 async def donut(ctx):
395 await robot(ctx, "donut")
396
397 @bot.command(name='pose')
398 async def pose(ctx):
399 await robot(ctx, "pose")
400
401 @bot.command(name='worm')
402 async def worm(ctx):
403 await robot(ctx, "worm")
404
405 @bot.command(name='spin')
406 async def spin(ctx):
407 await robot(ctx, "spin")
408
409 @bot.command(name='walk')
410 async def walk(ctx):
411 await robot(ctx, "walk")
412
413 @bot.command(name='swerve')
414 async def swerve(ctx):
415 await robot(ctx, "swerve")
416
417 @bot.command(name='hang')
418 async def hang(ctx):
419 await robot(ctx, "hang")
420
421 @bot.command(name='turn')
422 async def turn(ctx):
423 await robot(ctx, "turn")
424
425 @bot.command(name='advance')
426 async def advance(ctx):
427 await robot(ctx, "advance")
428
429 @bot.command(name='retreat')
430 async def retreat(ctx):
431 await robot(ctx, "retreat")
432
433 @bot.command(name='hand')
434 async def hand(ctx):
435 await robot(ctx, "hand")
436
437 @bot.command(name='charge')
438 async def charge(ctx):
439 await robot(ctx, "charge")
440
441 @bot.command(name='shrink')
442 async def shrink(ctx):
443 await robot(ctx, "shrink")
444
445 @bot.command(name='inch')
446 async def inch(ctx):
447 await robot(ctx, "inch")
448
449 @bot.command(name='scooch')
450 async def scooch(ctx):
451 await robot(ctx, "scooch")
452
453 @bot.command(name='45')
454 async def rot_45(ctx):
455 await robot(ctx, "45")
456
457 @bot.command(name='90')
458 async def rot_90(ctx):
459 await robot(ctx, "90")
460
461 @bot.command(name='180')
462 async def rot_180(ctx):
463 await robot(ctx, "180")
464
465 async def robot(ctx, first_command):
466 if ctx.author.name not in (goodboy, badboy):
467 if goodboy is None:
468 await ctx.send(f"use !goodboy to become goodboy first")
469 elif badboy is None:
470 await ctx.send(f"use !badboy to become badboy first")
471 else:
472 await ctx.send(f"{goodboy} is goodboy and {badboy} is badboy")
473 else:
474 if ctx.author.name == goodboy:
475 robot_id = 0
476 else:
477 robot_id = 1
478 commands = ctx.content.lower().split()
479 for ii, command in enumerate(commands):
480 if command.startswith("!"):
481 move = command[1:]
482 if move in ("advance", "retreat", "turn", "hang", "swerve", "walk", "spin", "worm", "pose", "donut",
483 "ring", "drill", "bash", "wheels", "star", "hand", "charge", "shrink", "inch", "scooch", "45", "90", "180"):
484 subprocess_command = ["python", os.path.join("..", "send_command.py"), move, "--robot", str(robot_id)]
485 if ii < len(commands) - 1:
486 subprocess_command.append("--chain")
487 print(f"sending {subprocess_command}...")
488 subprocess.run(subprocess_command)
489 else:
490 await ctx.send(f"{command} is not a supported robot move")
491 if len(commands) > 1 and ii == len(commands) - 1:
492 subprocess_command = ["python", os.path.join("..", "send_command.py"), "advance", "--robot", str(robot_id)]
493 print(f"sending dummy {subprocess_command}...")
494 subprocess.run(subprocess_command)
495
496 #stuff for boxing robots
497
498 @bot.command(name='greenleft')
499 async def greenleft(ctx):
500 if ctx.author.name==greendriver:
501 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","21","1",";","gpio","-g","write","19","1"])
502 #print(f"on")
503 time.sleep(0.01)
504 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","21","0",";","gpio","-g","write","19","0"])
505 #print(f"off")
506 await ctx.send(f"greenbot turned left")
507 else:
508 await ctx.send(f"you are not the greendriver")
509
510 @bot.command(name='greenright')
511 async def greenright(ctx):
512 if ctx.author.name==greendriver:
513 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","06","1",";","gpio","-g","write","20","1"])
514 #print(f"on")
515 time.sleep(0.01)
516 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","06","0",";","gpio","-g","write","20","0"])
517 #print(f"off")
518 await ctx.send(f"greenbot turned right")
519 else:
520 await ctx.send(f"you are not the greendriver")
521
522 @bot.command(name='greenfwd')
523 async def greenfwd(ctx):
524 if ctx.author.name==greendriver:
525 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","06","1",";","gpio","-g","write","21","1"])
526 #print(f"on")
527 time.sleep(0.01)
528 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","06","0",";","gpio","-g","write","21","0"])
529 #print(f"off")
530 await ctx.send(f"greenbot advanced")
531 else:
532 await ctx.send(f"you are not the greendriver")
533
534 @bot.command(name='greenback')
535 async def greenback(ctx):
536 if ctx.author.name==greendriver:
537 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","19","1",";","gpio","-g","write","20","1"])
538 #print(f"on")
539 time.sleep(0.01)
540 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","19","0",";","gpio","-g","write","20","0"])
541 #print(f"off")
542 await ctx.send(f"greenbot retreated")
543 else:
544 await ctx.send(f"you are not the greendriver")
545
546 @bot.command(name='yellowleft')
547 async def yellowleft(ctx):
548 if ctx.author.name==yellowdriver:
549 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","12","1",";","gpio","-g","write","16","1"])
550 #print(f"on")
551 time.sleep(0.01)
552 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","12","0",";","gpio","-g","write","16","0"])
553 #print(f"off")
554 await ctx.send(f"yellowbot turned left")
555 else:
556 await ctx.send(f"you are not the yellowdriver")
557
558 @bot.command(name='yellowright')
559 async def yellowright(ctx):
560 if ctx.author.name==yellowdriver:
561 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","13","1",";","gpio","-g","write","26","1"])
562 #print(f"on")
563 time.sleep(0.01)
564 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","13","0",";","gpio","-g","write","26","0"])
565 #print(f"off")
566 await ctx.send(f"yellowbot turned right")
567 else:
568 await ctx.send(f"you are not the yellowdriver")
569
570 @bot.command(name='yellowfwd')
571 async def yellowfwd(ctx):
572 if ctx.author.name==yellowdriver:
573 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","13","1",";","gpio","-g","write","12","1"])
574 #print(f"on")
575 time.sleep(0.01)
576 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","13","0",";","gpio","-g","write","12","0"])
577 #print(f"off")
578 await ctx.send(f"yellowbot advanced")
579 else:
580 await ctx.send(f"you are not the yellowdriver")
581
582 @bot.command(name='yellowback')
583 async def yellowback(ctx):
584 if ctx.author.name==yellowdriver:
585 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","16","1",";","gpio","-g","write","26","1"])
586 #print(f"on")
587 time.sleep(0.01)
588 subprocess.run(["ssh","pi@partywagon","-x","gpio","-g","write","16","0",";","gpio","-g","write","26","0"])
589 #print(f"off")
590 await ctx.send(f"yellowbot retreated")
591 else:
592 await ctx.send(f"you are not the yellowdriver")
593
594 if __name__ == "__main__":
595 bot.run()