Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ errloc = '../data/logs/err.log'
# All ID's should be int!
guild = # Put your guild/server ID here
logch = # Put your log channel ID here (this is the channel where all logs will be dumped)
infrepch = # Put your infraction report channel ID here (this is the channel where all infraction reports will be send)

# Server roles
mutedrole = # Put the muted role ID here. This role will be assigned when the mute command is used
Expand Down
102 changes: 74 additions & 28 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from models import errors
from models.colors import COLOR
from models.measure import Measure
from models.Infraction import Infraction
from modules import db
from modules import log
from modules import markdown
Expand Down Expand Up @@ -67,7 +68,8 @@ async def update_stats(self):
if u is None:
await log.log(self.bot, "Cannot lift mute, member probably left", to_channel=True, footertxt=f"User ID: {case}", color=COLOR.WARN.value)
else:
u.remove_roles(mutedr, reason='Automatically lifted (NULL ERROR) by bot')
u.remove_roles(
mutedr, reason='Automatically lifted (NULL ERROR) by bot')
await log.log(self.bot, f"Mute on {u} lifted (NULL ERROR).", to_channel=True, footertxt=f"User ID: {case}", color=COLOR.ATTENTION_WARN.value)

except Exception:
Expand All @@ -81,9 +83,8 @@ async def update_stats(self):
# Global functions

def in_dm(ctx):

"""Checks if a message was sent in DMs

Required parameters:
- ctx: discord.Context object

Expand All @@ -95,12 +96,31 @@ def in_dm(ctx):
return False


async def report_infraction(bot: discord.ext.commands.Bot, infraction: Infraction):
"""Logs the infraction to the infraction report channel

Required parameters:
- bot: discord.ext.commands.Bot object
- infraction: models.Infraction.Infraction object

