cogs, shardable, setup, update, say_ralf commands
Signed-off-by: Marc Ahlgrim <marc@onemarcfifty.com>
This commit is contained in:
@@ -2,16 +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.4):
|
||||
|
||||
- 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
|
||||
|
||||
## How to use
|
||||
@@ -22,10 +18,45 @@ You need the discord.py wrapper from Rapptz :
|
||||
cd discord.py/
|
||||
python3 -m pip install -U .[voice]
|
||||
|
||||
Next, adapt the `secret.py` file to reflect your token etc.
|
||||
Also, customize all settings in `config.py` and the text files in the
|
||||
`bot_messages`directory
|
||||
Next, adapt the `config.json` file to reflect your token etc.
|
||||
|
||||
Now you can cd into the bot's directory and launch it with
|
||||
|
||||
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.
|
||||
@@ -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 ;-)
|
||||
@@ -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
|
||||
@@ -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 ?
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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 ?
|
||||
@@ -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
@@ -1,19 +1,19 @@
|
||||
import discord
|
||||
import random
|
||||
import numpy as np
|
||||
import utils
|
||||
import datetime
|
||||
import config
|
||||
import os
|
||||
import ast
|
||||
|
||||
from glob import glob
|
||||
from dateutil import tz
|
||||
from sys import exit
|
||||
|
||||
from discord import app_commands
|
||||
from discord.ext import tasks
|
||||
from discord.ext.commands import Bot,AutoShardedBot
|
||||
|
||||
from classes.dis_events import DiscordEvents
|
||||
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
|
||||
lastNotifyTimeStamp = None
|
||||
theGuild : discord.Guild = None
|
||||
# each guild has the following elements:
|
||||
# - a list of scheduled Events (EventsList)
|
||||
# - 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
|
||||
guildEventsClass: DiscordEvents = None
|
||||
# the guildDataList contains one GuildData class per item.
|
||||
# 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
|
||||
# #######################################
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
print('Init')
|
||||
|
||||
# Try to set all intents
|
||||
|
||||
intents = discord.Intents.all()
|
||||
super().__init__(intents=intents)
|
||||
|
||||
# We need a `discord.app_commands.CommandTree` instance
|
||||
# to register application commands (slash commands in this case)
|
||||
|
||||
self.tree = app_commands.CommandTree(self)
|
||||
super().__init__(command_prefix="!",intents=intents)
|
||||
self.prefix="!"
|
||||
self.configData=Config('config.json')
|
||||
|
||||
# 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()
|
||||
guildNode=self.configData.readGuild(interaction.guild.id)
|
||||
member = interaction.user
|
||||
|
||||
for option in x.Menu.options:
|
||||
role = option.value
|
||||
if not (member.get_role(role) is None):
|
||||
option.default=True
|
||||
x=Subscribe(autoEvents=guildNode["AUTO_EVENTS"],member=member)
|
||||
await interaction.response.send_modal(x)
|
||||
|
||||
self.channel_idle_timer = 0
|
||||
self.idle_channel = self.get_channel(config.CONFIG["IDLE_MESSAGE_CHANNEL_ID"])
|
||||
# The setup command will ask for the guild parameters and
|
||||
# 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
|
||||
# command tree to sync
|
||||
# and loads the cogs
|
||||
# #########################
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
# Sync the application command with Discord.
|
||||
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
|
||||
# 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")
|
||||
if self.idle_channel == None:
|
||||
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)}")
|
||||
idle_channel_id=guildNode["IDLE_MESSAGE_CHANNEL_ID"]
|
||||
idle_channel=self.get_channel(idle_channel_id)
|
||||
gdn:self.GuildData
|
||||
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
|
||||
@@ -105,37 +247,18 @@ class OMFClient(discord.Client):
|
||||
async def on_ready(self):
|
||||
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
|
||||
# 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 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"]
|
||||
)
|
||||
for theGuild in self.guilds:
|
||||
await self.readMessageTemplates(theGuild)
|
||||
|
||||
# start the schedulers
|
||||
|
||||
@@ -149,11 +272,14 @@ class OMFClient(discord.Client):
|
||||
# for the next sunday
|
||||
# ######################################################
|
||||
|
||||
async def create_events (self):
|
||||
async def create_events (self,theGuild):
|
||||
|
||||
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
|
||||
theDate:datetime.datetime = utils.onDay(datetime.date.today(),theEvent['day_of_week'])
|
||||
@@ -171,16 +297,18 @@ class OMFClient(discord.Client):
|
||||
# after 2 AM
|
||||
strStart=theDate.strftime(f"%Y-%m-%dT{utcStartTime}")
|
||||
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_description=theEvent['description'],
|
||||
event_start_time=f"{strStart}",
|
||||
event_end_time=f"{strEnd}",
|
||||
event_metadata={},
|
||||
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
|
||||
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"]}')
|
||||
|
||||
|
||||
@@ -188,9 +316,10 @@ class OMFClient(discord.Client):
|
||||
# get_event_list gives a list of scheduled events
|
||||
# ######################################################
|
||||
|
||||
async def get_events_list (self):
|
||||
eventList = await self.guildEventsClass.list_guild_events()
|
||||
self.guildEventsList = eventList
|
||||
async def get_events_list (self,theGuild: discord.Guild):
|
||||
|
||||
eventList = await self.EventsClass.list_guild_events(theGuild.id)
|
||||
self.GuildData(self.guildDataList[f'{theGuild.id}']).EventsList = eventList
|
||||
return eventList
|
||||
|
||||
# ######################################################
|
||||
@@ -207,30 +336,16 @@ class OMFClient(discord.Client):
|
||||
if message.flags.ephemeral:
|
||||
return
|
||||
|
||||
|
||||
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
|
||||
if message.author == self.user:
|
||||
return
|
||||
|
||||
# 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:
|
||||
print("create events")
|
||||
try:
|
||||
await self.create_events()
|
||||
except Exception as e:
|
||||
print(f"Daily Task create Events failed: {e}")
|
||||
for theGuild in self.guilds:
|
||||
try:
|
||||
await self.create_events(theGuild=theGuild)
|
||||
except Exception as e:
|
||||
print(f"Daily Task create Events failed: {e}")
|
||||
|
||||
|
||||
# ######################################################
|
||||
@@ -281,50 +397,60 @@ class OMFClient(discord.Client):
|
||||
@tasks.loop(minutes=10)
|
||||
async def task_scheduler(self):
|
||||
|
||||
self.channel_idle_timer += 1
|
||||
print("SCHEDULE")
|
||||
|
||||
# #####################################
|
||||
# see if we need to send a random message
|
||||
# if the counter is greater than CHANNEL_IDLE_INTERVAL
|
||||
# then send a random message into the idle_channel
|
||||
# #####################################
|
||||
try:
|
||||
if self.channel_idle_timer >= config.CONFIG["CHANNEL_IDLE_INTERVAL"]:
|
||||
self.channel_idle_timer = 0
|
||||
await self.send_random_message()
|
||||
except Exception as e:
|
||||
print(f"Scheduler random_message failed: {e}")
|
||||
for theGuild in self.guilds:
|
||||
guildNode=self.configData.readGuild(theGuild.id)
|
||||
if guildNode is None:
|
||||
print (f"Guild {theGuild.id} has no setup")
|
||||
continue
|
||||
gdn:self.GuildData
|
||||
gdn=self.guildDataList[f'{theGuild.id}']
|
||||
gdn.channel_idle_timer += 1
|
||||
|
||||
# see if we need to send out notifications for events
|
||||
# The Event details are stored in config.
|
||||
eventList=None
|
||||
for theEvent in config.AUTO_EVENTS:
|
||||
# first let's convert the String dates to datetime:
|
||||
theDate=utils.onDay(datetime.date.today(),theEvent['day_of_week'])
|
||||
startTime=theEvent['start_time']
|
||||
eventScheduledTimeDate=datetime.datetime.fromisoformat (theDate.strftime(f"%Y-%m-%dT{startTime}"))
|
||||
# now let's figure out the time deltas:
|
||||
timeUntilEvent=eventScheduledTimeDate - datetime.datetime.today()
|
||||
earliestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"]) - datetime.timedelta(minutes=10)
|
||||
latestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"])
|
||||
#print("found scheduled event")
|
||||
#print(f"The event is on {theDate} at {startTime} - that is in {timeUntilEvent}")
|
||||
#print (f"we ping between {earliestPing} and {latestPing}")
|
||||
# If we are in the interval then let's initiate the reminder
|
||||
if (earliestPing < datetime.datetime.today() <= latestPing):
|
||||
# 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()
|
||||
for theScheduledEvent in eventList:
|
||||
if theScheduledEvent["name"] == theEvent["title"]:
|
||||
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:
|
||||
print(f"Scheduler event_reminder failed: {e}")
|
||||
# #####################################
|
||||
# see if we need to send a random message
|
||||
# if the counter is greater than CHANNEL_IDLE_INTERVAL
|
||||
# then send a random message into the idle_channel
|
||||
# #####################################
|
||||
|
||||
try:
|
||||
if gdn.channel_idle_timer >= guildNode["CHANNEL_IDLE_INTERVAL"]:
|
||||
gdn.channel_idle_timer = 0
|
||||
await self.send_random_message(guildID=theGuild.id)
|
||||
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.
|
||||
eventList=None
|
||||
|
||||
for theEvent in guildNode["AUTO_EVENTS"]:
|
||||
# first let's convert the String dates to datetime:
|
||||
theDate=utils.onDay(datetime.date.today(),theEvent['day_of_week'])
|
||||
startTime=theEvent['start_time']
|
||||
eventScheduledTimeDate=datetime.datetime.fromisoformat (theDate.strftime(f"%Y-%m-%dT{startTime}"))
|
||||
# now let's figure out the time deltas:
|
||||
timeUntilEvent=eventScheduledTimeDate - datetime.datetime.today()
|
||||
earliestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"]) - datetime.timedelta(minutes=10)
|
||||
latestPing=eventScheduledTimeDate - datetime.timedelta(minutes=theEvent["notify_minutes"])
|
||||
#print("found scheduled event")
|
||||
#print(f"The event is on {theDate} at {startTime} - that is in {timeUntilEvent}")
|
||||
#print (f"we ping between {earliestPing} and {latestPing}")
|
||||
# If we are in the interval then let's initiate the reminder
|
||||
if (earliestPing < datetime.datetime.today() <= latestPing):
|
||||
# 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}")
|
||||
|
||||
@@ -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
@@ -10,24 +10,21 @@ from classes.restfulapi import DiscordAPI
|
||||
|
||||
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}")
|
||||
# def __init__(self,
|
||||
# discord_token: str,
|
||||
# client_id: str,
|
||||
# bot_permissions: int,
|
||||
# api_version: int) -> None:
|
||||
#
|
||||
# super().__init__(discord_token,client_id,bot_permissions,api_version)
|
||||
|
||||
|
||||
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
|
||||
# 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)
|
||||
return response_list
|
||||
|
||||
@@ -39,7 +36,8 @@ class DiscordEvents(DiscordAPI):
|
||||
event_end_time: str,
|
||||
event_metadata: dict,
|
||||
event_privacy_level=2,
|
||||
channel_id=None
|
||||
channel_id=None,
|
||||
guild_id=0
|
||||
) -> None:
|
||||
|
||||
# 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
|
||||
# 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({
|
||||
'name': event_name,
|
||||
'privacy_level': event_privacy_level,
|
||||
|
||||
+25
-17
@@ -1,6 +1,8 @@
|
||||
import discord
|
||||
import traceback
|
||||
import config
|
||||
|
||||
from numpy import array
|
||||
from classes.config import Config
|
||||
|
||||
# ############################################
|
||||
# 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'):
|
||||
|
||||
# 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 = []
|
||||
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
|
||||
# create the menu items from the Event roles:
|
||||
menu_options = []
|
||||
for menu_option in autoEvents:
|
||||
menu_options.append(discord.SelectOption(
|
||||
label= menu_option["notify_hint"],
|
||||
value=menu_option["subscription_role_num"]))
|
||||
|
||||
Menu = discord.ui.Select(
|
||||
options = menu_options,
|
||||
max_values=len(config.AUTO_EVENTS),
|
||||
min_values=0)
|
||||
# define the UI dropdown menu Element
|
||||
self.Menu = discord.ui.Select(
|
||||
options = menu_options,
|
||||
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
|
||||
|
||||
# first we remove all roles
|
||||
|
||||
for option in self.Menu.options:
|
||||
role=member.get_role(option.value)
|
||||
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
|
||||
# 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):
|
||||
|
||||
@@ -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))
|
||||
@@ -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
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"secret" :
|
||||
{
|
||||
"BOT_TOKEN" : "YOURBOTTOKEN",
|
||||
"GUILD_ID" : "YOURGUILDID",
|
||||
"CLIENT_ID" : "YOURCLIENTID",
|
||||
"AVOID_SPAM" : 1
|
||||
"secret": {
|
||||
"BOT_TOKEN": "YOURBOTTOKEN",
|
||||
"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"
|
||||
"guilds": {
|
||||
"123456456": {
|
||||
"config": {
|
||||
"CHANNEL_IDLE_INTERVAL": 4,
|
||||
"IDLE_MESSAGE_DIR": "bot_messages",
|
||||
"IDLE_MESSAGE_CHANNEL_ID": 12345678900,
|
||||
"QUESTION_SLEEPING_TIME": 2,
|
||||
"SUPPORT_CHANNEL_ID": 1234567,
|
||||
"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...",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import classes.bot as bot
|
||||
import config
|
||||
|
||||
if config.cfg is None:
|
||||
config.readConfig()
|
||||
|
||||
client = bot.OMFClient()
|
||||
client.run(config.SECRETS["BOT_TOKEN"])
|
||||
client = bot.OMFBot()
|
||||
client.run()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"secret": {
|
||||
"BOT_TOKEN": "YOURBOTTOKEN",
|
||||
"CLIENT_ID": "YOURCLIENTID"
|
||||
},
|
||||
"guilds":
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user