You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Enso-Bot/bot/__init__.py

796 lines
32 KiB
Python

# Ensō~Chan - A Multi Purpose Discord Bot That Has Everything Your Server Needs!
# Copyright (C) 2020 Goudham Suresh
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import datetime
import os
import random
import aiohttp
import asyncpg as asyncpg
import discord
from decouple import config
from discord import Colour, Embed
from discord.ext import commands, tasks
from discord.ext.commands import when_mentioned_or
from bot.libs.cache import MyCoolCache
counter = 0
# Get DB information from .env
password = config('DB_PASS')
host = config('DB_HOST')
user = config('DB_USER')
port = config('DB_PORT')
db = config('DB_NAME')
disforge_auth = config('DISFORGE_AUTH')
disc_bots_gg_auth = config('DISCORD_BOTS_BOTS_AUTH')
# Getting the bot token from environment variables
API_TOKEN = config('DISCORD_TOKEN')
class Bot(commands.Bot):
def __init__(self, **options):
async def get_prefix(bot, message):
"""Allow the commands to be used with mentioning the bot"""
if message.guild is None:
return "."
return when_mentioned_or(self.get_prefix_for_guild(message.guild.id))(bot, message)
super().__init__(command_prefix=get_prefix, case_insensitive=True, **options)
self.db = None
self.description = 'All current available commands within Ensō~Chan',
self.owner_id = 154840866496839680 # Your unique User ID
self.admin_colour = Colour(0x62167a) # Admin Embed Colour
self.version = "0.8.2" # Version number of Ensō~Chan
self.remove_command("help") # Remove default help command
# Instance variables for Enso
self.hammyMention = '<@154840866496839680>'
self.hammy_role_ID = "<@&715412394968350756>"
self.blank_space = "\u200b"
self.enso_embedmod_colours = Colour(0x62167a)
self.enso_ensochancommands_Mention = "<#721449922838134876>"
self.enso_ensochancommands_ID = 721449922838134876
self.enso_verification_ID = 728034083678060594
self.enso_selfroles_ID = 722347423913213992
self.enso_guild_ID = 663651584399507476
self.enso_newpeople_ID = 669771571337887765
self.enso_modmail_ID = 728083016290926623
self.enso_feedback_ID = 739807803438268427
# Cross/Tick Emojis
self.cross = "<:xMark:746834944629932032>"
self.tick = "<:greenTick:746834932936212570>"
# Instance variables for cache
self.enso_cache = {}
self.modmail_cache = {}
self.member_cache = MyCoolCache(100)
async def create_connection():
"""Setting up connection using asyncpg"""
self.db = await asyncpg.create_pool(
host=host,
port=int(port),
user=user,
password=password,
database=db,
loop=self.loop)
async def startup_cache_log():
"""Store the guilds/modmail systems in cache from the database on startup"""
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query to get all records of guilds that the bot is in
try:
results = await conn.fetch("""SELECT * FROM guilds""")
# Store the guilds information within cache
for row in results:
self.enso_cache[row["guild_id"]] = {"prefix": row["prefix"],
"modlogs": row["modlogs"],
"roles_persist": row["roles_persist"]}
# Catch errors
except asyncpg.PostgresError as e:
print("PostGres Error: Guild Records Could Not Be Loaded Into Cache On Startup", e)
# Query to get all records of modmails within guilds
try:
results = await conn.fetch("""SELECT * FROM moderatormail""")
# Store the information for modmail within cache
for row in results:
self.modmail_cache[row["guild_id"]] = {"modmail_channel_id": row["modmail_channel_id"],
"message_id": row["message_id"],
"modmail_logging_channel_id":
row["modmail_logging_channel_id"]}
# Catch errors
except asyncpg.PostgresError as e:
print("PostGres Error: Modmail Records Could Not Be Loaded Into Cache On Startup", e)
# Release connection back to pool
await pool.release(conn)
# Establish Database Connection
self.loop.run_until_complete(create_connection())
# Load Information Into Cache
self.loop.run_until_complete(startup_cache_log())
async def post_bot_stats():
"""Update guild count on bot lists"""
async with aiohttp.ClientSession() as session:
await session.post(f"https://discord.bots.gg/api/v1/bots/{self.user.id}/stats",
data={"guildCount": {len(self.guilds)},
"Content-Type": "application/json"},
headers={'Authorization': disc_bots_gg_auth})
await session.post(f"https://disforge.com/api/botstats/{self.user.id}",
data={"servers": {len(self.guilds)}},
headers={'Authorization': disforge_auth})
await session.close()
@tasks.loop(minutes=10, reconnect=True)
async def change_status():
"""Creating custom statuses as background task"""
global counter
# Waiting for the bot to ready
await self.wait_until_ready()
# Update guild count on discord.bots.gg
await post_bot_stats()
# Define array of statuses
looping_statuses = [
discord.Activity(
type=discord.ActivityType.watching,
name=f"{len(self.users)} Weebs | {self.version}"),
discord.Activity(
type=discord.ActivityType.watching,
name=f"Hamothy | Real Life | {self.version}"),
discord.Activity(
type=discord.ActivityType.watching,
name=f"Hamothy Program | {self.version}"),
discord.Game(name=f".help | {self.version}")
]
# Check if the counter is at the end of the array
if counter == (len(looping_statuses) - 1):
# Reset the loop
counter = 0
else:
# Increase the counter
counter += 1
# Display the next status in the loop
await self.change_presence(activity=looping_statuses[counter])
# Start the background task(s)
change_status.start()
# --------------------------------------------!Cache Section!-------------------------------------------------------
def store_cache(self, guild_id, prefix, modlogs, roles_persist):
"""Storing guild information within cache"""
self.enso_cache[guild_id] = {"prefix": prefix,
"modlogs": modlogs,
"roles_persist": roles_persist}
def del_cache(self, guild_id):
"""Deleting the entry of the guild within the cache"""
del self.enso_cache[guild_id]
# --------------------------------------------!End Cache Section!---------------------------------------------------
# --------------------------------------------!Modmail Section!-----------------------------------------------------
def cache_store_modmail(self, guild_id, modmail_channel, message, modmail_logging_channel):
"""Storing all modmail channels within cache"""
self.modmail_cache[guild_id] = {"modmail_channel_id": modmail_channel,
"message_id": message,
"modmail_logging_channel_id": modmail_logging_channel}
def get_modmail(self, guild_id):
"""Returning the modmail system of the guild"""
return self.modmail_cache.get(guild_id)
def update_modmail(self, guild_id, channel_id):
"""Update the modmail channel"""
self.modmail_cache[guild_id]["modmail_logging_channel_id"] = channel_id
def delete_modmail(self, guild_id):
"""Deleting the modmail system of the guild within the Cache"""
del self.modmail_cache[guild_id]
# --------------------------------------------!EndModmail Section!--------------------------------------------------
# --------------------------------------------!RolePersist Section!-------------------------------------------------
def get_roles_persist(self, guild_id):
"""Returning rolespersist value of the guild"""
role_persist = self.enso_cache.get(guild_id)
return role_persist.get("roles_persist") if role_persist else None
async def update_role_persist(self, guild_id, value):
"""Update the rolepersist value of the guild (Enabled or Disabled)"""
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query for updating rolepersist values For guilds
try:
update_query = """UPDATE guilds SET roles_persist = $1 WHERE guild_id = $2"""
await conn.execute(update_query, value, guild_id)
# Catch error
except asyncpg.PostgresError as e:
print(f"PostGres Error: RolePersist For Guild {guild_id} Could Not Be Updated", e)
# Store in cache
else:
self.enso_cache[guild_id]["roles_persist"] = value
# Release connection back to pool
finally:
await pool.release(conn)
# --------------------------------------------!End RolePersist Section!---------------------------------------------
# --------------------------------------------!ModLogs Section!-----------------------------------------------------
async def storage_modlog_for_guild(self, ctx, channel_id, setup):
"""Updating the modlog within the dict and database"""
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query to update modlogs within the database
try:
update_query = """UPDATE guilds SET modlogs = $1 WHERE guild_id = $2"""
rowcount = await conn.execute(update_query, channel_id, ctx.guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print("PostGres Error: Modlogs Value In Guilds Table Could Not Be Updated/Setup", e)
# Let the user know that modlogs channel has been updated/setup
else:
if setup:
print(rowcount, f"Modlog channel for guild {ctx.guild} has been Setup")
await self.generate_embed(ctx, desc=f"**Modlogs Channel** successfully setup in <#{channel_id}>" +
f"\nPlease refer to **{ctx.prefix}help** for any information")
else:
print(rowcount, f"Modlog channel for guild {ctx.guild} has been Updated")
await self.generate_embed(ctx,
desc=f"Modlog Channel for **{ctx.guild}** has been updated to <#{channel_id}>")
# Store in cache
self.enso_cache[ctx.guild.id]["modlogs"] = channel_id
# Release connection back to pool
finally:
await pool.release(conn)
def remove_modlog_channel(self, guild_id):
"""Remove the value of modlog for the guild specified"""
self.enso_cache[guild_id]["modlogs"] = None
def get_modlog_for_guild(self, guild_id):
"""Get the modlog channel of the guild that the user is in"""
modlogs = self.enso_cache.get(guild_id)
return modlogs.get("modlogs") if modlogs else None
# --------------------------------------------!End ModLogs Section!-------------------------------------------------
# --------------------------------------------!Prefixes Section!----------------------------------------------------
async def storage_prefix_for_guild(self, ctx, prefix):
"""Updating the prefix within the dict and database when the method is called"""
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query to update the existing prefix within the database
try:
update_query = """UPDATE guilds SET prefix = $1 WHERE guild_id = $2"""
rowcount = await conn.execute(update_query, prefix, ctx.guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Prefix For Guild {ctx.guild.id} Could Not Be Updated", e)
# Let the user know that the guild prefix has been updated
else:
print(rowcount, f"Guild prefix has been updated for guild {ctx.guild}")
await self.generate_embed(ctx, desc=f"Guild prefix has been updated to **{prefix}**")
# Store in cache
self.enso_cache[ctx.guild.id]["prefix"] = prefix
# Release connection back to pool
finally:
await pool.release(conn)
def get_prefix_for_guild(self, guild_id):
"""Get the prefix of the guild that the user is in"""
prefix = self.enso_cache[guild_id]["prefix"]
if prefix:
return prefix
return "."
# --------------------------------------------!End Prefixes Section!------------------------------------------------
# --------------------------------------------!Roles/Colour/Embed Section!------------------------------------------
@staticmethod
def random_colour():
"""Generate a random hex colour"""
return Colour(random.randint(0, 0xFFFFFF))
async def generate_embed(self, ctx, desc):
"""Generate embed"""
embed = Embed(description=desc,
colour=self.admin_colour)
await ctx.send(embed=embed)
async def store_roles(self, target, ctx, member):
"""Storing user roles within database"""
role_ids = ", ".join([str(r.id) for r in target.roles])
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query to store existing roles of the member within the database
try:
update_query = """UPDATE members SET muted_roles = $1 WHERE guild_id = $2 AND member_id = $3"""
rowcount = await conn.execute(update_query, role_ids, ctx.guild.id, member.id)
result = await self.check_cache(member.id, member.guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Roles Could Not Be Stored For Member {member.id} in Guild {member.guild.id}", e)
# Print success
# Update cache
else:
result["muted_roles"] = role_ids
print(rowcount, f"Roles Added For User {member} in {ctx.guild}")
# Release connection back to pool
finally:
await pool.release(conn)
async def clear_roles(self, member):
"""Clear the roles when the user has been unmuted"""
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Query to clear the existing role of the member from the database
try:
update = """UPDATE members SET muted_roles = NULL WHERE guild_id = $1 AND member_id = $2"""
rowcount = await conn.execute(update, member.guild.id, member.id)
result = await self.check_cache(member.id, member.guild.id)
# Catch error
except asyncpg.PostgresError as e:
print(f"PostGres Error: Roles Could Not Be Cleared for Member {member.id} in Guild {member.guild.id}",
e)
# Print success
# Update cache
else:
result["muted_roles"] = None
print(rowcount, f"Roles Cleared For User {member} in {member.guild.name}")
# Release connection back to pool
finally:
await pool.release(conn)
# --------------------------------------------!End Roles/Colour/Embed Section!--------------------------------------
# --------------------------------------------!Events Section!------------------------------------------------------
async def on_message(self, message):
"""Make sure bot messages are not tracked"""
# Ignoring messages that start with 2 ..
if message.content.startswith("..") or message.author.bot:
return
# Processing the message
await self.process_commands(message)
async def on_ready(self):
"""Display startup message"""
print("UvU Senpaiii I'm ready\n")
async def on_guild_join(self, guild):
"""
Store users in a database
Store prefix/modlogs in the cache
"""
# Store every single record into an array
records = [(member.id, None, None, guild.id, None, None, None, 0) for member in guild.members]
# Setup up pool connection
pool = self.db
async with pool.acquire() as conn:
# Insert the guild information into guilds table
try:
insert = """INSERT INTO guilds VALUES ($1, $2, $3, $4) ON CONFLICT (guild_id) DO NOTHING"""
rowcount = await conn.execute(insert, guild.id, ".", None, 0)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Guild {guild.id} Could Not Be Inserted Into Guilds Table", e)
# Print success
else:
print(rowcount, f"Record(s) inserted successfully into {guild}")
self.store_cache(guild.id, modlogs=None, prefix=".", roles_persist=0)
# Query to insert all the member details to members table
try:
rowcount = await conn.copy_records_to_table("members", records=records)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Members Could Not Be Inserted Into Members Table For Guild {guild.id}", e)
# Store in cache
else:
print(rowcount, f"Record(s) inserted successfully into Members from {guild}")
# Release connection back to pool
await pool.release(conn)
async def on_guild_remove(self, guild):
"""
Remove users in the database for the guild
Remove the modlogs/guild from the cache
"""
# Setup pool connection
pool = self.db
async with pool.acquire() as conn:
# Delete the guild information as the bot leaves the server
try:
delete = """DELETE FROM guilds WHERE guild_id = $1"""
rowcount = await conn.execute(delete, guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: On Guild Remove Event Record Was Not Deleted For {guild.id}", e)
# Delete the key - value pair for the guild
else:
print(rowcount, f"Record deleted successfully from Guild {guild}")
self.del_cache(guild.id)
# Delete all records of members from that guild
try:
delete = """DELETE FROM members WHERE guild_id = $1"""
rowcount = await conn.execute(delete, guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: All Members Could Not Be Deleted From {guild.id}", e)
# Print success
else:
print(rowcount, f"Record(s) deleted successfully from Members from {guild}")
# Remove any/all members stored in cache from that guild
self.member_cache.remove_many(guild.id)
# Release connection back to pool
await pool.release(conn)
async def on_member_join(self, member):
"""
Bot event to insert new members into the database
In the Enso guild, it will send an introduction embed
"""
# Ignoring bots
if member.bot: return
# Get the guild and role persist value of the guild
guild = member.guild
role_persist = self.get_roles_persist(guild.id)
# Setup pool connection
pool = self.db
async with pool.acquire() as conn:
# Define the insert statement that will insert the user's information
# On conflict, set the left values to null
try:
insert = """INSERT INTO members (guild_id, member_id) VALUES ($1, $2)
ON CONFLICT (guild_id, member_id) DO UPDATE SET left_at = NULL, has_left = 0"""
rowcount = await conn.execute(insert, member.guild.id, member.id)
# Catch errors
except asyncpg.PostgresError as e:
print(
f"PostGres Error: {member} | {member.id} was not be able to be added to {member.guild} | {member.guild.id}",
e)
# Print success
else:
print(rowcount, f"{member} Joined {member.guild}, Record Inserted Into Members")
print(rowcount, f"Roles Cleared For {member} in {member.guild}")
# Get the roles of the user from the database
try:
select_query = """SELECT * FROM members WHERE guild_id = $1 AND member_id = $2"""
user_joined = await conn.fetchrow(select_query, member.guild.id, member.id)
role_ids = user_joined["roles"]
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: {member} | {member.id} Record Not Found", e)
# Give roles back to the user if role persist is enabled
else:
if role_persist == 1:
# Get Enso Chan
bot = guild.get_member(self.user.id)
# Set flag for what value role_ids is
flag = role_ids
# Check permissions of Enso
if bot.guild_permissions.manage_roles and flag:
# Get all the roles of the user before they were muted from the database
roles = [member.guild.get_role(int(id_)) for id_ in role_ids.split(", ") if len(id_)]
# Give the member their roles back
await member.edit(roles=roles)
print(f"{member} Had Their Roles Given Back In {member.guild}")
# Don't give roles if user has no roles to be given
elif bot.guild_permissions.manage_roles and not flag:
print(f"Member {member} | {member.id} Had No Roles To Be Given")
# No permissions to give roles in the server
else:
print(
f"Insufficient Permissions to Add Roles to {member} | {member.id} in {member.guild} | {member.guild.id}")
# Reset the roles entry for the database
try:
update_query = """UPDATE members SET roles = NULL WHERE guild_id = $1 AND member_id = $2"""
rowcount = await conn.execute(update_query, member.guild.id, member.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Clearing Member {member.id} Roles in Guild {member.guild.id}", e)
# Print success
# Update cache
else:
print(rowcount, f"Roles Cleared For {member} in {member.guild}")
# Release connection back to pool
await pool.release(conn)
# Make sure the guild is Enso and send welcoming embed to the server
if guild.id == self.enso_guild_ID:
new_people = guild.get_channel(self.enso_newpeople_ID)
server_icon = guild.icon_url
welcome_gif = "https://cdn.discordapp.com/attachments/669808733337157662/730186321913446521/NewPeople.gif"
embed = Embed(title="\n**Welcome To Ensō!**",
colour=self.admin_colour,
timestamp=datetime.datetime.utcnow())
embed.set_thumbnail(url=server_icon)
embed.set_image(url=welcome_gif)
embed.add_field(
name=self.blank_space,
value=f"Hello {member.mention}! We hope you enjoy your stay in this server!",
inline=False)
embed.add_field(
name=self.blank_space,
value=f"Be sure to check out our <#669815048658747392> channel to read the rules and <#683490529862090814> channel to get caught up with any changes! ",
inline=False)
embed.add_field(
name=self.blank_space,
value=f"Last but not least, feel free to go into <#669775971297132556> to introduce yourself!",
inline=False)
# Send embed to #newpeople
await new_people.send(embed=embed)
async def on_member_remove(self, member):
"""Storing member roles within the database when the member leaves"""
# Ignoring bots
if member.bot: return
# Get the datetime of when the user left the guild
left_at = datetime.datetime.utcnow()
# Store member roles within a string to insert into database
role_ids = ", ".join([str(r.id) for r in member.roles if not r.managed])
# Setup pool connection
pool = self.db
async with pool.acquire() as conn:
# Store member roles within the database
try:
update = """UPDATE members SET roles = $1, left_at = $2, has_left = 1
WHERE guild_id = $3 AND member_id = $4"""
rowcount = await conn.execute(update, role_ids, left_at, member.guild.id, member.id)
# Catch Error
except asyncpg.PostgresError as e:
print(f"PostGres Error: Roles Could Not Be Added To {member} When Leaving {member.guild.id}", e)
# Print success
else:
print(rowcount, f"{member} Left {member.guild.name}, Roles stored into Members")
# Release connection back to pool
finally:
await pool.release(conn)
async def on_guild_channel_delete(self, channel):
"""Deleting modlogs/modmail channel if it's deleted in the guild"""
# Get the modlogs channel (channel or none)
modlogs = self.get_modlog_for_guild(channel.guild.id)
# Get the modmail record - (normal and logging channels)
modmail_record = self.get_modmail(channel.guild.id)
modmail_channel = modmail_record.get("modmail_channel_id") if modmail_record else None
modmail_logging_channel = modmail_record.get("modmail_logging_channel_id") if modmail_record else None
# Get pool
pool = self.db
# Delete the modlogs system from the database when modlogs channel is deleted
if channel.id == modlogs:
# Setup pool connection
async with pool.acquire() as conn:
# Set channel to none
try:
update = """UPDATE guilds SET modlogs = NULL WHERE guild_id = $1"""
await conn.execute(update, channel.guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Guild Modlogs Could Not Be Deleted For {channel.guild.id}", e)
# Delete channel from cache
else:
self.remove_modlog_channel(channel.guild.id)
# Release connection back to pool
finally:
await pool.release(conn)
# If modmail channels are deleted, delete the entire system
if channel.id == modmail_channel or channel.id == modmail_logging_channel:
# Set up pool connection
async with pool.acquire() as conn:
# Remove the moderatormail record from the database
try:
delete = """DELETE FROM moderatormail WHERE guild_id = $1"""
await conn.execute(delete, channel.guild.id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: ModeratorMail Record Could Not Be Deleted for Guild {channel.guild.id}", e)
# Delete from cache
else:
self.delete_modmail(channel.guild.id)
# Release connection back to pool
finally:
await pool.release(conn)
# --------------------------------------------!End Events Section!----------------------------------------------
async def check_cache(self, member_id, guild_id):
"""Checks if member is in the member cache"""
# Return key-value pair if member is already in the cache
if (member_id, guild_id) in self.member_cache.cache:
return self.member_cache.cache[member_id, guild_id]
else:
# Setup pool connection
pool = self.db
async with pool.acquire() as conn:
# Get the author's/members row from the Members Table
try:
select_query = """SELECT * FROM members WHERE member_id = $1 and guild_id = $2"""
result = await conn.fetchrow(select_query, member_id, guild_id)
# Catch errors
except asyncpg.PostgresError as e:
print(f"PostGres Error: Member {member_id} From Guild {guild_id}"
"Record Could Not Be Retrieved When Checking Cache", e)
# Store it in cache
else:
dict_items = {"married": result["married"],
"married_date": result["married_date"],
"muted_roles": result["muted_roles"],
"roles": result["roles"]}
self.member_cache.store_cache((member_id, guild_id), dict_items)
return self.member_cache.cache[(member_id, guild_id)]
# Release connection back to pool
finally:
await pool.release(conn)
def execute(self):
"""Load the cogs and then run the bot"""
for file in os.listdir(f'.{os.sep}cogs'):
if file.endswith('.py'):
self.load_extension(f"cogs.{file[:-3]}")
# Run the bot, allowing it to come online
try:
self.run(API_TOKEN)
except discord.errors.LoginFailure as e:
print(e, "Login unsuccessful.")