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

588 lines
24 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 logging
import os
import random
import aiohttp
import aiomysql
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
logger = logging.getLogger('discord')
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
logger.addHandler(handler)
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')
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(str(message.guild.id)))(bot, message)
super().__init__(command_prefix=get_prefix, **options)
self.db = None
self.description = 'All current available commands within Ensō~Chan', # Set a description for the bot
self.owner_id = 154840866496839680 # Your unique User ID
self.case_insensitive = True # Commands are now Case Insensitive
self.admin_colour = Colour(0x62167a) # Admin Embed Colour
self.version = "0.7.2" # Version number of Ensō~Chan
self.remove_command("help") # Remove default help command
# Define variables that are for Enso only
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
self.enso_cache = {}
self.member_cache = MyCoolCache(1000)
async def check_cache(member_id, guild_id):
pool = self.db
# If the key is within the cache already
if (member_id, guild_id) in self.member_cache.cache:
return self.member_cache.cache[member_id, guild_id]
else:
# fetch data from database
# Setup pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as author_cursor:
# Get the author's/members row from the Members Table
select_query = """SELECT * FROM members WHERE discordID = (%s) and guildID = (%s)"""
member_val = member_id, guild_id,
# Execute The SQL Query
await author_cursor.execute(select_query, member_val)
result = await author_cursor.fetchone()
# Store it in cache
dict_items = {"married": result[1],
"marriage_date": result[2],
"muted_roles": result[4],
"roles": result[5]}
self.member_cache.store_cache([member_id, guild_id], dict_items)
async def create_connection():
"""Setting up connection using pool/aiomysql"""
self.db = await aiomysql.create_pool(
host=host,
port=int(port),
user=user,
password=password,
db=db,
loop=self.loop)
async def startup_cache_log():
"""Store the modlogs/prefixes in cache from the database on startup"""
# Setup pool
pool = self.db
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Grab the prefix of the server from the database
select_query = """SELECT * FROM guilds"""
# Execute the query
await cur.execute(select_query)
results = await cur.fetchall()
# Store the guildID's, modlog channels and prefixes within cache
for row in results:
self.enso_cache[row[0]] = {"prefix": row[1], "modlogs": row[2], "roles_persist": row[3]}
# Make sure the connection is setup before the bot is ready
self.loop.run_until_complete(create_connection())
self.loop.run_until_complete(startup_cache_log())
async def post_bot_stats():
"""Method To Update Guild Count On discord.bots.gg"""
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.close()
@tasks.loop(minutes=10, reconnect=True)
async def change_status():
"""Creating Custom Statuses as a 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, channel, rolespersist):
"""Storing GuildID, Modlogs Channel and Prefix in Cache"""
self.enso_cache[guild_id] = {"prefix": prefix, "modlogs": channel, "roles_persist": rolespersist}
def del_cache(self, guild_id):
"""Deleting the entry of the guild within the cache"""
del self.enso_cache[guild_id]
# --------------------------------------------!End Cache Section!---------------------------------------------------
# --------------------------------------------!RolePersist Section!-------------------------------------------------
def get_roles_persist(self, guild_id):
"""Returning rolespersist value of the guild"""
return self.enso_cache[guild_id]["roles_persist"]
async def update_role_persist(self, guild_id, value, pool):
"""Update the rolepersist value of the guild (Enabled or Disabled)"""
self.enso_cache[guild_id]["roles_persist"] = value
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Update the existing prefix within the database
update_query = """UPDATE guilds SET rolespersist = (%s) WHERE guildID = (%s)"""
update_vals = value, guild_id,
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
# --------------------------------------------!End RolePersist Section!---------------------------------------------
# --------------------------------------------!ModLogs Section!-----------------------------------------------------
async def storage_modlog_for_guild(self, pool, ctx, channelID, setup):
"""Updating the modlog within the dict and database"""
self.enso_cache[str(ctx.guild.id)]["modlogs"] = channelID
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Update the existing modlogs channel within the database
update_query = """UPDATE guilds SET modlogs = (%s) WHERE guildID = (%s)"""
update_vals = channelID, ctx.guild.id,
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
# Send custom confirmation messages to log based on the command update or setup
if setup:
print(cur.rowcount, f"Modlog channel for guild {ctx.guild.name} has been Setup")
else:
print(cur.rowcount, f"Modlog channel for guild {ctx.guild.name} has been Updated")
if setup:
# Send confirmation that modmail channel has been setup
await self.bot.generate_embed(ctx, desc=f"**Modlogs Channel** successfully setup in <#{channelID}>" +
f"\nPlease refer to **{ctx.prefix}help** for any information")
else:
# Let the user know that the guild modlogs channel has been updated
channel = ctx.guild.get_channel(channelID)
await self.generate_embed(ctx,
desc=f"Modlog Channel for **{ctx.guild.name}** has been updated to {channel.mention}")
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"""
channel = self.enso_cache[guild_id]["modlogs"]
return channel
# --------------------------------------------!End ModLogs Section!-------------------------------------------------
# --------------------------------------------!Prefixes Section!----------------------------------------------------
async def storage_prefix_for_guild(self, pool, ctx, prefix):
"""Updating the prefix within the dict and database when the method is called"""
self.enso_cache[str(ctx.guild.id)]["prefix"] = prefix
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Update the existing prefix within the database
update_query = """UPDATE guilds SET prefix = (%s) WHERE guildID = (%s)"""
update_vals = prefix, ctx.guild.id,
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
print(cur.rowcount, f"Guild prefix has been updated for guild {ctx.guild.name}")
# Let the user know that the guild prefix has been updated
await self.generate_embed(ctx, desc=f"**Guild prefix has been updated to `{prefix}`**")
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 is not None:
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 storeRoles(self, target, ctx, member):
"""Storing User Roles within Database"""
pool = self.db
role_ids = ", ".join([str(r.id) for r in target.roles])
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Store the existing roles of the user within the database
update_query = """UPDATE members SET mutedroles = (%s) WHERE guildID = (%s) AND discordID = (%s)"""
update_vals = role_ids, ctx.guild.id, member.id
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
print(cur.rowcount, f"Roles Added For User {member} in {ctx.guild.name}")
async def clearRoles(self, member):
"""Clear the roles when the user has been unmuted"""
pool = self.db
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Clear the existing roles of the user from the database
update_query = """UPDATE members SET mutedroles = NULL WHERE guildID = (%s) AND discordID = (%s)"""
update_vals = member.guild.id, member.id
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
print(cur.rowcount, f"Roles Cleared For User {member} in {member.guild.name}")
# --------------------------------------------!End Roles/Colour/Embed Section!--------------------------------------
# --------------------------------------------!Events Section!------------------------------------------------------
async def on_message(self, message):
"""Make sure bot messages are not tracked"""
if message.author.bot: return
# Processing the message
await self.process_commands(message)
@staticmethod
async def on_ready():
"""Displaying if Bot is Ready"""
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 guildID, modlogs channel and prefix to cache
self.store_cache(str(guild.id), channel=None, prefix="~", rolespersist=0)
# Setup pool
pool = self.db
# Grabbing the values to be inserted
records = ", ".join(map(lambda m: f"({guild.id}, {m.id})", guild.members))
# Setup up pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Define the insert statement for inserting the guild into the guilds table
insert_query = """INSERT INTO guilds (guildID) VALUES (%s) ON DUPLICATE KEY UPDATE guildID = VALUES(guildID)"""
val = guild.id,
# Execute the query
await cur.execute(insert_query, val)
await conn.commit()
print(cur.rowcount, f"Record(s) inserted successfully into Guilds from {guild.name}")
async with conn.cursor() as cur:
# Define the insert statement that will insert the user's information
insert = """INSERT INTO members (guildID, discordID) VALUES {}
ON DUPLICATE KEY UPDATE guildID = VALUES(guildID), discordID = VALUES(discordID)""".format(
records)
# Execute the query
await cur.execute(insert)
await conn.commit()
print(cur.rowcount, f"Record(s) inserted successfully into Members from {guild.name}")
async def on_guild_remove(self, guild):
"""
Remove users in the database for the guild
Remove the modlogs/guild from the cache
"""
# Delete the key - value pairs for the guild
self.del_cache(str(guild.id))
# Setup pool
pool = self.db
# Setup pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Delete the guild and prefix information as the bot leaves the server
delete_query = """DELETE FROM guilds WHERE guildID = %s"""
val = guild.id,
# Execute the query
await cur.execute(delete_query, val)
await conn.commit()
print(cur.rowcount, f"Record deleted successfully from Guild {guild.name}")
async with conn.cursor() as cur:
# Delete the record of the member as the bot leaves the server
delete_query = """DELETE FROM members WHERE guildID = %s"""
vals = guild.id,
# Execute the query
await cur.execute(delete_query, vals)
await conn.commit()
print(cur.rowcount, f"Record(s) deleted successfully from Members from {guild.name}")
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
"""
if member.bot: return
# Get the guild
guild = member.guild
# Setup pool
pool = self.db
role_persist = self.get_roles_persist(str(guild.id))
# Setup pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Define the insert statement that will insert the user's information
insert_query = """INSERT INTO members (guildID, discordID) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE guildID = VALUES(guildID), discordID = VALUES(discordID)"""
vals = member.guild.id, member.id,
# Execute the SQL Query
await cur.execute(insert_query, vals)
await conn.commit()
print(cur.rowcount, f"{member} Joined {member.guild.name}, Record Inserted Into Members")
async with conn.cursor() as cur:
# Get the roles of the user from the database
select_query = """SELECT * FROM members WHERE guildID = (%s) AND discordID = (%s)"""
vals = member.guild.id, member.id,
# Execute the SQL Query
await cur.execute(select_query, vals)
result = await cur.fetchone()
role_ids = result[5]
if role_persist == 1:
# Get Enso Chan
bot = guild.get_member(self.user.id)
# Check permissions of Enso
if bot.guild_permissions.manage_roles and role_ids is not None:
# 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.name}")
else:
print(f"Insufficient Permissions to Add Roles to {member} in {member.guild.name}")
# Reset the roles entry for the database
update_query = """UPDATE members SET roles = NULL WHERE guildID = (%s) AND discordID = (%s)"""
update_vals = member.guild.id, member.id,
# Execute the query
await cur.execute(update_query, update_vals)
await conn.commit()
print(cur.rowcount, f"Roles Cleared For {member} in {member.guild.name}")
# Make sure the guild is Enso
if guild.id == self.enso_guild_ID:
# Set the channel id to "newpeople"
new_people = guild.get_channel(self.enso_newpeople_ID)
# Set the enso server icon and the welcoming gif
server_icon = guild.icon_url
welcome_gif = "https://cdn.discordapp.com/attachments/669808733337157662/730186321913446521/NewPeople.gif"
# Set up embed for the #newpeople channel
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 User Roles within Database When User Leaves Guild"""
if member.bot: return
role_ids = ", ".join([str(r.id) for r in member.roles if not r.managed])
# Setup pool
pool = self.db
# Setup pool connection and cursor
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# Define the insert statement that will insert the user's information
update_query = """UPDATE members SET roles = (%s) WHERE guildID = (%s) AND discordID = (%s)"""
vals = role_ids, member.guild.id, member.id,
# Execute the SQL Query
await cur.execute(update_query, vals)
await conn.commit()
print(cur.rowcount, f"{member} Left {member.guild.name}, Roles stored into Members")
# --------------------------------------------!End Events Section!----------------------------------------------
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.")