@@ -3,6 +3,7 @@
|
|||||||
# Config file with bot secret
|
# Config file with bot secret
|
||||||
|
|
||||||
secret.py
|
secret.py
|
||||||
|
config.json*
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -2,15 +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.3):
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ Talk about tech stuff with the other members
|
|||||||
or just listen in...
|
or just listen in...
|
||||||
|
|
||||||
****Everyone is invited!****
|
****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 ;-)
|
||||||
@@ -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
|
||||||
@@ -2,7 +2,7 @@ Hi, I am R.A.L.F., your assistant bot!
|
|||||||
|
|
||||||
Did you know that we have a ***support channel*** here ?
|
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.
|
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.
|
Please do also check in every now and then to the <#866779182293057566> channel.
|
||||||
|
|||||||
+94
-41
@@ -1,15 +1,21 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
|
||||||
from dis_events import DiscordEvents
|
|
||||||
import random
|
import random
|
||||||
from glob import glob
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import config
|
|
||||||
from secret import BOT_TOKEN, CLIENT_ID, GUILD_ID
|
|
||||||
import utils
|
import utils
|
||||||
from discord.ext import tasks
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import config
|
||||||
|
|
||||||
|
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 classes.dis_events import DiscordEvents
|
||||||
|
from classes.support import Support
|
||||||
|
from classes.subscribe import Subscribe
|
||||||
|
|
||||||
|
|
||||||
# #######################################
|
# #######################################
|
||||||
# The OMFClient class
|
# The OMFClient class
|
||||||
@@ -21,8 +27,11 @@ class OMFClient(discord.Client):
|
|||||||
channel_idle_timer: int
|
channel_idle_timer: int
|
||||||
asked_question = False
|
asked_question = False
|
||||||
last_question: discord.Message = None
|
last_question: discord.Message = None
|
||||||
guild_Events = None
|
|
||||||
lastNotifyTimeStamp = None
|
lastNotifyTimeStamp = None
|
||||||
|
theGuild : discord.Guild = None
|
||||||
|
|
||||||
|
guildEventsList = None
|
||||||
|
guildEventsClass: DiscordEvents = None
|
||||||
|
|
||||||
# #######################################
|
# #######################################
|
||||||
# init constructor
|
# init constructor
|
||||||
@@ -42,8 +51,39 @@ class OMFClient(discord.Client):
|
|||||||
|
|
||||||
self.tree = app_commands.CommandTree(self)
|
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.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
|
# setup_hook waits for the
|
||||||
@@ -62,15 +102,14 @@ class OMFClient(discord.Client):
|
|||||||
async def send_random_message(self):
|
async def send_random_message(self):
|
||||||
print("Sending random message")
|
print("Sending random message")
|
||||||
if self.idle_channel == None:
|
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)}")
|
await self.idle_channel.send(f"{random.choice(self.idle_messages)}")
|
||||||
|
|
||||||
# ######################################################
|
# ######################################################
|
||||||
# on_ready is called once the client is initialized
|
# on_ready is called once the client is initialized
|
||||||
# it then reads in the files in the config.IDLE_MESSAGE_DIR
|
# it then reads in the files in the config.IDLE_MESSAGE_DIR
|
||||||
# directory and posts them randomly every
|
# directory and starts the schedulers
|
||||||
# config.CHANNEL_IDLE_INTERVAL seconds into the
|
|
||||||
# config.IDLE_MESSAGE_CHANNEL_ID channel
|
|
||||||
# ######################################################
|
# ######################################################
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
@@ -82,13 +121,32 @@ class OMFClient(discord.Client):
|
|||||||
|
|
||||||
self.idle_messages = []
|
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)
|
print ("read {}",filename)
|
||||||
with open(filename) as f:
|
with open(filename) as f:
|
||||||
self.idle_messages.append(f.read())
|
self.idle_messages.append(f.read())
|
||||||
|
|
||||||
self.idle_messages = np.array(self.idle_messages)
|
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
|
||||||
|
|
||||||
self.task_scheduler.start()
|
self.task_scheduler.start()
|
||||||
@@ -97,6 +155,7 @@ class OMFClient(discord.Client):
|
|||||||
|
|
||||||
# ######################################################
|
# ######################################################
|
||||||
# handle_ping is called when a user sends ping
|
# handle_ping is called when a user sends ping
|
||||||
|
# (case sensitive, exact phrase)
|
||||||
# it just replies with pong
|
# it just replies with pong
|
||||||
# ######################################################
|
# ######################################################
|
||||||
|
|
||||||
@@ -112,8 +171,6 @@ class OMFClient(discord.Client):
|
|||||||
|
|
||||||
print("Create Events")
|
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:
|
for theEvent in config.AUTO_EVENTS:
|
||||||
|
|
||||||
# calculate the date of the future event
|
# calculate the date of the future event
|
||||||
@@ -132,16 +189,16 @@ 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 newEvent.create_guild_event(
|
await self.guildEventsClass.create_guild_event(
|
||||||
GUILD_ID,
|
event_name=theEvent['title'],
|
||||||
theEvent['title'],
|
event_description=theEvent['description'],
|
||||||
theEvent['description'],
|
event_start_time=f"{strStart}",
|
||||||
f"{strStart}",
|
event_end_time=f"{strEnd}",
|
||||||
f"{strEnd}",
|
event_metadata={},
|
||||||
{},2,theEvent['channel'])
|
event_privacy_level=2,
|
||||||
|
channel_id=theEvent['channel'])
|
||||||
# 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.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"]}')
|
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):
|
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 self.guildEventsClass.list_guild_events()
|
||||||
eventList = await newEvent.list_guild_events(GUILD_ID)
|
self.guildEventsList = eventList
|
||||||
self.guild_Events = eventList
|
|
||||||
return eventList
|
return eventList
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ######################################################
|
# ######################################################
|
||||||
# on_message scans for message contents and takes
|
# on_message scans for message contents and takes
|
||||||
# corresponding actions.
|
# corresponding actions.
|
||||||
@@ -171,15 +225,15 @@ class OMFClient(discord.Client):
|
|||||||
# 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.channel_idle_timer = 0
|
||||||
|
|
||||||
# reply to ping
|
# reply to ping
|
||||||
if message.content == 'ping':
|
if message.content == 'ping':
|
||||||
await self.handle_ping(message)
|
await self.handle_ping(message)
|
||||||
|
|
||||||
# check if there is a question
|
|
||||||
|
|
||||||
|
# check if there is a question
|
||||||
if "?" in message.content:
|
if "?" in message.content:
|
||||||
self.asked_question = True
|
self.asked_question = True
|
||||||
self.last_question = message
|
self.last_question = message
|
||||||
@@ -187,14 +241,14 @@ class OMFClient(discord.Client):
|
|||||||
self.asked_question = False
|
self.asked_question = False
|
||||||
self.last_question = None
|
self.last_question = None
|
||||||
|
|
||||||
|
|
||||||
# ######################################################
|
# ######################################################
|
||||||
# on_typing detects if a user types.
|
# on_typing detects if a user types.
|
||||||
# We might use this one day to have users agree to policies etc.
|
# We might use this one day to have users agree to policies etc.
|
||||||
# before they are allowed to speak
|
# 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, _):
|
async def on_typing(self, channel, user, _):
|
||||||
# we do not want the bot to reply to itself
|
# we do not want the bot to reply to itself
|
||||||
if user.id == self.user.id:
|
if user.id == self.user.id:
|
||||||
@@ -212,8 +266,8 @@ class OMFClient(discord.Client):
|
|||||||
async def daily_tasks(self):
|
async def daily_tasks(self):
|
||||||
print("DAILY TASKS")
|
print("DAILY TASKS")
|
||||||
|
|
||||||
# Every Monday we want to create the scheduled events
|
# Every Monday (weekday 0) we want to create the
|
||||||
# for the next Sunday
|
# scheduled events for the next Sunday
|
||||||
|
|
||||||
if datetime.date.today().weekday() == 0:
|
if datetime.date.today().weekday() == 0:
|
||||||
print("create events")
|
print("create events")
|
||||||
@@ -232,7 +286,7 @@ class OMFClient(discord.Client):
|
|||||||
# ######################################################
|
# ######################################################
|
||||||
|
|
||||||
|
|
||||||
@tasks.loop(minutes=10)
|
@tasks.loop(seconds=10)
|
||||||
async def task_scheduler(self):
|
async def task_scheduler(self):
|
||||||
|
|
||||||
self.channel_idle_timer += 1
|
self.channel_idle_timer += 1
|
||||||
@@ -248,7 +302,7 @@ class OMFClient(discord.Client):
|
|||||||
# TODO - we need to send out a message to the @here role
|
# TODO - we need to send out a message to the @here role
|
||||||
# asking users to help if the question had not been answered
|
# 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
|
# 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")
|
print("QUESTION WITHOUT REPLY")
|
||||||
self.asked_question = False
|
self.asked_question = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -260,7 +314,7 @@ class OMFClient(discord.Client):
|
|||||||
# then send a random message into the idle_channel
|
# then send a random message into the idle_channel
|
||||||
# #####################################
|
# #####################################
|
||||||
try:
|
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
|
self.channel_idle_timer = 0
|
||||||
await self.send_random_message()
|
await self.send_random_message()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -290,11 +344,10 @@ class OMFClient(discord.Client):
|
|||||||
print("Let me check if the event is still on")
|
print("Let me check if the event is still on")
|
||||||
try:
|
try:
|
||||||
if eventList == None:
|
if eventList == None:
|
||||||
newEvent = DiscordEvents(BOT_TOKEN,f'https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&permissions=8&scope=bot')
|
eventList = await self.get_events_list()
|
||||||
eventList = await newEvent.list_guild_events(GUILD_ID)
|
|
||||||
for theScheduledEvent in eventList:
|
for theScheduledEvent in eventList:
|
||||||
if theScheduledEvent["name"] == theEvent["title"]:
|
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"]}'
|
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}")
|
await channel.send(f"{theMessageText}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -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}')
|
||||||
@@ -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()
|
||||||
@@ -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__)
|
||||||
@@ -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__)
|
||||||
@@ -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
|
# the number of scheduler cycles that the channel needs to be idle
|
||||||
# before the bot posts a generic "did you know"
|
# before the bot posts a generic "did you know"
|
||||||
# message
|
# message
|
||||||
|
|
||||||
CHANNEL_IDLE_INTERVAL: int = 4
|
# IDLE_MESSAGE_DIR (path without trailing slash)
|
||||||
|
|
||||||
# the name of the directory where the text files are
|
# the name of the directory where the text files are
|
||||||
# located which contain the messages
|
# located which contain the messages
|
||||||
# which the bot will randomly send
|
# which the bot will randomly send
|
||||||
# (1 file = 1 message)
|
# (1 file = 1 message)
|
||||||
|
|
||||||
IDLE_MESSAGE_DIR: str = "bot_messages"
|
# IDLE_MESSAGE_CHANNEL_ID
|
||||||
|
|
||||||
# the channel where the bot will post messages to
|
# the channel where the bot will post messages to
|
||||||
|
|
||||||
IDLE_MESSAGE_CHANNEL_ID = 758271650226765848
|
# QUESTION_SLEEPING_TIME (number)
|
||||||
|
# # Variable that indicates when the bot answers after a question has been asked
|
||||||
# Variable that indicates when the bot answers after a question has been asked
|
|
||||||
# (in scheduler cycles)
|
# (in scheduler cycles)
|
||||||
|
|
||||||
QUESTION_SLEEPING_TIME = 2
|
# #######################
|
||||||
|
# "AUTO_EVENTS"
|
||||||
|
# #######################
|
||||||
|
|
||||||
# The Auto Events.
|
# The Auto Events.
|
||||||
# this is used in three contexts:
|
# this is used in three contexts:
|
||||||
# 1. Automatic creation of the event
|
# 1. Automatic creation of the event
|
||||||
# 2. Automatic reminder of subscribed users
|
# 2. Automatic reminder of subscribed users
|
||||||
# 3. in the /subscribe command
|
# 3. in the /subscribe command
|
||||||
|
# this needs to be an array of dict
|
||||||
# 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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import bot
|
import classes.bot as bot
|
||||||
import secret
|
import config
|
||||||
|
|
||||||
|
if config.cfg is None:
|
||||||
|
config.readConfig()
|
||||||
|
|
||||||
client = bot.OMFClient()
|
client = bot.OMFClient()
|
||||||
client.run(secret.BOT_TOKEN)
|
client.run(config.SECRETS["BOT_TOKEN"])
|
||||||
|
|||||||
@@ -3,22 +3,12 @@ import datetime
|
|||||||
# ################################################
|
# ################################################
|
||||||
# Returns the date of the next given weekday after
|
# Returns the date of the next given weekday after
|
||||||
# the given date. For example, the date of next Monday.
|
# the given date. For example, the date of next Monday.
|
||||||
#
|
|
||||||
# NB: if it IS the day we're looking for, this returns 0.
|
# NB: if it IS the day we're looking for, this returns 0.
|
||||||
# consider then doing onDay(foo, day + 1).
|
# consider then doing onDay(foo, day + 1).
|
||||||
#
|
|
||||||
# Monday=0, Tuesday=1 .... Sunday=6
|
# Monday=0, Tuesday=1 .... Sunday=6
|
||||||
# ################################################
|
# ################################################
|
||||||
|
|
||||||
|
|
||||||
def onDay(date, day):
|
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
|
days = (day - date.weekday() + 7) % 7
|
||||||
return date + datetime.timedelta(days=days)
|
return date + datetime.timedelta(days=days)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user