Compare commits

..

4 Commits

9 changed files with 1176 additions and 768 deletions

View File

@ -176,27 +176,6 @@ class BotMessages(Model):
database = db
class Resources(Model):
id = AutoField()
guild_id = BigIntegerField()
title = TextField()
description = TextField()
url = TextField()
class Meta:
table_name = "Resources"
database = db
class ResourceTags(Model):
resource_id = ForeignKeyField(Resources, to_field="id")
tag = TextField()
class Meta:
primary_key = CompositeKey("resource_id", "tag")
table_name = "ResourceTags"
database = db
class BadNames(Model):
id = AutoField()

View File

@ -7,6 +7,9 @@ from naff import (
listen,
slash_command,
InteractionContext,
AuditLogEventType,
AuditLogEntry,
RoleSelectMenu,
)
from naff.api import events
from naff.models.discord.embed import (
@ -19,105 +22,128 @@ from naff.models.discord.embed import (
from dotenv import load_dotenv
from database import GuildSettings as GuildSettingsModel, JoinLeave
bot = Client(intents=Intents.ALL, debug_scope=387153131378835456)
class HeimdallrClient(Client):
@listen()
async def on_ready(self):
print("------------------------------------")
print(f"Bot '{bot.user.username}' is ready!")
print(f"This bot is owned by {bot.owner}")
print("------------------------------------")
for guild in bot.guilds:
guild_q = GuildSettingsModel.get_or_none(GuildSettingsModel.guild_id == guild.id)
if guild_q is None:
guild_q = GuildSettingsModel(guild_id=guild.id)
guild_q.save()
@listen()
async def on_ready():
print(f"Bot '{bot.user.username}' is ready!")
print(f"This bot is owned by {bot.owner}")
@listen(events.MemberAdd)
async def on_member_join(self, event: events.MemberAdd):
joinleave_q: JoinLeave
joinleave_q, _ = JoinLeave.get_or_create(guild_id=event.guild.id)
for guild in bot.guilds:
guild_q = GuildSettingsModel.get_or_none(GuildSettingsModel.guild_id == guild.id)
if guild_q is None:
guild_q = GuildSettingsModel(guild_id=guild.id)
guild_q.save()
if joinleave_q.message_channel is None:
return
channel = await bot.fetch_channel(joinleave_q.message_channel)
if not joinleave_q.join_message_enabled:
return
if joinleave_q.join_message is None or joinleave_q.join_message == "":
await channel.send(
f"{event.member.mention} has joined the server!"
)
return
await channel.send(str(joinleave_q.join_message).format(member=event.member, guild=event.guild))
@listen(events.MemberRemove)
async def on_member_leave(self, event: events.MemberRemove):
joinleave_q: JoinLeave
joinleave_q, _ = JoinLeave.get_or_create(guild_id=event.guild.id)
guildsettings_q: GuildSettingsModel
guildsettings_q, _ = GuildSettingsModel.get_or_create(guild_id=event.guild.id)
entry: AuditLogEntry
async for entry in event.guild.audit_log_history(
action_type=AuditLogEventType.MEMBER_KICK,
limit=10
):
if (entry.target_id != event.member.id or
guildsettings_q.admin_channel is None):
continue
channel = await bot.fetch_channel(guildsettings_q.admin_channel)
await channel.send(f"{event.member.mention} was kicked with reason: {entry.reason}")
break
if joinleave_q.message_channel is None:
return
channel = await bot.fetch_channel(joinleave_q.message_channel)
if not joinleave_q.leave_message_enabled:
return
if joinleave_q.leave_message is None or joinleave_q.leave_message == "":
await channel.send(
f"{event.member.mention} has left the server!"
)
return
await channel.send(str(joinleave_q.leave_message).format(member=event.member, guild=event.guild))
# @listen()
# async def on_message_create(event: MessageCreate):
# print(f"{event.message.author.username}: {event.message.content}")
@listen(events.MemberAdd)
async def on_member_join(event: events.MemberAdd):
joinleave_q: JoinLeave
joinleave_q, _ = JoinLeave.get_or_create(guild_id=event.guild.id)
if joinleave_q.message_channel is None:
return
channel = await bot.fetch_channel(joinleave_q.message_channel)
if not joinleave_q.join_message_enabled:
return
if joinleave_q.join_message is None or joinleave_q.join_message == "":
await channel.send(
f"{event.member.mention} has joined the server!"
@slash_command(name="ping", description="Ping the bot")
async def ping_command(self, ctx: InteractionContext):
ctx.ephemeral = True
await ctx.send("Pong!",
components=RoleSelectMenu(placeholder="HONK")
)
return
await channel.send(str(joinleave_q.join_message).format(member=event.member, guild=event.guild))
@listen(events.MemberRemove)
async def on_member_leave(event: events.MemberRemove):
joinleave_q: JoinLeave
joinleave_q, _ = JoinLeave.get_or_create(guild_id=event.guild.id)
if joinleave_q.message_channel is None:
return
channel = await bot.fetch_channel(joinleave_q.message_channel)
if not joinleave_q.leave_message_enabled:
return
if joinleave_q.leave_message is None or joinleave_q.leave_message == "":
await channel.send(
f"{event.member.mention} has left the server!"
@slash_command(name="bot-info", description="Get info about the bot")
async def bot_info_command(self, ctx: InteractionContext):
await ctx.send(
ephemeral=True,
embed=Embed(
title=f"{bot.user.username}",
description=f"This bot is owned by {bot.owner}",
color=0x00FF00,
timestamp=bot.user.created_at,
url=bot.user.avatar.as_url(),
author=EmbedAuthor(
name=bot.user.username,
icon_url=bot.user.avatar.as_url(),
),
footer=EmbedFooter(
text=f"Created at {bot.user.created_at}",
icon_url=bot.user.avatar.as_url(),
),
fields=[
EmbedField(
name="**Extensions**",
value=f"{', '.join(bot.ext.keys())}",
),
EmbedField(
name="**Guilds**",
value="\n".join([g.name for g in sorted(bot.guilds, key=lambda g: g.name)]),
),
],
),
)
return
await channel.send(str(joinleave_q.leave_message).format(member=event.member, guild=event.guild))
bot = HeimdallrClient(intents=Intents.ALL, debug_scope=387153131378835456,
sync_interactions=True,
fetch_members=True,
)
@slash_command(name="ping", description="Ping the bot")
async def ping_command(ctx: InteractionContext):
ctx.ephemeral = True
await ctx.send("Pong!")
@slash_command(name="bot-info", description="Get info about the bot")
async def bot_info_command(ctx: InteractionContext):
await ctx.send(
ephemeral=True,
embed=Embed(
title=f"{bot.user.username}",
description=f"This bot is owned by {bot.owner}",
color=0x00FF00,
timestamp=bot.user.created_at,
url=bot.user.avatar.as_url(),
author=EmbedAuthor(
name=bot.user.username,
icon_url=bot.user.avatar.as_url(),
),
footer=EmbedFooter(
text=f"Created at {bot.user.created_at}",
icon_url=bot.user.avatar.as_url(),
),
fields=[
EmbedField(
name="**Extensions**",
value=f"{', '.join(bot.ext.keys())}",
),
EmbedField(
name="**Guilds**",
value="\n".join([g.name for g in sorted(bot.guilds, key=lambda g: g.name)]),
),
],
),
)
def set_loglevel(level: str):
loglevel = logging.WARNING
@ -158,7 +184,8 @@ def main():
bot.load_extension("heimdallr.commands.self_roles")
bot.load_extension("heimdallr.commands.polls")
bot.load_extension("heimdallr.commands.bot_messages")
bot.load_extension("heimdallr.commands.modmail")
bot.start(getenv("DISCORD_TOKEN"))
if __name__ == "__main__":
main()
main()

View File

@ -0,0 +1,244 @@
"""Deprecated module
This was intended to be the resources module. A user-submitted, curated directory of
resources for a guild. For the time being, forum channels serve as enough of an equivalent
for the needs of NLL. So this is to be considered deprecated.
"""
from naff import (
slash_command,
slash_option,
Extension,
Client,
Permissions,
InteractionContext,
OptionTypes,
Embed,
Member,
EmbedField,
EmbedAuthor,
Modal,
InputText,
TextStyles,
ChannelTypes,
GuildText,
)
import logging
from database import (
Resources as ResourcesModel,
ResourceTags as ResourceTagsModel,
ResourceAuthors as ResourceAuthorsModel,
ResourcesUnpublished as ResourcesUnpublishedModel,
ResourceChannels as ResourceChannelsModel,
)
class Resources(Extension):
def __init__(self, client: Client) -> None:
self.client = client
@slash_command(
name="adm",
group_name="set",
group_description="Set settings for this guild",
sub_cmd_name="submitted_resources_channel",
sub_cmd_description="Set whether or not to use the name filter",
dm_permission=False,
default_member_permissions=Permissions.MANAGE_GUILD,
)
@slash_option(
name="channel",
description="The channel in which submitted/unpublished resources should appear",
opt_type=OptionTypes.CHANNEL,
channel_types=[ChannelTypes.GUILD_TEXT],
)
async def adm_set_submitted_resources_channel(
self, ctx: InteractionContext, channel: GuildText|None = None
):
if channel is None:
resource_channel = ResourceChannelsModel.get_or_none(guild_id=ctx.guild.id)
if resource_channel is not None:
resource_channel.delete_instance()
await ctx.send(ephemeral=True, content="Submitted resource channel unset.")
return
resource_channel: ResourceChannelsModel
resource_channel, _ = ResourceChannelsModel.get_or_create(guild_id=ctx.guild.id)
resource_channel.channel_id = channel.id
resource_channel.save()
await ctx.send(ephemeral=True,
content=f"Submitted resource channel set to {channel.mention}."
)
@slash_command(
name="resources",
description="Get or add resources",
default_member_permissions=Permissions.SEND_MESSAGES,
dm_permission=False,
)
async def resources(self, ctx: InteractionContext):
...
@resources.subcommand(
sub_cmd_name="list",
sub_cmd_description="List resources",
)
@slash_option(
name="page",
description="Page of resources to show",
opt_type=OptionTypes.INTEGER,
required=False,
)
async def resources_list(self, ctx: InteractionContext, page: int = 1):
await ctx.defer(ephemeral=True)
LIMIT = 10
resources: list[ResourcesModel] = (
ResourcesModel.select()
.paginate(page, LIMIT)
.order_by(ResourcesModel.id.asc())
)
embeds: list[Embed] = []
for resource in resources:
resource_authors: list[
ResourceAuthorsModel
] = ResourceAuthorsModel.select().where(
ResourceAuthorsModel.resource_id == resource.id
)
authors: list[str] = []
for author in resource_authors:
member = await self.client.fetch_member(author.user_id, ctx.guild_id)
if member is None:
authors.append(author.name)
else:
authors.append(
f"{member.display_name} ({member.username}#{member.discriminator})"
)
embed = Embed(title=resource.title, description=resource.description)
embed.add_field("**Authors**", "\n".join(authors))
no_resources = ResourcesModel.select().count()
pages = no_resources // LIMIT
if no_resources % LIMIT != 0:
pages += 1
await ctx.send(ephemeral=True, content=f"Page {page} of {pages}", embeds=embeds)
@resources.subcommand(
sub_cmd_name="submit",
sub_cmd_description="Submit a resource for approval",
)
async def resources_submit(self, ctx: InteractionContext):
modal_wait = await ctx.send_modal(
Modal(
title="New resource",
components=[
InputText(
label="Title",
custom_id="title",
style=TextStyles.SHORT,
placeholder="A title!",
min_length=8,
max_length=64,
),
InputText(
label="Content",
custom_id="content",
style=TextStyles.PARAGRAPH,
placeholder="A description of the resource.",
min_length=16,
),
InputText(
label="Tags",
custom_id="tags",
style=TextStyles.SHORT,
placeholder="Tags separated by spaces; e.g: grammar language-dialects tag",
max_length=128,
),
],
)
)
modal_ctx = await self.client.wait_for_modal(modal_wait, author=ctx.author.id)
await modal_ctx.defer(ephemeral=True)
title = modal_ctx.responses.get("title")
content = modal_ctx.responses.get("content")
tags = modal_ctx.responses.get("tags")
if title is None:
await modal_ctx.send("Title cannot be empty.")
return
if content is None:
await modal_ctx.send("Content cannot be empty.")
return
resource: ResourcesUnpublishedModel = ResourcesUnpublishedModel.create(
guild_id=modal_ctx.guild.id,
title=title,
description=content,
tags=tags,
author=modal_ctx.author.id,
)
await modal_ctx.send(f"Your resource *{title}* has been submitted!")
await self.__post_to_resources_submission_channel(resource)
async def __post_to_resources_submission_channel(self, resource: ResourcesUnpublishedModel):
res_unpublished_channel: ResourceChannelsModel | None = ResourceChannelsModel.get_or_none(
ResourceChannelsModel.guild_id == resource.guild_id
)
if res_unpublished_channel is None:
return # No unpublished resources channel has been set.
channel_id = int(res_unpublished_channel.channel_id)
channel = await self.client.fetch_channel(channel_id)
guild_id = int(resource.guild_id)
user_id = int(resource.author)
author: Member | None = await self.client.fetch_member(guild_id=guild_id, user_id=user_id)
author_text = None
author_icon_url = None
if author is None:
author_text = f"<@{int(resource.author)}>"
else:
author_icon_url = author.display_avatar.as_url()
if author.display_name != author.username:
author_text = f"{author.display_name} ({author.username}#{author.discriminator})"
else:
author_text = f"{author.username}#{author.discriminator}"
await channel.send(
embed=Embed(
title=str(resource.title),
description=str(resource.description),
author=EmbedAuthor(
name=author_text,
icon_url=author_icon_url,
)
)
)
def setup(bot: Client):
ResourcesModel.create_table(safe=True)
ResourceTagsModel.create_table(safe=True)
ResourcesUnpublishedModel.create_table(safe=True)
ResourceChannelsModel.create_table(safe=True)
Resources(bot)
logging.info("Resources extension loaded")

View File

@ -351,9 +351,9 @@ class Gatekeep(Extension):
),
)
@listen(events.Button)
async def on_regenerate_button(self, button: events.Button):
ctx = button.context
@listen(events.ButtonPressed)
async def on_regenerate_button(self, button: events.ButtonPressed):
ctx = button.ctx
member = ctx.author
if (

View File

@ -0,0 +1,142 @@
import logging
import uuid
from database import GuildSettings
from naff import (
Extension,
Client,
slash_command,
slash_option,
InteractionContext,
ModalContext,
Permissions,
OptionTypes,
Button,
ButtonStyles,
listen,
events,
Modal,
InputText,
TextStyles,
Role,
Embed,
EmbedAuthor,
)
class ModMail(Extension):
def __init__(self, client: Client):
self.client: Client = client
@slash_command(
name="adm",
sub_cmd_name="create-modmail-button",
sub_cmd_description="Create a button for users to create a modmail thread.",
dm_permission=False,
default_member_permissions=Permissions.MANAGE_GUILD,
)
@slash_option(
name="button-text",
description="The text that should be displayed on the button.",
opt_type=OptionTypes.STRING,
required=True
)
@slash_option(
name="notify-role",
description="Role that should be tagged upon creation of the modmail thread.",
opt_type=OptionTypes.ROLE,
required=True
)
async def adm_create_modmail_button(self, ctx: InteractionContext, button_text: str, role: Role):
await ctx.defer(ephemeral=True)
await ctx.channel.send(
components=Button(
style=ButtonStyles.GREEN,
label=button_text,
custom_id=f"send-modmail-button:{role.id}"
)
)
await ctx.send("Button created!", ephemeral=True)
@listen(events.ButtonPressed)
async def on_button(self, button: events.ButtonPressed):
ctx = button.ctx
if not ctx.custom_id.startswith("send-modmail-button"):
return
(_, role) = ctx.custom_id.split(":")
if role is None:
return
role = await ctx.guild.fetch_role(role)
if role is None:
return
modmail_modal = Modal(
title="Send modmail",
custom_id=f"modmail-modal:{uuid.uuid4()}",
components=[
InputText(
label="Topic",
style=TextStyles.SHORT,
custom_id="modmail_title",
required=True,
min_length=5,
max_length=100,
placeholder="Title or topic of the modmail"
),
InputText(
label="Message",
style=TextStyles.PARAGRAPH,
custom_id="modmail_body",
required=True,
min_length=24,
max_length=2000,
)
]
)
await ctx.send_modal(modmail_modal)
modal_ctx: ModalContext = await self.client.wait_for_modal(modmail_modal)
thread = await ctx.channel.create_private_thread(
name=modal_ctx.responses["modmail_title"],
invitable=True,
reason=(
f"{ctx.author.username}#{ctx.author.discriminator} "
"created a support thread."
)
)
msg = await thread.send(
content=f"{role.mention} {ctx.author.mention}",
embed=Embed(
title=modal_ctx.responses["modmail_title"],
description=modal_ctx.responses["modmail_body"],
author=EmbedAuthor(
name=(
f"{ctx.author.nickname} "
f"({ctx.author.username}#{ctx.author.discriminator})"
),
icon_url=ctx.author.avatar.as_url()
)
)
)
if msg:
await modal_ctx.send(
"Message created",
ephemeral=True,
components=Button(
style=ButtonStyles.LINK,
label="Go to message",
url=msg.jump_url,
)
)
def setup(client: Client):
ModMail(client)
logging.info(f"{ModMail.__name__} extension loaded.")

View File

@ -308,9 +308,9 @@ class Polls(Extension):
poll_entry.channel_id = poll_message.channel.id
poll_entry.save()
@listen(events.Button)
async def on_button(self, button: events.Button): #pylint: disable=too-many-branches,too-many-statements
ctx = button.context
@listen(events.ButtonPressed)
async def on_button(self, button: events.ButtonPressed): #pylint: disable=too-many-branches,too-many-statements
ctx = button.ctx
# Ensure that the pressed button is a vote button.
if ctx.custom_id.startswith("poll-vote:"):

View File

@ -12,7 +12,7 @@ from naff import (
Role,
Embed,
AutocompleteContext,
Select,
StringSelectMenu,
SelectOption,
listen,
context_menu,
@ -436,7 +436,7 @@ class SelfRoles(Extension):
max_vals = 1 if group_q.exclusive else len(options)
select = Select(
select = StringSelectMenu(
options=options,
placeholder=f"Select roles from the group {group}",
custom_id=f"role-group-assign:{group}",
@ -552,7 +552,7 @@ class SelfRoles(Extension):
or msg.embeds[0].title is None
or len(msg.components) < 1
or len(msg.components[0].components) < 1
or not isinstance(msg.components[0].components[0], Select)
or not isinstance(msg.components[0].components[0], StringSelectMenu)
or not msg.components[0].components[0].custom_id.startswith("role-group-assign")
):
await ctx.send("Could not identify this as a role-group message!", ephemeral=True)
@ -600,7 +600,7 @@ class SelfRoles(Extension):
max_vals = 1 if group_q.exclusive else len(options)
select = Select(
select = StringSelectMenu(
options=options,
placeholder=f"Select roles from the group {group}",
custom_id=f"role-group-assign:{group}",

1316
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ heimdallr = "heimdallr.Heimdallr:main"
[tool.poetry.dependencies]
python = "^3.10"
peewee = "^3.15.1"
naff = "^1.11.1"
naff = "2.1.0"
orjson = "^3.7.8"
python-dotenv = "^0.20.0"
captcha = "^0.4"