# 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 " \n Please 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. " )