├── .gitignore ├── README.md ├── bot.py ├── cogs ├── database_bg.py ├── general.py ├── special.py └── war-reporter.py ├── creds_sample.py ├── database └── database.py ├── images └── venv_image.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # This is where you "hide" the sensitive files in case you plan on 2 | # posting your code online 3 | creds.py 4 | bot_database.db 5 | 6 | .idea 7 | venv_bot/ 8 | .vscode/ 9 | __pycache* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clash of Clans Sample Discord Bot [Python] 2 | Sample Discord bot, using Python, to access the Clash of Clans API 3 | 4 | ## Getting Started 5 | 1. Install the latest version of [Python](https://www.python.org/). 6 | 1. Sign up for a [Clash of Clans Developer Account](https://developer.clashofclans.com/#/). 7 | 1. Sign up for a [Discord Developer Account](https://discordapp.com/developers) and create an App with a Bot User. 8 | 1. Rename `creds_sample.py` to `creds.py` and add your Clash of Clans Account email and password, and your Discord Bot Token. 9 | 1. Create and activate a virtual environment `python3 -m venv venv_bot && source venv_bot/bin/activate` 10 | 1. Run `pip install -r requirements.txt` to install dependencies like the discord.py & coc.py libraries. 11 | 1. Run `python bot.py` 12 | 13 | ## Test Your Bot 14 | 1. Add your bot to a test Discord Server using an URL like this: 15 | `https://discordapp.com/oauth2/authorize?client_id=`Insert Client ID from your Discord App`&scope=bot` 16 | 1. Send the command `!clan #CLASH_CLAN_TAG` to load info about the clan. 17 | 18 | ## About virtual environments 19 | New developers may not see the benefit of using a virtual environment until they start making a second or third bot. They may come to realize that updating their packages for one bot will interfere with the other bots and potentially even breaking their other bots. This is because you are downloading and updating your packages globally. A way around this is using virtual environments to segregate projects. 20 | ### Create the environment 21 | ``` 22 | python3 -m venv venv_test 23 | ``` 24 | ### Activate the environment 25 | ``` 26 | source venv_test/bin/activate 27 | ``` 28 | You should now see your environment tag in the command line indicating that you are now in the virtual environment. 29 | ![venv](images/venv_image.png) 30 | ## Project Developers 31 | TubaKid, Sidekick, mathsman, majordoobie... 32 | 33 | Find us and ask questions at https://discord.gg/Eaja7gJ -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | # Import other libraries to be used in your code 2 | import coc 3 | import creds 4 | import traceback 5 | 6 | from database.database import BotDatabase 7 | from discord.ext import commands 8 | 9 | description = "This is where you provide a concise description of your bot. Not sure if this is ever visible anywhere." 10 | 11 | # This is where you can select the prefix you'd like used for your bot commands 12 | prefix = "!" 13 | 14 | # Here you make the connection to the COC API using the coc.py library 15 | coc_client = coc.login(creds.coc_dev_email, creds.coc_dev_password, client=coc.EventsClient, key_names="war_home") 16 | 17 | # These are the cogs that you are using in your bot 18 | initial_extensions = ( 19 | "cogs.general", 20 | "cogs.special", 21 | "cogs.database_bg", 22 | "cogs.war-reporter", 23 | ) 24 | 25 | # File path to your sqlite3 db file 26 | SQLITE_FILE = 'database/bot_database.db' 27 | 28 | 29 | class MyBot(commands.Bot): 30 | # The __init__ method is a standard method seen at the beginning of most classes 31 | # it declares the variables that will be used throughout the class 32 | def __init__(self): 33 | super().__init__(command_prefix=prefix, 34 | description=description, 35 | case_insensitive=True) 36 | self.coc = coc_client 37 | # This instantiates the database class 38 | self.dbconn = BotDatabase(SQLITE_FILE) 39 | 40 | # Load all extensions (see the cogs folder) 41 | for extension in initial_extensions: 42 | try: 43 | self.load_extension(extension) 44 | except Exception as extension: 45 | traceback.print_exc() 46 | 47 | async def on_ready(self): 48 | print(f"Bot is logged in as {self.user} ID: {self.user.id}") 49 | 50 | 51 | # the following if statement ensures that bot.py is the actual file being executed 52 | # the alternative is that this file might be imported into another file (in which case, we don't run the following) 53 | if __name__ == "__main__": 54 | try: 55 | bot = MyBot() 56 | bot.run(creds.discord_bot_token) 57 | except: 58 | traceback.print_exc() 59 | -------------------------------------------------------------------------------- /cogs/database_bg.py: -------------------------------------------------------------------------------- 1 | from coc import utils 2 | from datetime import datetime 3 | from discord.ext import commands, tasks 4 | 5 | 6 | class DatabaseBackground(commands.Cog): 7 | def __init__(self, bot): 8 | self.bot = bot 9 | self.update.start() 10 | 11 | def cog_unload(self): 12 | self.update.cancel() 13 | 14 | @commands.command(name="add_user") 15 | async def add_user(self, ctx, player_tag): 16 | """Command is used to register a user to the database""" 17 | player_tag = utils.correct_tag(player_tag) 18 | player = await self.bot.coc.get_player(player_tag) 19 | self.bot.dbconn.register_user((player.tag, player.name, player.town_hall, )) 20 | await ctx.send(f"User added: {player.name}") 21 | 22 | @tasks.loop(minutes=3.0) 23 | async def update(self): 24 | """This method updates the database every 3 minutes""" 25 | tags = self.bot.dbconn.get_players() 26 | tag_list = [tag[0] for tag in tags] 27 | async for player in self.bot.coc.get_players(tag_list): 28 | self.bot.dbconn.update_donation((datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 29 | player.tag, 30 | player.get_achievement("Friend in Need").value)) 31 | 32 | @update.before_loop 33 | async def before_update(self): 34 | """This method prevents the task from running before the bot is connected""" 35 | await self.bot.wait_until_ready() 36 | 37 | 38 | def setup(bot): 39 | bot.add_cog(DatabaseBackground(bot)) 40 | -------------------------------------------------------------------------------- /cogs/general.py: -------------------------------------------------------------------------------- 1 | import coc 2 | 3 | from discord.ext import commands 4 | 5 | 6 | class General(commands.Cog): 7 | """Description of what this file does""" 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.command(name="get_clan", aliases=["getclan", "clan"]) 12 | async def get_clan(self, ctx, clan_tag): 13 | """Gets clan information from API and displays it for user""" 14 | # This line uses a utility in the coc.py library to correct clan tags (case, missing #, etc.) 15 | clan_tag = coc.utils.correct_tag(clan_tag) 16 | 17 | clan = await self.bot.coc.get_clan(clan_tag) 18 | content = f"The clan name for {clan_tag} is {clan.name}.\n" 19 | content += f"{clan.name} currently has {clan.member_count} members.\n\n" 20 | 21 | war = await self.bot.coc.get_current_war(clan_tag) 22 | if war: 23 | content += f"Current war state is {war.state}\n" 24 | if war.state != "notInWar": 25 | content += f"Opponent: {war.opponent}" 26 | 27 | await ctx.send(content) 28 | 29 | 30 | def setup(bot): 31 | bot.add_cog(General(bot)) 32 | -------------------------------------------------------------------------------- /cogs/special.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from discord.ext import commands 4 | 5 | 6 | class Special(commands.Cog): 7 | """Description of what this file does""" 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.command(name="avatar", hidden=True) 12 | async def avatar(self, ctx, user: discord.Member = None): 13 | """Command to see a larger version of the given member's avatar 14 | Examples: 15 | ++avatar @mention 16 | ++avatar 123456789 17 | ++avatar member#1234 18 | """ 19 | if not user: 20 | user = ctx.author 21 | embed = discord.Embed(color=discord.Color.blue()) 22 | embed.add_field(name=f"{user.name}#{user.discriminator}", value=user.display_name, inline=True) 23 | embed.add_field(name="Avatar URL", value=user.avatar_url, inline=True) 24 | embed.set_image(url=user.avatar_url_as(size=128)) 25 | embed.set_footer(text=f"Discord ID: {user.id}", 26 | icon_url="https://discordapp.com/assets/2c21aeda16de354ba5334551a883b481.png") 27 | response = await ctx.send(embed=embed) 28 | 29 | 30 | def setup(bot): 31 | bot.add_cog(Special(bot)) 32 | -------------------------------------------------------------------------------- /cogs/war-reporter.py: -------------------------------------------------------------------------------- 1 | import coc 2 | import creds 3 | 4 | from discord.ext import commands 5 | 6 | CLAN_TAG = creds.clan_tag 7 | WAR_REPORT_CHANNEL_ID = creds.war_channel 8 | 9 | REPORT_STYLE = """ 10 | {att.attacker.name} (No. {att.attacker.map_position}, TH{att.attacker.town_hall}) just {verb} {att.defender.name} 11 | (No. {att.defender.map_position}, TH{att.defender.town_hall}) for {att.stars} stars and {att.destruction}%. 12 | """ 13 | 14 | 15 | class WarReporter(commands.Cog): 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | self.bot.coc.add_events( 20 | self.on_war_attack, 21 | self.on_war_state_change 22 | ) 23 | self.bot.coc.add_war_updates(CLAN_TAG) 24 | 25 | def cog_unload(self): 26 | self.bot.coc.remove_events( 27 | self.on_war_attack, 28 | self.on_war_state_change 29 | ) 30 | self.bot.coc.stop_updates("war") 31 | 32 | @property 33 | def report_channel(self): 34 | return self.bot.get_chanel(WAR_REPORT_CHANNEL_ID) 35 | 36 | @coc.WarEvents.war_attack() 37 | async def on_war_attack(self, attack, war): 38 | if attack.attacker.is_opponenet: 39 | verb = "defended" 40 | else: 41 | verb = "attacked" 42 | 43 | await self.report_channel.send(REPORT_STYLE.format(att=attack, verb=verb)) 44 | 45 | @coc.WarEvents.state() 46 | async def on_war_state_change(self, current_state, war): 47 | await self.report_channel.send("{0.clan.name} just entered {1} state!".format(war, current_state)) 48 | 49 | 50 | def setup(bot): 51 | bot.add_cog(WarReporter(bot)) 52 | -------------------------------------------------------------------------------- /creds_sample.py: -------------------------------------------------------------------------------- 1 | # CLASH OF CLANS API 2 | # Your Clash of Clash Developer Account email & password will be used to auto-generate a Key, 3 | # so don't worry about creating a new Key 4 | coc_dev_email = "" # Clash of Clans Developer Account email 5 | coc_dev_password = "" # Clash of Clans Developer Account password 6 | 7 | # DISCORD DEVELOPER 8 | discord_bot_token = "" # 59 character Bot Token from your Discord App 9 | 10 | # OTHER INFORMATION 11 | clan_tag = "" # Your clan's tag - this doesn't have to be in creds, but if you're going to use it a lot... 12 | default_channel = 123456780 # Default Discord Channel ID for messages 13 | war_channel = 123456789 # Discord Channel ID for war reporting 14 | -------------------------------------------------------------------------------- /database/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class BotDatabase: 5 | def __init__(self, db_filepath): 6 | self.conn = sqlite3.connect(db_filepath) 7 | self.conn.cursor().execute("PRAGMA foreign_keys = 1") # Enables foreign keys 8 | self.instantiate_db() 9 | 10 | def instantiate_db(self): 11 | """Method creates the database if they do not exist""" 12 | create_player_table = ("CREATE TABLE IF NOT EXISTS coc_players " 13 | "(coc_tag text NOT NULL, " 14 | "coc_name text NOT NULL, " 15 | "coc_th integer NOT NULL, " 16 | "PRIMARY KEY(coc_tag))") 17 | create_donation_table = ("CREATE TABLE IF NOT EXISTS player_donation " 18 | "(update_date date NOT NULL, " 19 | "coc_tag text NOT NULL, " 20 | "coc_donation integer NOT NULL, " 21 | "PRIMARY KEY (update_date, coc_tag), " 22 | "CONSTRAINT coc_tag_ref FOREIGN KEY (coc_tag) " 23 | "REFERENCES coc_players (coc_tag))") 24 | 25 | self.conn.cursor().execute(create_player_table) 26 | self.conn.cursor().execute(create_donation_table) 27 | self.conn.commit() 28 | 29 | def register_user(self, tuple_data): 30 | """Method is used to register a user by taking a tuple of data to commit""" 31 | sql = "INSERT INTO coc_players (coc_tag, coc_name, coc_th) VALUES (?,?,?)" 32 | self.conn.cursor().execute(sql, tuple_data) 33 | self.conn.commit() 34 | 35 | def update_donation(self, tuple_data): 36 | """Method updates the donation of the registered users""" 37 | sql = ("INSERT INTO player_donation (update_date, coc_tag, coc_donation) " 38 | "VALUES (?,?,?)") 39 | self.conn.cursor().execute(sql, tuple_data) 40 | self.conn.commit() 41 | 42 | def get_players(self): 43 | """Method gets all the registered users""" 44 | sql = "SELECT coc_tag FROM coc_players" 45 | cur = self.conn.cursor() 46 | cur.execute(sql) 47 | row = cur.fetchall() 48 | return row 49 | -------------------------------------------------------------------------------- /images/venv_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpmjones/coc_sample_bot/61a299d6203696f04f06fc67c4f507a2892edc50/images/venv_image.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py 2 | git+https://github.com/mathsman5133/coc.py --------------------------------------------------------------------------------