Returns:
None
"""
# Get the channel the report should be send to
ch = bot.get_guild(config.guild).get_channel(config.infrepch)

# Send the infraction
await ch.send(embed=str(infraction))
del ch

# region mod commands

# This decorator adds the command to the command list


@bot.command()
@commands.check_any(commands.has_any_role(*config.elevated_roles), commands.is_owner())
# The function name is the name of the command, unless specified.
# The function name is the name of the command, unless specified.
async def ban(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: str = "No reason supplied; Pluto Mod Bot"):
# Check if the musr object was properly parsed as a User object
if isinstance(musr, discord.Member):
Expand All @@ -113,6 +133,8 @@ async def ban(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: st
return await ctx.send("_🚫 You can't ban invincible users_")
# Put it in the database
db.AddInfraction(musr.id, Measure.BAN, reason, ctx.author.id)
report_infraction(bot, Infraction(
musr.id, Measure.BAN, reason, ctx.author.id))

await musr.send(f"You were banned from {ctx.guild} • {reason}")

Expand All @@ -131,7 +153,6 @@ async def ban(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: st
await ctx.send("🚫 Couldn't parse user properly")



@bot.command()
@commands.check_any(commands.has_any_role(*config.elevated_roles), commands.is_owner())
async def kick(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: str = None):
Expand All @@ -149,6 +170,8 @@ async def kick(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: s

# Put it in the database
db.AddInfraction(musr.id, Measure.KICK, reason, ctx.author.id)
report_infraction(bot, Infraction(
musr.id, Measure.KICK, reason, ctx.author.id))

# Add it to the recentrmv list
bot.recentrmv.append(musr.id)
Expand All @@ -166,6 +189,7 @@ async def kick(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: s
else:
await ctx.send("🚫 Couldn't parse user properly")


@bot.command()
@commands.check_any(commands.has_any_role(*config.elevated_roles), commands.is_owner())
async def mute(ctx, musr: typing.Union[discord.Member, str] = None, duration: str = "30m", *, reason: str = "No reason supplied"):
Expand All @@ -174,9 +198,9 @@ async def mute(ctx, musr: typing.Union[discord.Member, str] = None, duration: st
# ignore if self
if ctx.author == musr:
return

alts = db.GetAlts(musr.id)

# Fail if user is invincible:
if(len([r for r in musr.roles if r.id in config.invincibleroles]) > 0):
await ctx.send("_🚫 You can't mute invincible users_")
Expand All @@ -190,13 +214,16 @@ async def mute(ctx, musr: typing.Union[discord.Member, str] = None, duration: st

# Put it in the database
db.AddInfraction(musr.id, Measure.MUTE, reason, ctx.author.id)
report_infraction(bot, Infraction(
musr.id, Measure.MUTE, reason, ctx.author.id))

# Try to get the role
mr = ctx.guild.get_role(config.mutedrole)

# Bot couldn't find the correct role
if mr is None:
raise errors.RoleNotFoundError("Muted role not found!", "Update ID in config file")
raise errors.RoleNotFoundError(
"Muted role not found!", "Update ID in config file")

try:
ti = markdown.add_time_from_str(duration)
Expand All @@ -207,7 +234,6 @@ async def mute(ctx, musr: typing.Union[discord.Member, str] = None, duration: st
# Assign the muted role
await musr.add_roles(mr, reason=reason)


await musr.send(f"You were muted in {ctx.guild} for {markdown.duration_to_text(duration)} • {reason}")

# Log it
Expand All @@ -218,6 +244,7 @@ async def mute(ctx, musr: typing.Union[discord.Member, str] = None, duration: st
else:
await ctx.send("🚫 Couldn't parse user properly")


@bot.command()
@commands.check_any(commands.has_any_role(*config.elevated_roles), commands.is_owner())
async def unmute(ctx, musr: typing.Union[discord.Member, str]):
Expand Down Expand Up @@ -259,6 +286,8 @@ async def warn(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: s

# Put it in the database
db.AddInfraction(musr.id, Measure.WARN, reason, ctx.author.id)
report_infraction(bot, Infraction(
musr.id, Measure.WARN, reason, ctx.author.id))

# Log it
await log.log(bot, f"{musr.mention} was warned by {ctx.author.mention} with reason: {reason}", to_channel=True, footertxt=f"User ID: {musr.id}", color=COLOR.ATTENTION_WARN.value)
Expand All @@ -268,6 +297,7 @@ async def warn(ctx, musr: typing.Union[discord.Member, str] = None, *, reason: s
else:
await ctx.send("🚫 Couldn't parse user properly")


@bot.command()
@commands.check_any(commands.has_any_role(*config.elevated_roles), commands.is_owner())
async def purge(ctx, amount: int = 50):
Expand All @@ -287,18 +317,22 @@ async def whois(ctx, musr: typing.Union[discord.Member, str] = None):
if(musr == None):
musr = ctx.author
if isinstance(musr, discord.Member):
embed = discord.Embed(title="WHOIS", description=f"<@{musr.id}>", color=0x469eff)
embed = discord.Embed(
title="WHOIS", description=f"<@{musr.id}>", color=0x469eff)
embed.set_author(name="Pluto's Shitty Mod Bot")
embed.set_thumbnail(url=f"{str(musr.avatar_url)}")
embed.add_field(name="Username", value=f"{musr}", inline=True)
embed.add_field(name="Registered", value=f"{str(musr.created_at)}", inline=True)
embed.add_field(name="Registered",
value=f"{str(musr.created_at)}", inline=True)
if not in_dm(ctx):
embed.add_field(name="Nickname", value=f"{musr.nick}", inline=True)
embed.add_field(name="Joined", value=f"{str(musr.joined_at)}", inline=True)
embed.add_field(
name="Joined", value=f"{str(musr.joined_at)}", inline=True)

embed.set_footer(text=f"User ID: {musr.id}")
else:
embed = discord.Embed(title="WHOIS", description=f"<@{musr}>", color=0x469eff)
embed = discord.Embed(
title="WHOIS", description=f"<@{musr}>", color=0x469eff)
embed.set_author(name="Pluto's Shitty Mod Bot")

# Check if the author has elevated permissions
Expand All @@ -309,7 +343,7 @@ async def whois(ctx, musr: typing.Union[discord.Member, str] = None):

# Get all infractions and convert it into a markdown format
if isinstance(musr, str):
# if the argument provided was not automatically converted to discord.Member, try to parse it to an id (int)
# if the argument provided was not automatically converted to discord.Member, try to parse it to an id (int)
try:
md1 = markdown.infr_data_to_md(db.GetAllInfractions(int(musr)))
md2 = markdown.alt_data_to_md(bot, db.GetAlts(int(musr)))
Expand Down Expand Up @@ -348,7 +382,6 @@ async def on_member_ban(guild, user):
# Put it in the database
db.AddInfraction(user.id, Measure.BAN, reason, 0)


await log.log(bot, f"{user} was banned with reason: {reason}", to_channel=True, footertxt=f"User ID: {user.id}", color=COLOR.ATTENTION_BAD.value)


Expand All @@ -362,7 +395,7 @@ async def infraction(ctx, id: str, *, cmd: str = None):
if len(res) < 1:
return await ctx.send("🚫 Didn't find any infractions")

# if delete argument was supplied with the command
# if delete argument was supplied with the command
if cmd == 'delete':
# If just one result is found, delete it
if len(res) == 1:
Expand All @@ -374,7 +407,8 @@ async def infraction(ctx, id: str, *, cmd: str = None):
return await log.log(bot, f"{ctx.author.mention} deleted infraction {res[0][0]}", to_channel=True, footertxt=f'User ID: {ctx.author.id}', color=COLOR.ATTENTION_INFO.value)

# Create an embed, fill it with data and send it!
embed = discord.Embed(title="Infractions", description=f"Found {len(res)} result(s). Showing first", color=0x469EFF)
embed = discord.Embed(
title="Infractions", description=f"Found {len(res)} result(s). Showing first", color=0x469EFF)
embed.set_author(name="Pluto's Shitty Mod Bot")
case = res[0]

Expand All @@ -383,14 +417,19 @@ async def infraction(ctx, id: str, *, cmd: str = None):
embed.add_field(name="User", value=f"<@{int(case[1])}>", inline=True)
embed.add_field(name="Type", value=f"{str(Measure(case[2]))}", inline=True)
embed.add_field(name="Reason", value=f"{case[3]}", inline=True)
embed.add_field(name="Recorded by", value=f"<@{int(case[4])}>", inline=True)
embed.add_field(name="Timestamp", value=f"{time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(case[5])))}", inline=True)
embed.add_field(name="Recorded by",
value=f"<@{int(case[4])}>", inline=True)
embed.add_field(
name="Timestamp", value=f"{time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(case[5])))}", inline=True)
if case[6] is not None:
u = discord.utils.find(lambda u: u.id == int(case[6]), bot.get_guild(config.guild).members)
u = discord.utils.find(lambda u: u.id == int(
case[6]), bot.get_guild(config.guild).members)
if u is not None:
embed.add_field(name="Alt account", value=f"{u.mention}", inline=True)
embed.add_field(name="Alt account",
value=f"{u.mention}", inline=True)
else:
embed.add_field(name="Alt account", value=f"{case[6]}", inline=True)
embed.add_field(name="Alt account",
value=f"{case[6]}", inline=True)

await ctx.send(embed=embed)
del embed
Expand Down Expand Up @@ -428,7 +467,8 @@ async def on_member_join(member):

# Bot couldn't find the correct role
if mr is None:
raise errors.RoleNotFoundError("Muted role not found!", "Update ID in config file")
raise errors.RoleNotFoundError(
"Muted role not found!", "Update ID in config file")
else:
# Assign the muted role
await member.add_roles(mr, reason="Auto-reassigned by Pluto's Shitty Mod Bot")
Expand All @@ -439,7 +479,8 @@ async def on_member_join(member):
# Bot couldn't find the correct role
rq = member.guild.get_role(r)
if rq is None:
raise errors.RoleNotFoundError("{r} could not be found!", "Update ID in config file")
raise errors.RoleNotFoundError(
"{r} could not be found!", "Update ID in config file")
await member.add_roles(rq, reason="Auto-assigned by Pluto's Shitty Mod Bot")
await log.log(bot, f"Auto assigned `{rq}` to {member}", to_channel=True, footertxt=f"User ID: {member.id}", color=COLOR.INFO.value)

Expand All @@ -461,6 +502,8 @@ async def on_member_update(before, after):
{after.nick}""", to_channel=True, footertxt=f"Message ID: {after.id}; Created at: {before.created_at}", color=COLOR.INFO.value)

