import discord from datetime import datetime from gtts import gTTS from forex_python.converter import CurrencyRates from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from random import randint import asyncio import json import time import hashlib import os.path ROOT = "/home/bot/ticker" with open(ROOT + "/config.json") as file: conf = json.load(file) # guilds SEGUIS = conf['guilds']['seguis'] GOONS = conf['guilds']['goons'] # users MATT = conf['users']['matt'] DAD = conf['users']['dad'] CHEW = conf['users']['chew'] # channels SMOKEYS = conf['channels']['smokeys'] RETARD = conf['channels']['retard_dimension'] GHETTO = conf['channels']['league_ghetto'] client = discord.Client() c = CurrencyRates() s = AsyncIOScheduler(event_loop = client.loop) ticker_queue = asyncio.Queue() # returns a file path from the cache def to_tts(message, lang = 'ja', cache = True): # the file we save to is based on the hash of the message file_name = ROOT + "/dqn.mp3" if cache: message_hash = hashlib.md5(message.encode('utf-8')).hexdigest() file_name = ROOT + "/cache/" + message_hash + ".mp3" print(message) # the hash lets us cache if not os.path.isfile(file_name) or not cache: print("file does not exist, cacheing") tts = gTTS(message, lang = lang) tts.save(file_name) return file_name # announcer loop async def ticker_update(): while True: await client.wait_until_ready() # blocks until there is an event waiting in the queue announce_file, target_voice_channel, after = await ticker_queue.get() print("ticker event has been removed from queue") # there are three scenarios here # 1. we are not connected to a voice channel. we should CONNECT to the target voice channel # 2. we are connected to the wrong channel. we should MOVE to the target channel # 3. we are already in the correct channel. we should do nothing guild = target_voice_channel.guild voice_client = guild.voice_client # may be none # connect vc if not (voice_client and voice_client.is_connected()): voice_client = await target_voice_channel.connect() # switch channels if not target_voice_channel.id == voice_client.channel.id: await voice_client.move_to(target_voice_channel) source = discord.FFmpegOpusAudio(announce_file) voice_client.play(source) # block while the message is playing while voice_client.is_playing(): await asyncio.sleep(0.1) if after: await after(target_voice_channel) # the queue being empty indicates all events completed if ticker_queue.empty(): await voice_client.disconnect() @client.event async def on_ready(): print(client.user.name) # add afk message @client.event async def on_voice_state_update(member, before, after): if member.id == MATT and after.channel and after.channel.id == SMOKEYS: await asyncio.sleep(randint(3, 30)) await member.move_to(client.get_channel(RETARD)) if member.id == CHEW and not before.channel: await add_event(to_tts("Hey everyone, the pineapple is here!", 'en'), after.channel) # only run on moves if before.channel and after.channel: # moves to the afk channel if member.guild.afk_channel and after.channel.id == member.guild.afk_channel.id: file_name = to_tts(member.display_name + "はretard dimensionに移動しました") await add_event(file_name, before.channel) # either matts channel, or the most active channel def get_best_channel(guild_id): guild = client.get_guild(guild_id) matt = discord.utils.find(lambda m: m.id == MATT, guild.members) # why the fuck does python still not have safe navigation operators? matt_voice = ((matt.voice.channel if matt.voice.channel else None) if matt.voice else None) if matt else None top_channel = sorted(guild.voice_channels, key = lambda chan: len(chan.members), reverse = True)[0] return matt_voice if matt_voice else top_channel # helper async def add_event(file_name, voice_channel, after = None): print("ticker event has been added to queue") await ticker_queue.put((file_name, voice_channel, after)) # decorator for simple scheduled announcements def update(guild, trigger): def decorator(func): async def wrapper(): voice_channel = get_best_channel(guild) await add_event(func(), voice_channel) s.add_job(wrapper, trigger) return wrapper return decorator @update(SEGUIS, CronTrigger(hour = '0-19,21-23')) def ticker(): return to_tts("The current JPY exchange rate is " + str(c.get_rate('USD', 'JPY')) + " yen to a dollar") @update(SEGUIS, CronTrigger(hour = '20')) def flatten(): return to_tts("Matt, it is time for your 4 PM dick flattening") @update(GOONS, CronTrigger(hour = '*', minute = '15')) def bussing(): BUS_START = 1587179700 since_start = time.time() - BUS_START hours_since_start = since_start / 60 / 60 file_name = to_tts("GIVE IT UP FOR " + str(int(hours_since_start)) + " HOURS SINCE DEPARTURE!! WOO!!", 'en', False) return file_name # ban league async def league_ghetto(): await client.wait_until_ready() # short circuiting ands are okay, but this is still hell. leaguers = filter(lambda member: member.activity and member.activity.type == discord.ActivityType.playing and member.activity.name == 'League of Legends' and member.voice and member.voice.channel and member.voice.channel.id != GHETTO, client.get_guild(SEGUIS).members) for member in leaguers: # using the id because i don't know the internals of this library and i dont care to check # its entirely possible that the same object is used after the move_to and if before_channel # referenced that then its in the bone zone. before_channel = member.voice.channel.id # why doesn't python have async lambdas yet? async def move(_): await member.move_to(client.get_channel(GHETTO)) file_name = to_tts(member.display_name + ", you have been found in violation of the No League Act of 2020." + " Administrative action will now be taken against your account.", lang = 'de') await add_event(file_name, client.get_channel(before_channel), move) client.loop.create_task(ticker_update()) s.add_job(league_ghetto, CronTrigger(minute = '*/5')) s.start() with open(ROOT + "/priv/token") as file: token = file.readline() client.run(token)