cogs, shardable, setup, update, say_ralf commands

Signed-off-by: Marc Ahlgrim <marc@onemarcfifty.com>
This commit is contained in:
Marc Ahlgrim
2022-07-26 11:43:33 +02:00
parent 2e25186e96
commit 455da6a9f3
17 changed files with 503 additions and 363 deletions
+39 -8
View File
@@ -2,16 +2,12 @@
R.A.L.F. (short for Responsive Artificial Lifeform ;-) ) is the "housekeeping" bot on the oneMarcFifty Discord Server 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.4):
- reply with "pong" to a "ping" (WHOA!!!) - reply with "pong" to a "ping" (WHOA!!!)
- Send out configurable random messages when the server is idle (like "Did you know...") - 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) - automatically create Events (we have Sunday video sessions at 9 AM and 6 PM)
- remind subscribers of upcoming events - 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 - let the user subscribe / unsubscribe to notification messages with a modal view
## How to use ## How to use
@@ -22,10 +18,45 @@ You need the discord.py wrapper from Rapptz :
cd discord.py/ cd discord.py/
python3 -m pip install -U .[voice] python3 -m pip install -U .[voice]
Next, adapt the `secret.py` file to reflect your token etc. Next, adapt the `config.json` file to reflect your token etc.
Also, customize all settings in `config.py` and the text files in the
`bot_messages`directory
Now you can cd into the bot's directory and launch it with Now you can cd into the bot's directory and launch it with
python3 main.py python3 main.py
## slash commands
The bot supports the following commands:
**/setup** set up basic functionality (channel for idle messages, frequency of the messages, channel that contains the templates)
**/subscribe** let a user subscribe to roles that are defined in the scheduled events
**/update** update the bot (re-read the template channel)
**/say_ralf** let ralf say something. You can specify the channel and the message ;-)
## How do you configure R.A.L.F. ?
First you need a minimum `config.json`like shown in `minimum.config.json` that contains your token and client id.
All other values are taken from a "Template" channel. When you run the **/setup** command, R.A.L.F. will let you chose that one.
In that template channel, you just create messages that you want R.A.L.F. to send randomly into the configured idle_channel.
### how to configure Events ?
A special type of template message contains a definition of scheduled events. The message needs to contain data that can be converted to a dict object, like this:
{
"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": 12345452435,
"notify_hint": "get notified when the PM session starts",
"subscription_role_num": 12432345423,
"notify_minutes": 30,
"day_of_week": 6,
"start_time": "18:00:00",
"end_time": "19:00:00"
}
(when you use the /update command, it will tell you if it could read it or not.)
Every Monday, R.A.L.F. will then go through the Events and create them as scheduled events on the guild/server.
If the user has the `subscription_role_num` role, then he/she will be notified by R.A.L.F. roughly `notify_minutes`before the event starts.
-14
View File
@@ -1,14 +0,0 @@
Hi, I am R.A.L.F., your assistant bot.
Did you know that there is a **video/voice session** that is **open to everyone** on this server?
***Every Sunday at 9 AM and 6 PM Berlin/Paris time*** you can join in the <#758271650688008202> channel.
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...
****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 ;-)
-6
View File
@@ -1,6 +0,0 @@
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
-5
View File
@@ -1,5 +0,0 @@
Hi, this is R.A.L.F., your assistant bot.
Please check in every now and then to the <#954732247909552149> channel.
This is where folks here on the server ask for help.
Maybe **** YOU **** can help someone there ?
-3
View File
@@ -1,3 +0,0 @@
Hi, I am R.A.L.F., your assistant bot.
Did you know that you can see ***all of Marc's videos*** in the <#792662116456726538> channel?
-9
View File
@@ -1,9 +0,0 @@
Hi, I am R.A.L.F., your assistant bot.
**We are always looking for Sponsors!**
If you have Nitro or if you want to spend some $$$
*** maybe you want to boost the server ? ***
This will give everyone better video and voice quality in the Sunday Funday calls or for tech support calls.
-9
View File
@@ -1,9 +0,0 @@
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 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.
Maybe ****YOU**** can help someone there ?
-7
View File
@@ -1,7 +0,0 @@
Hi, I am R.A.L.F., the Responsive Artificial Life Form ;-)
Did you know that everyone can use the
*** <#957636889718968380> ***
channel for video/voice calls at any time ?
+268 -142
View File
@@ -1,19 +1,19 @@
import discord import discord
import random import random
import numpy as np
import utils import utils
import datetime import datetime
import config import os
import ast
from glob import glob
from dateutil import tz from dateutil import tz
from sys import exit
from discord import app_commands
from discord.ext import tasks from discord.ext import tasks
from discord.ext.commands import Bot,AutoShardedBot
from classes.dis_events import DiscordEvents from classes.dis_events import DiscordEvents
from classes.subscribe import Subscribe from classes.subscribe import Subscribe
from classes.config import Config
from dataclasses import dataclass
# ####################################### # #######################################
@@ -21,80 +21,222 @@ from classes.subscribe import Subscribe
# ####################################### # #######################################
class OMFClient(discord.Client): class OMFBot(AutoShardedBot):
channel_idle_timer: int # each guild has the following elements:
lastNotifyTimeStamp = None # - a list of scheduled Events (EventsList)
theGuild : discord.Guild = None # - a list of Messages that will be sent at idle times
# - a timer indicating the number of scheduler runs to
# wait before a new idle message is sent
lastSentMessage:discord.Message=None @dataclass
class GuildData:
EventsList = {}
idle_messages=[]
channel_idle_timer=0
guildEventsList = None # the guildDataList contains one GuildData class per item.
guildEventsClass: DiscordEvents = None # the key is the guild ID
guildDataList={}
# configData is the generic config object that reads / writes
# the config data to disk
configData:Config
# EventsClass is the interface to the restful api
# that allows us to create scheduled events
EventsClass : DiscordEvents
# ####################################### # #######################################
# init constructor # init constructor
# ####################################### # #######################################
def __init__(self) -> None: def __init__(self) -> None:
print('Init')
# Try to set all intents # Try to set all intents
intents = discord.Intents.all() intents = discord.Intents.all()
super().__init__(intents=intents) super().__init__(command_prefix="!",intents=intents)
self.prefix="!"
# We need a `discord.app_commands.CommandTree` instance self.configData=Config('config.json')
# to register application commands (slash commands in this case)
self.tree = app_commands.CommandTree(self)
# The subscribe command will add/remove the notification roles # The subscribe command will add/remove the notification roles
# based on the scheduled events # based on the scheduled events
@self.tree.command(name="subscribe", description="(un)subscribe to Events)") @self.tree.command(name="subscribe", description="(un)subscribe to Events)")
async def subscribe(interaction: discord.Interaction): 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 x: Subscribe
role: discord.Role
member: discord.Member member: discord.Member
x=Subscribe() guildNode=self.configData.readGuild(interaction.guild.id)
member = interaction.user member = interaction.user
x=Subscribe(autoEvents=guildNode["AUTO_EVENTS"],member=member)
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) await interaction.response.send_modal(x)
self.channel_idle_timer = 0 # The setup command will ask for the guild parameters and
self.idle_channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) # read them in
@self.tree.command(name="setup", description="Define parameters for the bot")
async def setup(
interaction: discord.Interaction,
template_channel:discord.TextChannel,
idle_channel:discord.TextChannel,
idle_sleepcycles:int,
avoid_spam:int):
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f'only an Administrator can do that', ephemeral=True)
else:
try:
jData={
"IDLE_MESSAGE_CHANNEL_ID" : idle_channel.id,
"CONFIG_CHANNEL_ID": template_channel.id,
"CHANNEL_IDLE_INTERVAL" : idle_sleepcycles,
"AVOID_SPAM" : avoid_spam,
"AUTO_EVENTS": []
}
self.configData.writeGuild(interaction.guild.id,jData)
await interaction.response.send_message(f'All updated\nThank you for using my services!\nyou might need to run /update', ephemeral=True)
except Exception as e:
print(f"ERROR in setup command: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
# The update command will read the guild configs from the
# message templates channel
@self.tree.command(name="update", description="read in the message templates and update the cache")
async def update(interaction: discord.Interaction):
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f'only an Administrator can do that', ephemeral=True)
else:
try:
await self.readMessageTemplates(interaction.guild)
#self.configData.writeGuild(interaction.guild.id,jData)
numMessages=len(self.guildDataList[f'{interaction.guild.id}'].idle_messages)
guildNode=self.configData.readGuild(interaction.guild.id)
eventNodes=guildNode["AUTO_EVENTS"]
numEvents=len(eventNodes)
await interaction.response.send_message(f'{numEvents} Events and {numMessages} Message templates\nThank you for using my services!', ephemeral=True)
except Exception as e:
print(f"ERROR in update command: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
@self.tree.command(name="say_ralf", description="admin function")
async def say_ralf(
interaction: discord.Interaction,
which_channel:discord.TextChannel,
message:str):
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f'only an Administrator can do that', ephemeral=True)
else:
try:
await which_channel.send(message)
await interaction.response.send_message('message sent', ephemeral=True)
except Exception as e:
print(f"ERROR in say_ralf: {e}")
await interaction.response.send_message(f'Ooops, there was a glitch!', ephemeral=True)
# ################################
# the bot run command just starts
# the bot with the token from
# the json config file
# ################################
def run(self,*args, **kwargs):
super().run(token=self.configData.getToken())
# ######################### # #########################
# setup_hook waits for the # setup_hook waits for the
# command tree to sync # command tree to sync
# and loads the cogs
# ######################### # #########################
async def setup_hook(self) -> None: async def setup_hook(self) -> None:
# Sync the application command with Discord. # Sync the application command with Discord.
await self.tree.sync() await self.tree.sync()
# load all cogs
try:
for file in os.listdir("cogs"):
if file.endswith(".py"):
name = file[:-3]
await self.load_extension(f"cogs.{name}")
except Exception as e:
print(f"ERROR in setup_hook: {e}")
# ###################################################### # ######################################################
# send_random_message is called when the server is idle # send_random_message is called when the server is idle
# and posts a random message to the server # and posts a random message to the server
# ###################################################### # ######################################################
async def send_random_message(self): async def send_random_message(self,guildID):
guildNode=self.configData.readGuild(guildID)
print("Sending random message") print("Sending random message")
if self.idle_channel == None: idle_channel_id=guildNode["IDLE_MESSAGE_CHANNEL_ID"]
self.idle_channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) idle_channel=self.get_channel(idle_channel_id)
print (f'The idle channel is {config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]} - {self.idle_channel}') gdn:self.GuildData
await self.idle_channel.send(f"{random.choice(self.idle_messages)}") gdn=self.guildDataList[f'{guildID}']
idle_messages=gdn.idle_messages
# if the author of the previously last sent message and
# the new message is ourselves, then delete the
# previous message
try:
lastSentMessage = await idle_channel.fetch_message(
idle_channel.last_message_id)
if (int(f'{guildNode["AVOID_SPAM"]}') == 1) and (lastSentMessage is not None):
if (lastSentMessage.author == self.user):
await lastSentMessage.delete()
except Exception as e:
print(f"delete lastmessage error: {e}")
try:
await idle_channel.send(f'{random.choice(idle_messages)}')
except Exception as e:
print(f"send random message error: {e}")
# ######################################################
# readMessageTemplates reads all messages from
# the guildID/"config"/"CONFIG_CHANNEL_ID"] node
# of the configdata and stores it in the idleMessages dict
# in an array under the guild ID key
# ######################################################
async def readMessageTemplates(self,theGuild:discord.Guild):
# we init the guild data with a new GuildData object
self.guildDataList[f'{theGuild.id}'] = self.GuildData()
guildNode=self.configData.readGuild(theGuild.id)
if guildNode is None:
print (f"Guild {theGuild.id} has no setup")
return
theTemplateChannel:discord.TextChannel
theTemplateChannel=theGuild.get_channel(int(guildNode["CONFIG_CHANNEL_ID"]))
message:discord.Message
messages = theTemplateChannel.history(limit=50)
self.guildDataList[f'{theGuild.id}'].idle_messages=[]
eventNodes=[]
async for message in messages:
messageContent:str
messageContent=message.content
try:
someDict=ast.literal_eval(messageContent)
if isinstance(someDict, dict):
eventNodes.append(someDict)
except Exception as e:
self.guildDataList[f'{theGuild.id}'].idle_messages.append(message.content)
guildNode["AUTO_EVENTS"]=eventNodes
self.configData.writeGuild(theGuild.id,guildNode)
numMessages=len(self.guildDataList[f'{theGuild.id}'].idle_messages)
numEvents=len(eventNodes)
print(f'{numEvents} Events and {numMessages} Message templates')
# ###################################################### # ######################################################
# on_ready is called once the client is initialized # on_ready is called once the client is initialized
@@ -105,37 +247,18 @@ class OMFClient(discord.Client):
async def on_ready(self): async def on_ready(self):
print('Logged on as', self.user) print('Logged on as', self.user)
self.EventsClass = DiscordEvents(
discord_token=self.configData.getToken(),
client_id=self.configData.getClientID(),
bot_permissions=8,
api_version=10)
# read in the random message files # read in the random message files
# the idle_messages array holds one element per message # the idle_messages array holds one element per message
# every file is read in as a whole into one element of the array # every message is read in as a whole into one element of the array
self.idle_messages = [] for theGuild in self.guilds:
await self.readMessageTemplates(theGuild)
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 # start the schedulers
@@ -149,11 +272,14 @@ class OMFClient(discord.Client):
# for the next sunday # for the next sunday
# ###################################################### # ######################################################
async def create_events (self): async def create_events (self,theGuild):
print("Create Events") print("Create Events")
for theEvent in config.AUTO_EVENTS: guildNode=self.configData.readGuild(theGuild.id)
eventNodes=guildNode["AUTO_EVENTS"]
for theEvent in eventNodes:
# calculate the date of the future event # calculate the date of the future event
theDate:datetime.datetime = utils.onDay(datetime.date.today(),theEvent['day_of_week']) theDate:datetime.datetime = utils.onDay(datetime.date.today(),theEvent['day_of_week'])
@@ -171,16 +297,18 @@ class OMFClient(discord.Client):
# after 2 AM # after 2 AM
strStart=theDate.strftime(f"%Y-%m-%dT{utcStartTime}") strStart=theDate.strftime(f"%Y-%m-%dT{utcStartTime}")
strEnd=theDate.strftime(f"%Y-%m-%dT{utcEndTime}") strEnd=theDate.strftime(f"%Y-%m-%dT{utcEndTime}")
await self.guildEventsClass.create_guild_event(
await self.EventsClass.create_guild_event(
event_name=theEvent['title'], event_name=theEvent['title'],
event_description=theEvent['description'], event_description=theEvent['description'],
event_start_time=f"{strStart}", event_start_time=f"{strStart}",
event_end_time=f"{strEnd}", event_end_time=f"{strEnd}",
event_metadata={}, event_metadata={},
event_privacy_level=2, event_privacy_level=2,
channel_id=theEvent['channel']) channel_id=theEvent['channel'],
guild_id=theGuild.id)
# once we have created the event, we let everyone know # once we have created the event, we let everyone know
channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) channel = theGuild.get_channel(guildNode["IDLE_MESSAGE_CHANNEL_ID"])
await channel.send(f'Hi - I have created the scheduled Event {theEvent["title"]}') await channel.send(f'Hi - I have created the scheduled Event {theEvent["title"]}')
@@ -188,9 +316,10 @@ class OMFClient(discord.Client):
# get_event_list gives a list of scheduled events # get_event_list gives a list of scheduled events
# ###################################################### # ######################################################
async def get_events_list (self): async def get_events_list (self,theGuild: discord.Guild):
eventList = await self.guildEventsClass.list_guild_events()
self.guildEventsList = eventList eventList = await self.EventsClass.list_guild_events(theGuild.id)
self.GuildData(self.guildDataList[f'{theGuild.id}']).EventsList = eventList
return eventList return eventList
# ###################################################### # ######################################################
@@ -207,30 +336,16 @@ class OMFClient(discord.Client):
if message.flags.ephemeral: if message.flags.ephemeral:
return return
print("{} has just sent {}".format(message.author, message.content)) print("{} has just sent {}".format(message.author, message.content))
# if the author of the previously last sent message and
# the new message is ourselves, then delete the
# previous message
if (int(f'{config.CONFIG["AVOID_SPAM"]}') == 1) and (self.lastSentMessage is not None):
if ((message.author == self.user) and
(self.lastSentMessage.author == self. user) and
(int(f"{self.lastSentMessage.channel.id}") == (int(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"])))):
try:
await self.lastSentMessage.delete()
except Exception as e:
print(f"delete lastmessage error: {e}")
self.lastSentMessage = message
# don't respond to ourselves # don't respond to ourselves
if message.author == self.user: if message.author == self.user:
return return
# reset the idle timer if a message has been sent or received # reset the idle timer if a message has been sent or received
self.channel_idle_timer = 0 self.guildDataList[f'{message.guild.id}'].channel_idle_timer=0
await self.process_commands(message)
# ###################################################### # ######################################################
@@ -263,10 +378,11 @@ class OMFClient(discord.Client):
if datetime.date.today().weekday() == 0: if datetime.date.today().weekday() == 0:
print("create events") print("create events")
try: for theGuild in self.guilds:
await self.create_events() try:
except Exception as e: await self.create_events(theGuild=theGuild)
print(f"Daily Task create Events failed: {e}") except Exception as e:
print(f"Daily Task create Events failed: {e}")
# ###################################################### # ######################################################
@@ -281,50 +397,60 @@ class OMFClient(discord.Client):
@tasks.loop(minutes=10) @tasks.loop(minutes=10)
async def task_scheduler(self): async def task_scheduler(self):
self.channel_idle_timer += 1
print("SCHEDULE") print("SCHEDULE")
# ##################################### for theGuild in self.guilds:
# see if we need to send a random message guildNode=self.configData.readGuild(theGuild.id)
# if the counter is greater than CHANNEL_IDLE_INTERVAL if guildNode is None:
# then send a random message into the idle_channel print (f"Guild {theGuild.id} has no setup")
# ##################################### continue
try: gdn:self.GuildData
if self.channel_idle_timer >= config.CONFIG["CHANNEL_IDLE_INTERVAL"]: gdn=self.guildDataList[f'{theGuild.id}']
self.channel_idle_timer = 0 gdn.channel_idle_timer += 1
await self.send_random_message()
except Exception as e:
print(f"Scheduler random_message failed: {e}")
# see if we need to send out notifications for events # #####################################
# The Event details are stored in config. # see if we need to send a random message
eventList=None # if the counter is greater than CHANNEL_IDLE_INTERVAL
for theEvent in config.AUTO_EVENTS: # then send a random message into the idle_channel
# first let's convert the String dates to datetime: # #####################################
theDate=utils.onDay(datetime.date.today(),theEvent['day_of_week'])
startTime=theEvent['start_time'] try:
eventScheduledTimeDate=datetime.datetime.fromisoformat (theDate.strftime(f"%Y-%m-%dT{startTime}")) if gdn.channel_idle_timer >= guildNode["CHANNEL_IDLE_INTERVAL"]:
# now let's figure out the time deltas: gdn.channel_idle_timer = 0
timeUntilEvent=eventScheduledTimeDate - datetime.datetime.today() await self.send_random_message(guildID=theGuild.id)
earliestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"]) - datetime.timedelta(minutes=10) except Exception as e:
latestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"]) print(f"Scheduler random_message failed: {e}")
#print("found scheduled event")
#print(f"The event is on {theDate} at {startTime} - that is in {timeUntilEvent}") # see if we need to send out notifications for events
#print (f"we ping between {earliestPing} and {latestPing}") # The Event details are stored in config.
# If we are in the interval then let's initiate the reminder eventList=None
if (earliestPing < datetime.datetime.today() <= latestPing):
# let's first check if the event is still on for theEvent in guildNode["AUTO_EVENTS"]:
# it may have been deleted or modified on the server # first let's convert the String dates to datetime:
# we don't want to alert for non-existing events theDate=utils.onDay(datetime.date.today(),theEvent['day_of_week'])
# we'll just use the title to compare for the time being. startTime=theEvent['start_time']
print("Let me check if the event is still on") eventScheduledTimeDate=datetime.datetime.fromisoformat (theDate.strftime(f"%Y-%m-%dT{startTime}"))
try: # now let's figure out the time deltas:
if eventList == None: timeUntilEvent=eventScheduledTimeDate - datetime.datetime.today()
eventList = await self.get_events_list() earliestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"]) - datetime.timedelta(minutes=10)
for theScheduledEvent in eventList: latestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"])
if theScheduledEvent["name"] == theEvent["title"]: #print("found scheduled event")
channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"]) #print(f"The event is on {theDate} at {startTime} - that is in {timeUntilEvent}")
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"]}' #print (f"we ping between {earliestPing} and {latestPing}")
await channel.send(f"{theMessageText}") # If we are in the interval then let's initiate the reminder
except Exception as e: if (earliestPing < datetime.datetime.today() <= latestPing):
print(f"Scheduler event_reminder failed: {e}") # let's first check if the event is still on
# it may have been deleted or modified on the server
# we don't want to alert for non-existing events
# we'll just use the title to compare for the time being.
print("Let me check if the event is still on")
try:
if eventList == None:
eventList = await self.get_events_list(theGuild=theGuild)
for theScheduledEvent in eventList:
if theScheduledEvent["name"] == theEvent["title"]:
channel = self.get_channel(guildNode["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:
print(f"Scheduler event_reminder failed: {e}")
+87
View File
@@ -0,0 +1,87 @@
import json
# very rudimentary implementation of a
# generic config class using a json File
# this is OK for few clients.
# for many (>100) clients we should consider file locking
# for very many (>10000) clients we should use a database
class Config():
configFileName:str
cfg:json
def __init__(self,filename:str) -> None:
self.configFileName = filename
self.readConfig()
def readConfig(self) -> json:
try:
f = open(self.configFileName)
self.cfg = json.load(f)
f.close()
except Exception as e:
print(f"Error reading Config Data: {e}")
def writeConfig(self):
try:
with open(self.configFileName, 'w', encoding='utf-8') as f:
json.dump(self.cfg, f, ensure_ascii=False, indent=5)
f.close()
except Exception as e:
print(f"Error writing Config Data: {e}")
def getNode(self,nodeID:str):
if nodeID in self.cfg:
return self.cfg[nodeID]
else:
return None
def getToken(self):
secretNode=self.getNode("secret")
return secretNode.get('BOT_TOKEN')
def getClientID(self):
secretNode=self.getNode("secret")
return secretNode.get('CLIENT_ID')
def readGuild(self,guildID) -> json:
guildNode=self.getNode("guilds")
return guildNode.get(f"{guildID}")
def writeGuild(self,guildID,nodeData):
guildNode=self.getNode("guilds")
guildNode[f"{guildID}"]=nodeData
self.writeConfig()
# 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"
# #######################
# "guilds"
# #######################
# the guilds node contains all guild specific items, such as channel IDs
# that node will be created if the /setup command is used
# "CONFIG_CHANNEL_ID" is the ID of the channel where the
# idle message templates are located
# "IDLE_MESSAGE_CHANNEL_ID" is the ID of the channel where the
# bot posts a message about the new "ticket"
# QUESTION_SLEEPING_TIME (number)
# # Variable that indicates when the bot answers after a question has been asked
# (in scheduler cycles)
+12 -14
View File
@@ -10,24 +10,21 @@ from classes.restfulapi import DiscordAPI
class DiscordEvents(DiscordAPI): class DiscordEvents(DiscordAPI):
def __init__(self, # def __init__(self,
discord_token: str, # discord_token: str,
client_id: str, # client_id: str,
bot_permissions: int, # bot_permissions: int,
api_version: int, # api_version: int) -> None:
guild_id:str) -> None: #
# super().__init__(discord_token,client_id,bot_permissions,api_version)
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: async def list_guild_events(self,guild_id) -> list:
# Returns a list of upcoming events for the supplied guild ID # Returns a list of upcoming events for the supplied guild ID
# Format of return is a list of one dictionary per event containing information. # 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' event_retrieve_url = f'{self.base_api_url}/guilds/{guild_id}/scheduled-events'
response_list = await self.get_api(event_retrieve_url) response_list = await self.get_api(event_retrieve_url)
return response_list return response_list
@@ -39,7 +36,8 @@ class DiscordEvents(DiscordAPI):
event_end_time: str, event_end_time: str,
event_metadata: dict, event_metadata: dict,
event_privacy_level=2, event_privacy_level=2,
channel_id=None channel_id=None,
guild_id=0
) -> None: ) -> None:
# Creates a guild event using the supplied arguments # Creates a guild event using the supplied arguments
@@ -50,7 +48,7 @@ class DiscordEvents(DiscordAPI):
# Event times can use UTC Offsets! - if you omit, then it will be # Event times can use UTC Offsets! - if you omit, then it will be
# undefined (i.e. UTC / GMT+0) # undefined (i.e. UTC / GMT+0)
event_create_url = f'{self.base_api_url}/guilds/{self.guild_id}/scheduled-events' event_create_url = f'{self.base_api_url}/guilds/{guild_id}/scheduled-events'
event_data = json.dumps({ event_data = json.dumps({
'name': event_name, 'name': event_name,
'privacy_level': event_privacy_level, 'privacy_level': event_privacy_level,
+25 -17
View File
@@ -1,6 +1,8 @@
import discord import discord
import traceback import traceback
import config
from numpy import array
from classes.config import Config
# ############################################ # ############################################
# the Subscribe() class is a modal ui dialog # the Subscribe() class is a modal ui dialog
@@ -12,23 +14,31 @@ import config
class Subscribe(discord.ui.Modal, title='(un)subscribe to Event-Notification'): 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) def __init__(self, autoEvents : array,member:discord.Member) -> None:
super().__init__()
menu_options = [] # create the menu items from the Event roles:
for menu_option in config.AUTO_EVENTS: menu_options = []
menu_options.append(discord.SelectOption(label= menu_option["notify_hint"], for menu_option in autoEvents:
value=menu_option["subscription_role_num"])) menu_options.append(discord.SelectOption(
# define the UI dropdown menu Element label= menu_option["notify_hint"],
value=menu_option["subscription_role_num"]))
Menu = discord.ui.Select( # define the UI dropdown menu Element
options = menu_options, self.Menu = discord.ui.Select(
max_values=len(config.AUTO_EVENTS), options = menu_options,
min_values=0) max_values=len(autoEvents),
min_values=0)
# Make sure the existing user roles are preselected
for option in self.Menu.options:
role = option.value
if not (member.get_role(role) is None):
option.default=True
# now add the child element to the form for drawing
self.add_item(self.Menu)
# ##################################### # #####################################
@@ -46,7 +56,6 @@ class Subscribe(discord.ui.Modal, title='(un)subscribe to Event-Notification'):
member = interaction.user member = interaction.user
# first we remove all roles # first we remove all roles
for option in self.Menu.options: for option in self.Menu.options:
role=member.get_role(option.value) role=member.get_role(option.value)
if not (role is None): if not (role is None):
@@ -55,7 +64,6 @@ class Subscribe(discord.ui.Modal, title='(un)subscribe to Event-Notification'):
# then we assign all selected roles # then we assign all selected roles
# unfortunately we need to loop through all rolles # unfortunately we need to loop through all rolles
# maybe there is a better solution ? # maybe there is a better solution ?
for option in self.Menu._selected_values: for option in self.Menu._selected_values:
for role in roles: for role in roles:
if int(option) == int(role.id): if int(option) == int(role.id):
+19
View File
@@ -0,0 +1,19 @@
import time
import os
from discord.ext import commands
class Information(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def ping(self, ctx):
before = time.monotonic()
before_ws = int(round(self.bot.latency * 1000, 1))
message = await ctx.send("🏓 Pong")
ping = (time.monotonic() - before) * 1000
await message.edit(content=f"🏓 Pong: {before_ws}ms | REST: {int(ping)}ms")
async def setup(bot):
await bot.add_cog(Information(bot))
-81
View File
@@ -1,81 +0,0 @@
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
# 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_CHANNEL_ID
# the channel where the bot will post messages to
# QUESTION_SLEEPING_TIME (number)
# # Variable that indicates when the bot answers after a question has been asked
# (in scheduler cycles)
# #######################
# "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
# this needs to be an array of dict
+40 -40
View File
@@ -1,44 +1,44 @@
{ {
"secret" : "secret": {
{ "BOT_TOKEN": "YOURBOTTOKEN",
"BOT_TOKEN" : "YOURBOTTOKEN", "CLIENT_ID": "YOURCLIENTID"
"GUILD_ID" : "YOURGUILDID",
"CLIENT_ID" : "YOURCLIENTID",
"AVOID_SPAM" : 1
}, },
"config" : "guilds": {
{ "123456456": {
"CHANNEL_IDLE_INTERVAL": 4, "config": {
"IDLE_MESSAGE_DIR" : "bot_messages", "CHANNEL_IDLE_INTERVAL": 4,
"IDLE_MESSAGE_CHANNEL_ID" : 12345678900, "IDLE_MESSAGE_DIR": "bot_messages",
"QUESTION_SLEEPING_TIME" : 2, "IDLE_MESSAGE_CHANNEL_ID": 12345678900,
"SUPPORT_CHANNEL_ID" : 1234567 "QUESTION_SLEEPING_TIME": 2,
}, "SUPPORT_CHANNEL_ID": 1234567,
"AUTO_EVENTS" : "AVOID_SPAM": 1
[ },
{ "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...", "title": "Sunday Funday session (AM)",
"channel":12345678900, "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...",
"notify_hint":"get notified when the AM session starts", "channel": 12345678900,
"subscription_role":"notify_am", "notify_hint": "get notified when the AM session starts",
"subscription_role_num":12345678900, "subscription_role": "notify_am",
"notify_minutes":30, "subscription_role_num": 12345678900,
"day_of_week":6, "notify_minutes": 30,
"start_time":"09:00:00", "day_of_week": 6,
"end_time":"10:00:00" "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...", "title": "Sunday Funday session (PM)",
"channel":12345678900, "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...",
"notify_hint":"get notified when the PM session starts", "channel": 12345678900,
"subscription_role":"notify_pm", "notify_hint": "get notified when the PM session starts",
"subscription_role_num":12345678900, "subscription_role": "notify_pm",
"notify_minutes":30, "subscription_role_num": 12345678900,
"day_of_week":6, "notify_minutes": 30,
"start_time":"18:00:00", "day_of_week": 6,
"end_time":"19:00:00" "start_time": "18:00:00",
"end_time": "19:00:00"
}
]
} }
] }
} }
+2 -6
View File
@@ -1,8 +1,4 @@
import classes.bot as bot import classes.bot as bot
import config
if config.cfg is None: client = bot.OMFBot()
config.readConfig() client.run()
client = bot.OMFClient()
client.run(config.SECRETS["BOT_TOKEN"])
+9
View File
@@ -0,0 +1,9 @@
{
"secret": {
"BOT_TOKEN": "YOURBOTTOKEN",
"CLIENT_ID": "YOURCLIENTID"
},
"guilds":
{
}
}