# This event is risen when a member left the server (this can be the cause of kicking too!)


@bot.event
async def on_member_remove(member):
await log.log(bot, f"Member {member} left", to_channel=True, footertxt=f"User ID: {member.id}", color=COLOR.ATTENTION_BAD.value)
Expand All @@ -473,7 +516,8 @@ async def on_message_delete(message):
return
if spam.deleting:
return
al = message.guild.audit_logs(limit=3, action=discord.AuditLogAction.message_delete, after=datetime.datetime.utcnow() - datetime.timedelta(seconds=10))
al = message.guild.audit_logs(limit=3, action=discord.AuditLogAction.message_delete,
after=datetime.datetime.utcnow() - datetime.timedelta(seconds=10))
re = await al.get(target=message.author)
if re is None or re.user is None:
await log.log(bot, f"**Message from {message.author.mention} deleted in <#{message.channel.id}>**:\n{message.content}", to_channel=True, to_log=False, footertxt=f"Message ID: {message.id}; Created at: {message.created_at}", color=COLOR.BAD.value, expiry=config.sensitive_expiry)
Expand Down Expand Up @@ -506,7 +550,7 @@ async def on_command_error(context, exception):
return
# Handling Forbidden and NotFound errors
if isinstance(exception.original, discord.errors.Forbidden):
# Try to send
# Try to send
try:
return await context.send("[DISCORD ERROR] HTTP 403")
except Exception:
Expand All @@ -520,14 +564,16 @@ async def on_command_error(context, exception):
if isinstance(exception.original, discord.ext.commands.UnexpectedQuoteError):
await context.send("_Unexpected closing of the string. Error has been logged_")

