diff --git a/.gitignore b/.gitignore index a4db8fb..220d1fa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Config file with bot secret secret.py +config.json* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 6d2a78d..72135ee 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,12 @@ R.A.L.F. (short for Responsive Artificial Lifeform ;-) ) is the "housekeeping" bot on the oneMarcFifty Discord Server -It can do the following things (release version 0.2): +It can do the following things (release version 0.3): - reply with "pong" to a "ping" (WHOA!!!) - Send out configurable random messages when the server is idle (like "Did you know...") - automatically create Events (we have Sunday video sessions at 9 AM and 6 PM) - remind subscribers of upcoming events - -Planned (release 0.3) - - Help the user create a support thread with a modal view - let the user subscribe / unsubscribe to notification messages with a modal view diff --git a/bot_messages/SundayFunday.txt b/bot_messages/SundayFunday.txt index 47c4ba0..cd3b1e6 100644 --- a/bot_messages/SundayFunday.txt +++ b/bot_messages/SundayFunday.txt @@ -9,3 +9,6 @@ Talk about tech stuff with the other members or just listen in... ****Everyone is invited!**** + +Type **/subscribe** if you want to get notified half an hour before the conference starts. +you can use the same command to unsubscribe at any time - no questions asked ;-) \ No newline at end of file diff --git a/bot_messages/bot_commands.txt b/bot_messages/bot_commands.txt new file mode 100644 index 0000000..1a1354d --- /dev/null +++ b/bot_messages/bot_commands.txt @@ -0,0 +1,6 @@ +Hi, I am R.A.L.F., your assistant bot. + +You can interact with me by typing "/" and then a command: + +**/support** helps you open a support thread as described in the <#954732247909552149> channel +**/subscribe** subscribe to get notified half an hour before the Sunday Funday session starts \ No newline at end of file diff --git a/bot_messages/support threads.txt b/bot_messages/support threads.txt index c6c06d5..7174c7c 100644 --- a/bot_messages/support threads.txt +++ b/bot_messages/support threads.txt @@ -2,7 +2,7 @@ Hi, I am R.A.L.F., your assistant bot! Did you know that we have a ***support channel*** here ? -Please see the <#954732247909552149> channel on how to get hep on a tech issue. +Please see the <#954732247909552149> channel on how to get help on a tech issue. Alternatively, you can type /support and I will help you to create one. Please do also check in every now and then to the <#866779182293057566> channel. diff --git a/bot.py b/classes/bot.py similarity index 74% rename from bot.py rename to classes/bot.py index 815328e..3df7c61 100644 --- a/bot.py +++ b/classes/bot.py @@ -1,15 +1,21 @@ import discord -from discord import app_commands -from dis_events import DiscordEvents import random -from glob import glob import numpy as np -import config -from secret import BOT_TOKEN, CLIENT_ID, GUILD_ID import utils -from discord.ext import tasks import datetime +import config + +from glob import glob from dateutil import tz +from sys import exit + +from discord import app_commands +from discord.ext import tasks + +from classes.dis_events import DiscordEvents +from classes.support import Support +from classes.subscribe import Subscribe + # ####################################### # The OMFClient class @@ -21,8 +27,11 @@ class OMFClient(discord.Client): channel_idle_timer: int asked_question = False last_question: discord.Message = None - guild_Events = None lastNotifyTimeStamp = None + theGuild : discord.Guild = None + + guildEventsList = None + guildEventsClass: DiscordEvents = None # ####################################### # init constructor @@ -42,8 +51,39 @@ class OMFClient(discord.Client): self.tree = app_commands.CommandTree(self) + # The support command will ask for a thread title and description + # and create a support thread for us + + @self.tree.command(name="support", description="Create a support thread") + async def support(interaction: discord.Interaction): + x : Support + x= Support() + await interaction.response.send_modal(x) + + # The subscribe command will add/remove the notification roles + # based on the scheduled events + + @self.tree.command(name="subscribe", description="(un)subscribe to Events)") + async def subscribe(interaction: discord.Interaction): + + # preload the menu items with the roles that the user has already + # we might move this to the init of the modal + + x: Subscribe + role: discord.Role + member: discord.Member + + x=Subscribe() + member = interaction.user + + for option in x.Menu.options: + role = option.value + if not (member.get_role(role) is None): + option.default=True + await interaction.response.send_modal(x) + self.channel_idle_timer = 0 - self.idle_channel = self.get_channel(config.IDLE_MESSAGE_CHANNEL_ID) + self.idle_channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) # ######################### # setup_hook waits for the @@ -62,15 +102,14 @@ class OMFClient(discord.Client): async def send_random_message(self): print("Sending random message") if self.idle_channel == None: - self.idle_channel = self.get_channel(config.IDLE_MESSAGE_CHANNEL_ID) + self.idle_channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) + print (f'The idle channel is {config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]} - {self.idle_channel}') await self.idle_channel.send(f"{random.choice(self.idle_messages)}") # ###################################################### # on_ready is called once the client is initialized # it then reads in the files in the config.IDLE_MESSAGE_DIR - # directory and posts them randomly every - # config.CHANNEL_IDLE_INTERVAL seconds into the - # config.IDLE_MESSAGE_CHANNEL_ID channel + # directory and starts the schedulers # ###################################################### async def on_ready(self): @@ -82,13 +121,32 @@ class OMFClient(discord.Client): self.idle_messages = [] - for filename in glob(config.IDLE_MESSAGE_DIR + '/*.txt'): + for filename in glob(config.CONFIG["IDLE_MESSAGE_DIR"] + '/*.txt'): print ("read {}",filename) with open(filename) as f: self.idle_messages.append(f.read()) self.idle_messages = np.array(self.idle_messages) + # store the guild for further use + guild: discord.Guild + for guild in self.guilds: + if (int(guild.id) == int(config.SECRETS["GUILD_ID"])): + print (f"GUILD MATCHES {guild.id}") + self.theGuild = guild + + if (self.theGuild is None): + print("the guild (Server ID)could not be found - please check all config data") + exit() + + self.guildEventsClass = DiscordEvents( + discord_token=config.SECRETS["BOT_TOKEN"], + client_id=config.SECRETS["CLIENT_ID"], + bot_permissions=8, + api_version=10, + guild_id=config.SECRETS["GUILD_ID"] + ) + # start the schedulers self.task_scheduler.start() @@ -97,6 +155,7 @@ class OMFClient(discord.Client): # ###################################################### # handle_ping is called when a user sends ping + # (case sensitive, exact phrase) # it just replies with pong # ###################################################### @@ -112,8 +171,6 @@ class OMFClient(discord.Client): print("Create Events") - newEvent = DiscordEvents(BOT_TOKEN,f'https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&permissions=8&scope=bot') - for theEvent in config.AUTO_EVENTS: # calculate the date of the future event @@ -132,16 +189,16 @@ class OMFClient(discord.Client): # after 2 AM strStart=theDate.strftime(f"%Y-%m-%dT{utcStartTime}") strEnd=theDate.strftime(f"%Y-%m-%dT{utcEndTime}") - await newEvent.create_guild_event( - GUILD_ID, - theEvent['title'], - theEvent['description'], - f"{strStart}", - f"{strEnd}", - {},2,theEvent['channel']) - + await self.guildEventsClass.create_guild_event( + event_name=theEvent['title'], + event_description=theEvent['description'], + event_start_time=f"{strStart}", + event_end_time=f"{strEnd}", + event_metadata={}, + event_privacy_level=2, + channel_id=theEvent['channel']) # once we have created the event, we let everyone know - channel = self.get_channel(config.IDLE_MESSAGE_CHANNEL_ID) + channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) await channel.send(f'Hi - I have created the scheduled Event {theEvent["title"]}') @@ -150,13 +207,10 @@ class OMFClient(discord.Client): # ###################################################### async def get_events_list (self): - newEvent = DiscordEvents(BOT_TOKEN,f'https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&permissions=8&scope=bot') - eventList = await newEvent.list_guild_events(GUILD_ID) - self.guild_Events = eventList + eventList = await self.guildEventsClass.list_guild_events() + self.guildEventsList = eventList return eventList - - # ###################################################### # on_message scans for message contents and takes # corresponding actions. @@ -171,15 +225,15 @@ class OMFClient(discord.Client): # don't respond to ourselves if message.author == self.user: return + # reset the idle timer if a message has been sent or received self.channel_idle_timer = 0 # reply to ping if message.content == 'ping': await self.handle_ping(message) - - # check if there is a question + # check if there is a question if "?" in message.content: self.asked_question = True self.last_question = message @@ -187,14 +241,14 @@ class OMFClient(discord.Client): self.asked_question = False self.last_question = None - # ###################################################### # on_typing detects if a user types. # We might use this one day to have users agree to policies etc. # before they are allowed to speak + # or we might launch the Support() Modal if a user starts + # to type in the support channel # ###################################################### - async def on_typing(self, channel, user, _): # we do not want the bot to reply to itself if user.id == self.user.id: @@ -212,8 +266,8 @@ class OMFClient(discord.Client): async def daily_tasks(self): print("DAILY TASKS") - # Every Monday we want to create the scheduled events - # for the next Sunday + # Every Monday (weekday 0) we want to create the + # scheduled events for the next Sunday if datetime.date.today().weekday() == 0: print("create events") @@ -248,7 +302,7 @@ class OMFClient(discord.Client): # TODO - we need to send out a message to the @here role # asking users to help if the question had not been answered # Also - if the message is a reply then we should not post into the channel - if self.channel_idle_timer > config.QUESTION_SLEEPING_TIME: + if self.channel_idle_timer > config.CONFIG["QUESTION_SLEEPING_TIME"]: print("QUESTION WITHOUT REPLY") self.asked_question = False except Exception as e: @@ -260,7 +314,7 @@ class OMFClient(discord.Client): # then send a random message into the idle_channel # ##################################### try: - if self.channel_idle_timer >= config.CHANNEL_IDLE_INTERVAL: + if self.channel_idle_timer >= config.CONFIG["CHANNEL_IDLE_INTERVAL"]: self.channel_idle_timer = 0 await self.send_random_message() except Exception as e: @@ -290,11 +344,10 @@ class OMFClient(discord.Client): print("Let me check if the event is still on") try: if eventList == None: - newEvent = DiscordEvents(BOT_TOKEN,f'https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&permissions=8&scope=bot') - eventList = await newEvent.list_guild_events(GUILD_ID) + eventList = await self.get_events_list() for theScheduledEvent in eventList: if theScheduledEvent["name"] == theEvent["title"]: - channel = self.get_channel(config.IDLE_MESSAGE_CHANNEL_ID) + channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) theMessageText=f'Hi <@&{theEvent["subscription_role_num"]}>, the event *** {theEvent["title"]} *** will start in roughly {theEvent["notify_minutes"]} minutes in the <#{theEvent["channel"]}> channel. {theEvent["description"]}' await channel.send(f"{theMessageText}") except Exception as e: diff --git a/classes/dis_events.py b/classes/dis_events.py new file mode 100644 index 0000000..f5f812c --- /dev/null +++ b/classes/dis_events.py @@ -0,0 +1,67 @@ +import json +from classes.restfulapi import DiscordAPI + +# ######################################################### +# The class for Discord scheduled events +# as of 2022-07, discord.py does not support +# scheduled Events. Therefore we need to use the Rest API +# ######################################################### + + +class DiscordEvents(DiscordAPI): + + def __init__(self, + discord_token: str, + client_id: str, + bot_permissions: int, + api_version: int, + guild_id:str) -> None: + + super().__init__(discord_token,client_id,bot_permissions,api_version) + self.guild_id = guild_id + print (f" DiscordEvent Client {client_id} guild {guild_id}") + + + async def list_guild_events(self) -> list: + + # Returns a list of upcoming events for the supplied guild ID + # Format of return is a list of one dictionary per event containing information. + + event_retrieve_url = f'{self.base_api_url}/guilds/{self.guild_id}/scheduled-events' + response_list = await self.get_api(event_retrieve_url) + return response_list + + async def create_guild_event( + self, + event_name: str, + event_description: str, + event_start_time: str, + event_end_time: str, + event_metadata: dict, + event_privacy_level=2, + channel_id=None + ) -> None: + + # Creates a guild event using the supplied arguments + # The expected event_metadata format is event_metadata={'location': 'YOUR_LOCATION_NAME'} + # We hard code Entity type to "2" which is Voice channel + # hence we need no Event Metadata + # The required time format is %Y-%m-%dT%H:%M:%S + # Event times can use UTC Offsets! - if you omit, then it will be + # undefined (i.e. UTC / GMT+0) + + event_create_url = f'{self.base_api_url}/guilds/{self.guild_id}/scheduled-events' + event_data = json.dumps({ + 'name': event_name, + 'privacy_level': event_privacy_level, + 'scheduled_start_time': event_start_time, + 'scheduled_end_time': event_end_time, + 'description': event_description, + 'channel_id': channel_id, + 'entity_metadata': event_metadata, + 'entity_type': 2 + }) + try: + await self.post_api(event_create_url,event_data) + except Exception as e: + print(f'DiscordEvents.createguildevent : {e}') diff --git a/classes/restfulapi.py b/classes/restfulapi.py new file mode 100644 index 0000000..f05449f --- /dev/null +++ b/classes/restfulapi.py @@ -0,0 +1,61 @@ +import json +import aiohttp + +# ######################################################### +# The class for Discord API integration +# this can be used if certain functions are not supported +# by discord.py +# ######################################################### + +class DiscordAPI: + + # the init constructor stores the authentication-relevant data + # such as the token and the bot url + + # guild ID, Client ID, Token, Bot permissions, API Version + + def __init__(self, + discord_token: str, + client_id: str, + bot_permissions: int, + api_version: int) -> None: + + self.base_api_url = f'https://discord.com/api/v{api_version}' + self.bot_url = f'https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions={bot_permissions}&scope=bot' + self.auth_headers = { + 'Authorization':f'Bot {discord_token}', + 'User-Agent':f'DiscordBot ({self.bot_url}) Python/3.9 aiohttp/3.8.1', + 'Content-Type':'application/json' + } + + # get_api does an https get on the api + # it expects json data as result + + async def get_api(self, api_url: str) -> list: + + async with aiohttp.ClientSession(headers=self.auth_headers) as session: + try: + async with session.get(api_url) as response: + response.raise_for_status() + assert response.status == 200 + response_list = json.loads(await response.read()) + except Exception as e: + print(f'get_api EXCEPTION: {e}') + finally: + await session.close() + return response_list + + # post_api does an https put to the api url + # it expects json data as var which it posts + + async def post_api(self, api_url: str, dumped_jsondata: str) -> None: + + async with aiohttp.ClientSession(headers=self.auth_headers) as session: + try: + async with session.post(api_url, data=dumped_jsondata) as response: + response.raise_for_status() + assert response.status == 200 + except Exception as e: + print(f'post_api EXCEPTION: {e}') + finally: + await session.close() \ No newline at end of file diff --git a/classes/subscribe.py b/classes/subscribe.py new file mode 100644 index 0000000..9bfbe82 --- /dev/null +++ b/classes/subscribe.py @@ -0,0 +1,73 @@ +import discord +import traceback +import config + +# ############################################ +# the Subscribe() class is a modal ui dialog +# that let's you select one or multiple +# roles that will be assigned to you. +# The roles are set in the config.AUTO_EVENTS +# variable +# ############################################ + +class Subscribe(discord.ui.Modal, title='(un)subscribe to Event-Notification'): + + # We need to make sure that the config is read at object definition time + # because AUTO_EVENTS might be empty otherwise + if config.AUTO_EVENTS == []: + config.readConfig() + + # define the menu options (label=text, vaue=role_id) + + menu_options = [] + for menu_option in config.AUTO_EVENTS: + menu_options.append(discord.SelectOption(label= menu_option["notify_hint"], + value=menu_option["subscription_role_num"])) + # define the UI dropdown menu Element + + Menu = discord.ui.Select( + options = menu_options, + max_values=len(config.AUTO_EVENTS), + min_values=0) + + + # ##################################### + # on_submit is called when the user submits + # the modal. It will then go through + # the menu options and revoke / assign + # the corresponding roles + # ##################################### + + + async def on_submit(self, interaction: discord.Interaction): + role: discord.Role + roles = interaction.user.guild.roles + member: discord.Member + member = interaction.user + + # first we remove all roles + + for option in self.Menu.options: + role=member.get_role(option.value) + if not (role is None): + await member.remove_roles(role) + + # then we assign all selected roles + # unfortunately we need to loop through all rolles + # maybe there is a better solution ? + + for option in self.Menu._selected_values: + for role in roles: + if int(option) == int(role.id): + await member.add_roles(role) + + + await interaction.response.send_message(f'Thanks for using my services !', ephemeral=True) + + + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + await interaction.response.send_message('Oops! Something went wrong.', ephemeral=True) + + # Make sure we know what the error actually is + traceback.print_tb(error.__traceback__) diff --git a/classes/support.py b/classes/support.py new file mode 100644 index 0000000..0b976c3 --- /dev/null +++ b/classes/support.py @@ -0,0 +1,79 @@ +import discord +import traceback +import config + +# ############################################ +# the Support() class is a modal ui dialog +# that helps you create a thread in a +# selected channel. It asks for a title +# and a description and then creates +# a Thread in the config.CONFIG["SUPPORT_CHANNEL_ID"] +# channel. It also sends a message to the +# config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"] +# in order to notify everyone that a +# new support message has been created +# ############################################ + + +class Support(discord.ui.Modal, title='Open a support thread'): + + # This will be a short input, where the user can enter a title + # for the new thread + + theTitle = discord.ui.TextInput( + label='Title', + placeholder='a catchy title for the issue', + ) + + # This is a longer, paragraph style input, where user can submit + # a description of the problem + + theDescription = discord.ui.TextInput( + label='Describe the problem', + style=discord.TextStyle.long, + placeholder='Type in what the problem is...', + required=False, + max_length=300, + ) + + # ############################################ + # on_submit is called when the user submits the + # Modal. This is where we create the thread + # and send all related messages + # ############################################ + + async def on_submit(self, interaction: discord.Interaction): + + # first let's find out which channel we will create the thread in + theGuild = interaction.guild + theChannel : discord.TextChannel + theChannel = theGuild.get_channel(config.CONFIG["SUPPORT_CHANNEL_ID"]) + + if not (theChannel is None): + try: + # we send a message into that channel that serves as "hook" for the thread + # (if we didn't have a message to hook then the thread would be created + # as private which requires a boost level) + + xMsg= await theChannel.send (f"Support Thread for <@{interaction.user.id}>") + newThread=await theChannel.create_thread(name=f"{self.theTitle.value}",message=xMsg,auto_archive_duration=1440) + + # next we want to post about the new "ticket" in the IDLE_MESSAGE_CHANNEL + + theChannel = theGuild.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) + if (not (theChannel is None)) and (not (newThread is None)): + xMsg= await theChannel.send (f'I have created a **Support Thread** on behalf of <@{interaction.user.id}> :\n\n <#{newThread.id}> in the <#{config.CONFIG["SUPPORT_CHANNEL_ID"]}>\n\nMaybe you could check in and see if **you** can help ??? \nMany thanks !') + xMsg= await newThread.send (f'<@{interaction.user.id}> describes the problem as follows: \n{self.theDescription.value} \n \n please tag the user on your reply - thank you!' ) + except Exception as e: + print(f"Support Error: {e}") + + # last but not least we send an ephemeral message to the user + # linking to the created thread + + await interaction.response.send_message(f'Your Support Thread has been created here: <#{newThread.id}> Please check if everything is correct.\nThank you for using my services!', ephemeral=True) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + await interaction.response.send_message('Oops! Something went wrong.', ephemeral=True) + + # Make sure we know what the error actually is + traceback.print_tb(error.__traceback__) diff --git a/config.py b/config.py index 95574d7..28308fb 100644 --- a/config.py +++ b/config.py @@ -1,57 +1,81 @@ +import json + +# Config data is stored in config.json +# see the config.json.example file and below comments +# readConfig() reads the file + +cfg = None +AUTO_EVENTS = [] +SECRETS = {} +CONFIG = {} + +def readConfig(): + try: + f = open('config.json') + global cfg + cfg = json.load(f) + #configData = json.loads(data) + f.close() + global AUTO_EVENTS + AUTO_EVENTS = cfg["AUTO_EVENTS"] + global CONFIG + CONFIG = cfg["config"] + global SECRETS + SECRETS = cfg["secret"] + except Exception as e: + print(f"Error reading Config Data: {e}") + +# the config.json contains the following main nodes: + +# ####################### +# "secret" +# ####################### + +# the secret key contains the following items: + +# the BOT_TOKEN is the Oauth2 token for your bot +# example: "BOT_TOKEN" : "DFHEZRERZQRJTSUPERSECRETTTOKENUTZZH" + +# The GUILD_ID is the ID of your Server - in the discord client, +# right click on your server and select " copy ID" to get it +# example: "GUILD_ID" : "0236540000563456" + +# The client ID can be copied from your App settings page and is needed +# to authenticate with the Discord Restful API for Event creation +# example "CLIENT_ID" : "9990236500564536" + +# ####################### +# "config" +# ####################### + +# the config node contains all generic config items, such as channel IDs +# and scheduler variables + +# CHANNEL_IDLE_INTERVAL (number) # the number of scheduler cycles that the channel needs to be idle # before the bot posts a generic "did you know" # message -CHANNEL_IDLE_INTERVAL: int = 4 - +# IDLE_MESSAGE_DIR (path without trailing slash) # the name of the directory where the text files are # located which contain the messages # which the bot will randomly send # (1 file = 1 message) -IDLE_MESSAGE_DIR: str = "bot_messages" - +# IDLE_MESSAGE_CHANNEL_ID # the channel where the bot will post messages to -IDLE_MESSAGE_CHANNEL_ID = 758271650226765848 - -# Variable that indicates when the bot answers after a question has been asked +# QUESTION_SLEEPING_TIME (number) +# # Variable that indicates when the bot answers after a question has been asked # (in scheduler cycles) -QUESTION_SLEEPING_TIME = 2 +# ####################### +# "AUTO_EVENTS" +# ####################### # The Auto Events. # this is used in three contexts: # 1. Automatic creation of the event # 2. Automatic reminder of subscribed users # 3. in the /subscribe command - -# ALL TIMES LOCAL TIMEZONE OF THE BOT !!!! - -AUTO_EVENTS = [ - { - 'title':"Sunday Funday session (AM)", - 'description':"Chat with Marc and the folks here on the server ! Share your screen if you want to walk through a problem. Talk about tech stuff with the other members or just listen in...", - 'channel':758271650688008202, - 'notify_hint':'get notified when the AM session starts', - 'subscription_role':'notify_am', - 'subscription_role_num':764893618066161695, - 'notify_minutes':30, - 'day_of_week':6, - 'start_time':"09:00:00", - 'end_time':"10:00:00" - }, - { - 'title':"Sunday Funday session (PM)", - 'description':"Chat with Marc and the folks here on the server ! Share your screen if you want to walk through a problem. Talk about tech stuff with the other members or just listen in...", - 'channel':758271650688008202, - 'notify_hint':'get notified when the PM session starts', - 'subscription_role':'notify_pm', - 'subscription_role_num':769829891419537448, - 'notify_minutes':30, - 'day_of_week':6, - 'start_time':"18:00:00", - 'end_time':"19:00:00" - } -] - +# this needs to be an array of dict diff --git a/dis_events.py b/dis_events.py deleted file mode 100644 index f2cb94d..0000000 --- a/dis_events.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import aiohttp - -# ######################################################### -# The class for Discord scheduled events -# as of 2022-07, discord.py does not support -# scheduled Events. Therefore we need to use the Rest API -# ######################################################### - - -class DiscordEvents: - '''Class to create and list Discord events utilizing their API''' - def __init__(self, discord_token: str, bot_url: str) -> None: - self.base_api_url = 'https://discord.com/api/v10' - self.auth_headers = { - 'Authorization':f'Bot {discord_token}', - 'User-Agent':f'DiscordBot ({bot_url}) Python/3.9 aiohttp/3.8.1', - 'Content-Type':'application/json' - } - - async def list_guild_events(self, guild_id: str) -> list: - - print("list_guild_events") - - '''Returns a list of upcoming events for the supplied guild ID - Format of return is a list of one dictionary per event containing information.''' - event_retrieve_url = f'{self.base_api_url}/guilds/{guild_id}/scheduled-events' - async with aiohttp.ClientSession(headers=self.auth_headers) as session: - try: - async with session.get(event_retrieve_url) as response: - response.raise_for_status() - assert response.status == 200 - response_list = json.loads(await response.read()) - except Exception as e: - print(f'EXCEPTION: {e}') - finally: - await session.close() - return response_list - - async def create_guild_event( - self, - guild_id: str, - event_name: str, - event_description: str, - event_start_time: str, - event_end_time: str, - event_metadata: dict, - event_privacy_level=2, - channel_id=None - ) -> None: - '''Creates a guild event using the supplied arguments - The expected event_metadata format is event_metadata={'location': 'YOUR_LOCATION_NAME'} - The required time format is %Y-%m-%dT%H:%M:%S''' - - print("create_guild_event") - - event_create_url = f'{self.base_api_url}/guilds/{guild_id}/scheduled-events' - event_data = json.dumps({ - 'name': event_name, - 'privacy_level': event_privacy_level, - 'scheduled_start_time': event_start_time, - 'scheduled_end_time': event_end_time, - 'description': event_description, - 'channel_id': channel_id, - 'entity_metadata': event_metadata, - 'entity_type': 2 - }) - async with aiohttp.ClientSession(headers=self.auth_headers) as session: - try: - async with session.post(event_create_url, data=event_data) as response: - response.raise_for_status() - assert response.status == 200 - except Exception as e: - print(f'EXCEPTION: {e}') - finally: - await session.close() \ No newline at end of file diff --git a/example.config.json b/example.config.json new file mode 100644 index 0000000..d1cb39f --- /dev/null +++ b/example.config.json @@ -0,0 +1,43 @@ +{ + "secret" : + { + "BOT_TOKEN" : "YOURBOTTOKEN", + "GUILD_ID" : "YOURGUILDID", + "CLIENT_ID" : "YOURCLIENTID" + }, + "config" : + { + "CHANNEL_IDLE_INTERVAL": 4, + "IDLE_MESSAGE_DIR" : "bot_messages", + "IDLE_MESSAGE_CHANNEL_ID" : 12345678900, + "QUESTION_SLEEPING_TIME" : 2, + "SUPPORT_CHANNEL_ID" : 1234567 + }, + "AUTO_EVENTS" : + [ + { + "title":"Sunday Funday session (AM)", + "description":"Chat with Marc and the folks here on the server ! Share your screen if you want to walk through a problem. Talk about tech stuff with the other members or just listen in...", + "channel":12345678900, + "notify_hint":"get notified when the AM session starts", + "subscription_role":"notify_am", + "subscription_role_num":12345678900, + "notify_minutes":30, + "day_of_week":6, + "start_time":"09:00:00", + "end_time":"10:00:00" + }, + { + "title":"Sunday Funday session (PM)", + "description":"Chat with Marc and the folks here on the server ! Share your screen if you want to walk through a problem. Talk about tech stuff with the other members or just listen in...", + "channel":12345678900, + "notify_hint":"get notified when the PM session starts", + "subscription_role":"notify_pm", + "subscription_role_num":12345678900, + "notify_minutes":30, + "day_of_week":6, + "start_time":"18:00:00", + "end_time":"19:00:00" + } + ] +} \ No newline at end of file diff --git a/main.py b/main.py index 5b07409..cdc5d57 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,8 @@ -import bot -import secret +import classes.bot as bot +import config + +if config.cfg is None: + config.readConfig() client = bot.OMFClient() -client.run(secret.BOT_TOKEN) +client.run(config.SECRETS["BOT_TOKEN"]) diff --git a/secret.py.example b/secret.py.example deleted file mode 100644 index a38b337..0000000 --- a/secret.py.example +++ /dev/null @@ -1,16 +0,0 @@ -# You need to create a file called secret.py that contains your -# discord token. This is just an example. Replace the tokens with your own ! - -# the BOT_TOKEN is the Oauth2 token for your bot - -BOT_TOKEN: str = "DFHEZRERZQRJTSUPERSECRETTTOKENUTZZH" - -# The GUILD_ID is the ID of your Server - in the discord client, -# right click on your server and select " copy ID" to get it - -GUILD_ID: str = "0236540000563456" - -# The client ID can be copied from your App settings page and is needed -# to authenticate with the Discord Restful API for Event creation - -CLIENT_ID: str = "9990236500564536" \ No newline at end of file diff --git a/utils.py b/utils.py index a2284f2..9d0bc3a 100644 --- a/utils.py +++ b/utils.py @@ -3,22 +3,12 @@ import datetime # ################################################ # Returns the date of the next given weekday after # the given date. For example, the date of next Monday. -# # NB: if it IS the day we're looking for, this returns 0. # consider then doing onDay(foo, day + 1). -# # Monday=0, Tuesday=1 .... Sunday=6 # ################################################ - def onDay(date, day): - """ - Returns the date of the next given weekday after - the given date. For example, the date of next Monday. - - NB: if it IS the day we're looking for, this returns 0. - consider then doing onDay(foo, day + 1). - """ days = (day - date.weekday() + 7) % 7 return date + datetime.timedelta(days=days)