ex = traceback.format_exception(type(exception), exception, exception.__traceback__)
ex = traceback.format_exception(
type(exception), exception, exception.__traceback__)
m = """[ERR] """
for line in ex:
m = """{}{}""".format(m, line)
if context.guild is None:
m = f"{m}\nIN DM, thanks to {context.author} ({context.author.id})\n\n"
else:
m = f"{m}\nIn {context.guild} ({context.guild.id}), thanks to {context.author} ({context.author.id})\n\n".format(m, context.guild, context.guild.id, context.author, context.author.id)
m = f"{m}\nIn {context.guild} ({context.guild.id}), thanks to {context.author} ({context.author.id})\n\n".format(
m, context.guild, context.guild.id, context.author, context.author.id)
log.errlog(m)

await context.send("Ohoh, something went wrong. Error has been logged")
Expand Down
65 changes: 65 additions & 0 deletions src/models/Infraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from modules import db
import discord
import time
import config


class Infraction:
def __init__(self, user_id: int, measure: measure.Measure, reason: str, author_id: int):
"""

:param user_id: int
:param measure: measure.Measure
:param reason: str
:param author_id: int
"""
self.user_id: int = user_id
self.measure: measure.Measure = measure
self.reason: str = reason
self.author_id: id = author_id
self._infraction = None

# Get infraction info from the database
res = db.GetInfraction(self.user_id)

# Error out if nothing is found
if len(res) < 1:
# return await ctx.send("🚫 Didn't find any infractions")
self._infraction = "🚫 Didn't find any infractions"
else:
# Create an embed, fill it with data and send it!
embed = discord.Embed(
title="Infractions", description=f"Found {len(res)} result(s). Showing first", color=0x469EFF)
embed.set_author(name="Pluto's Shitty Mod Bot")
case = res[0]

embed.add_field(name="GUID", value=f"{case[0]}", inline=True)

embed.add_field(
name="User", value=f"<@{int(case[1])}>", inline=True)
embed.add_field(
name="Type", value=f"{str(measure.Measure(case[2]))}", inline=True)
embed.add_field(name="Reason", value=f"{case[3]}", inline=True)
embed.add_field(name="Recorded by",
value=f"<@{int(case[4])}>", inline=True)
embed.add_field(
name="Timestamp", value=f"{time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(int(case[5])))}", inline=True)
if case[6] is not None:
u = discord.utils.find(lambda u: u.id == int(
case[6]), bot.get_guild(config.guild).members)
if u is not None:
embed.add_field(name="Alt account",
value=f"{u.mention}", inline=True)
else:
embed.add_field(name="Alt account",
value=f"{case[6]}", inline=True)

# await ctx.send(embed=embed)
self._infraction = embed
del embed

def __str__(self):
"""
:returns discord.Embed || str
"""
return self._infraction