From c8b62bc189037294ef42930bdc0b0030aa6e4a71 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 18 Aug 2022 22:42:09 +1000 Subject: [PATCH 01/15] make meme bot a cog --- cogs/hello.py | 15 ++--- cogs/meme.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ main2.py | 11 ++-- src/main.py | 158 -------------------------------------------------- 4 files changed, 168 insertions(+), 169 deletions(-) create mode 100644 cogs/meme.py delete mode 100644 src/main.py diff --git a/cogs/hello.py b/cogs/hello.py index 9cbe5d2..cb29569 100644 --- a/cogs/hello.py +++ b/cogs/hello.py @@ -1,18 +1,19 @@ from discord.ext import commands + class Hello(commands.Cog): def __init__(self, client): - self.client = client # defining bot as global var in class + self.client = client # defining bot as global var in class - @commands.Cog.listener() # this is a decorator for events/listeners + @commands.Cog.listener() # this is a decorator for events/listeners async def on_ready(self): print(f'We have logged in as {self.client.user}') - - @commands.command() # this is for making a command + @commands.command() # this is for making a command async def hello(self, ctx): print(ctx) await ctx.send(f'Hi!') - -def setup(bot): # a extension must have a setup function - bot.add_cog(Hello(bot)) # adding a cog \ No newline at end of file + + +async def setup(bot): # a extension must have a setup function + await bot.add_cog(Hello(bot)) # adding a cog diff --git a/cogs/meme.py b/cogs/meme.py new file mode 100644 index 0000000..afe2858 --- /dev/null +++ b/cogs/meme.py @@ -0,0 +1,153 @@ +# TODO move to cogs folder +import json +import os + +import requests +from discord.ext import commands +from dotenv import load_dotenv +from pymongo import MongoClient +from requests.exceptions import HTTPError +from schema import Schema, SchemaError, And, Regex + +load_dotenv() + +TOKEN = os.getenv('TOKEN') +COMMAND = os.getenv('DISCORD_COMMAND_ROOT') +BOTNAME = os.getenv('BOT_NAME') +API_ROOT = os.getenv('MEME_API_ROOT') +MONGODB_URI = os.getenv('MONGODB_URI') +DB = os.getenv('DB') +COLLECTION = os.getenv('COLLECTION') + +collection = MongoClient(MONGODB_URI)[DB][COLLECTION] + + +# all commands that this bot will respond to will begin with ";;bot_name" +# bot = commands.Bot(command_prefix=COMMAND + BOTNAME, strip_after_prefix=True, intents=discord.Intents.all()) + +def get_random_memes(count): + try: + response = requests.get(f"{API_ROOT}{count}") + except HTTPError as error: + raise error + json_data = json.loads(response.text) + return json_data + + +def get_random_meme_from_subreddits(subReddit, count): + try: + response = requests.get(f"{API_ROOT}{subReddit}/{count}") + except HTTPError as error: + raise error + if (response.status_code == 404 or response.status_code == 400): + raise HTTPError(response.text) + json_data = json.loads(response.text) + return json_data + + +def check_link_die(link): + try: + response = requests.get(link) + except: + return False + if (response.status_code == 404): + return False + return True + + +meme_schema = Schema({ + "name": str, + "url": And(str, Regex("^(https:\/\/i.redd.it\/)\w+\.\w{3}$"))}) + + +class Meme(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def new(self, ctx, *args): + # ;;random + if (len(args) == 0): + random_meme = get_random_memes(1)['memes'][0] + while (check_link_die(random_meme['url']) == False): + random_meme = get_random_memes(1)['memes'][0] + await ctx.send(random_meme['url']) + + # ;;random 5 + if (len(args) > 0): + try: + random_memes = get_random_memes(int(args[0]))['memes'] + except: + await ctx.send("Unrecognized command. Pretty sure that wasn't a number :)") + pass + random_memes_image_link = [e['url'] for e in random_memes] + for link in random_memes_image_link: + if (check_link_die(link) == False): + continue + else: + await ctx.send(link) + pass + + @commands.command() + async def subreddit(self, ctx, arg1): + subreddit = arg1 + try: + random_meme = get_random_meme_from_subreddits(subreddit, 1) + random_meme_subreddit = random_meme['memes'][0] + while (check_link_die(random_meme_subreddit['url']) == False): + random_meme = get_random_meme_from_subreddits(subreddit, 1) + random_meme_subreddit = random_meme['memes'][0] + await ctx.send(random_meme_subreddit['url']) + except HTTPError as err: + await ctx.send('Subreddit not found') + pass + pass + + @commands.command() + async def save(self, ctx, arg1): + try: + response = await ctx.channel.fetch_message(ctx.message.reference.message_id) + meme_url = response.content + except print(0): + await ctx.send("You forgot to reply to a meme, or the meme you replied to wasn't saved in the right format") + pass + saved_name = arg1 + saved_meme = { + "name": saved_name, + "url": meme_url, + } + try: + meme_schema.validate(saved_meme) + except SchemaError as err: + await ctx.send(err) + pass + collection.insert_one(saved_meme) + await ctx.send("Saved to database!") + pass + + @commands.command() + async def load(self, ctx, arg1): + lookup_name = arg1 + try: + found_meme = collection.find_one({"name": lookup_name}) + await ctx.send(found_meme["url"]) + except TypeError: + await ctx.send("Meme not found in database!") + pass + pass + + @commands.command() + async def delete(self, ctx, arg1): + lookup_name = arg1 + try: + found_meme = collection.find_one({"name": lookup_name}) + except TypeError: + await ctx.send("Meme not found in database!") + pass + collection.delete_one(found_meme) + await ctx.send("Meme was deleted from database!") + pass + + +async def setup(bot): + await bot.add_cog(Meme(bot)) diff --git a/main2.py b/main2.py index 8d85721..bf37a95 100644 --- a/main2.py +++ b/main2.py @@ -1,16 +1,19 @@ # main.py -from discord.ext import commands +import asyncio import os + +import discord +from discord.ext import commands from dotenv import load_dotenv load_dotenv() TOKEN = os.getenv('TOKEN') -client = commands.Bot(command_prefix = "!") +client = commands.Bot(command_prefix="!", intents=discord.Intents.all()) # Looks inside the /cogs/ folder and loads up all of our cogs for filename in os.listdir("./cogs"): - if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) + if filename.endswith(".py"): + asyncio.run(client.load_extension(f"cogs.{filename[:-3]}")) client.run(TOKEN) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 53f597e..0000000 --- a/src/main.py +++ /dev/null @@ -1,158 +0,0 @@ -# TODO move to cogs folder -from pymongo import MongoClient -import os -from dotenv import load_dotenv -import discord -from discord.ext import commands -import requests -from requests.exceptions import HTTPError -import json -from schema import Schema, SchemaError, And, Regex - -load_dotenv() - -TOKEN = os.getenv('DISCORD_TOKEN') -GUILD = os.getenv('DISCORD_GUILD') -COMMAND = os.getenv('DISCORD_COMMAND_ROOT') -BOTNAME = os.getenv('BOT_NAME') -API_ROOT = os.getenv('MEME_API_ROOT') -MONGODB_URI = os.getenv('MONGODB_URI') -DB = os.getenv('DB') -COLLECTION = os.getenv('COLLECTION') - -collection = MongoClient(MONGODB_URI)[DB][COLLECTION] - -# all commands that this bot will respond to will begin with ";;bot_name" -bot = commands.Bot(command_prefix=COMMAND + BOTNAME, strip_after_prefix=True) - - -def get_random_memes(count): - try: - response = requests.get(f"{API_ROOT}{count}") - except HTTPError as error: - raise error - json_data = json.loads(response.text) - return json_data - - -def get_random_meme_from_subreddits(subReddit, count): - try: - response = requests.get(f"{API_ROOT}{subReddit}/{count}") - except HTTPError as error: - raise error - if(response.status_code == 404 or response.status_code == 400): - raise HTTPError(response.text) - json_data = json.loads(response.text) - return json_data - - -def check_link_die(link): - try: - response = requests.get(link) - except: - return False - if(response.status_code == 404): - return False - return True - - -meme_schema = Schema({ - "name": str, - "url": And(str, Regex("^(https:\/\/i.redd.it\/)\w+\.\w{3}$"))}) - - -@commands.command(name='new') -async def _new(ctx, *args): - # ;;random - if (len(args) == 0): - random_meme = get_random_memes(1)['memes'][0] - while(check_link_die(random_meme['url']) == False): - random_meme = get_random_memes(1)['memes'][0] - await ctx.send(random_meme['url']) - - # ;;random 5 - if (len(args) > 0): - try: - random_memes = get_random_memes(int(args[0]))['memes'] - except: - await ctx.send("Unrecognized command. Pretty sure that wasn't a number :)") - pass - random_memes_image_link = [e['url'] for e in random_memes] - for link in random_memes_image_link: - if(check_link_die(link) == False): - continue - else: - await ctx.send(link) - pass -bot.add_command(_new) - - -@commands.command(name='subreddit') -async def _subreddit(ctx, arg1): - subreddit = arg1 - try: - random_meme = get_random_meme_from_subreddits(subreddit, 1) - random_meme_subreddit = random_meme['memes'][0] - while(check_link_die(random_meme_subreddit['url']) == False): - random_meme = get_random_meme_from_subreddits(subreddit, 1) - random_meme_subreddit = random_meme['memes'][0] - await ctx.send(random_meme_subreddit['url']) - except HTTPError as err: - await ctx.send('Subreddit not found') - pass - pass -bot.add_command(_subreddit) - - -@commands.command(name='save') -async def _save(ctx, arg1): - try: - response = await ctx.channel.fetch_message(ctx.message.reference.message_id) - meme_url = response.content - except print(0): - await ctx.send("You forgot to reply to a meme, or the meme you replied to wasn't saved in the right format") - pass - saved_name = arg1 - saved_meme = { - "name": saved_name, - "url": meme_url, - } - try: - meme_schema.validate(saved_meme) - except SchemaError as err: - await ctx.send(err) - pass - collection.insert_one(saved_meme) - await ctx.send("Saved to database!") - pass -bot.add_command(_save) - - -@commands.command(name='load') -async def _load(ctx, arg1): - lookup_name = arg1 - try: - found_meme = collection.find_one({"name": lookup_name}) - await ctx.send(found_meme["url"]) - except TypeError: - await ctx.send("Meme not found in database!") - pass - pass -bot.add_command(_load) - - -@commands.command(name='delete') -async def _delete(ctx, arg1): - lookup_name = arg1 - try: - found_meme = collection.find_one({"name": lookup_name}) - found_meme["url"] - except TypeError: - await ctx.send("Meme not found in database!") - pass - collection.delete_one(found_meme) - await ctx.send("Meme was deleted from database!") - pass -bot.add_command(_delete) - -bot.run(TOKEN) From 316b70b239e10a8c4ccd4e3cf13717d2aa8af758 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 18 Aug 2022 22:48:10 +1000 Subject: [PATCH 02/15] better command names --- cogs/meme.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cogs/meme.py b/cogs/meme.py index afe2858..530fae1 100644 --- a/cogs/meme.py +++ b/cogs/meme.py @@ -1,4 +1,3 @@ -# TODO move to cogs folder import json import os @@ -65,7 +64,7 @@ def __init__(self, bot): self.bot = bot @commands.command() - async def new(self, ctx, *args): + async def meme(self, ctx, *args): # ;;random if (len(args) == 0): random_meme = get_random_memes(1)['memes'][0] @@ -89,7 +88,7 @@ async def new(self, ctx, *args): pass @commands.command() - async def subreddit(self, ctx, arg1): + async def meme_subreddit(self, ctx, arg1): subreddit = arg1 try: random_meme = get_random_meme_from_subreddits(subreddit, 1) @@ -104,7 +103,7 @@ async def subreddit(self, ctx, arg1): pass @commands.command() - async def save(self, ctx, arg1): + async def save_meme(self, ctx, arg1): try: response = await ctx.channel.fetch_message(ctx.message.reference.message_id) meme_url = response.content @@ -126,7 +125,7 @@ async def save(self, ctx, arg1): pass @commands.command() - async def load(self, ctx, arg1): + async def load_meme(self, ctx, arg1): lookup_name = arg1 try: found_meme = collection.find_one({"name": lookup_name}) @@ -137,7 +136,7 @@ async def load(self, ctx, arg1): pass @commands.command() - async def delete(self, ctx, arg1): + async def delete_meme(self, ctx, arg1): lookup_name = arg1 try: found_meme = collection.find_one({"name": lookup_name}) From 486a6dec64919e0444641300b4349fd916ecab8e Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 09:05:26 +1000 Subject: [PATCH 03/15] Move w1 images to own folder --- w1/{ => images}/2022-07-27-20-48-43.png | Bin w1/{ => images}/2022-07-27-20-50-00.png | Bin w1/{ => images}/2022-07-27-20-51-23.png | Bin w1/{ => images}/2022-08-06-16-47-20.png | Bin w1/{ => images}/2022-08-06-16-52-41.png | Bin w1/{ => images}/2022-08-06-16-57-29.png | Bin w1/{ => images}/2022-08-06-16-57-47.png | Bin w1/{ => images}/2022-08-06-17-01-31.png | Bin w1/{ => images}/2022-08-06-17-02-00.png | Bin w1/{ => images}/2022-08-06-17-05-59.png | Bin w1/{ => images}/2022-08-06-17-11-00.png | Bin w1/{ => images}/2022-08-06-17-11-30.png | Bin w1/{ => images}/2022-08-06-17-17-22.png | Bin w1/{ => images}/2022-08-06-17-17-49.png | Bin w1/{ => images}/deployment_add_buildpack.gif | Bin w1/{ => images}/vscode_access_terminal.gif | Bin .../vscode_create_main_and_env_files.gif | Bin 17 files changed, 0 insertions(+), 0 deletions(-) rename w1/{ => images}/2022-07-27-20-48-43.png (100%) rename w1/{ => images}/2022-07-27-20-50-00.png (100%) rename w1/{ => images}/2022-07-27-20-51-23.png (100%) rename w1/{ => images}/2022-08-06-16-47-20.png (100%) rename w1/{ => images}/2022-08-06-16-52-41.png (100%) rename w1/{ => images}/2022-08-06-16-57-29.png (100%) rename w1/{ => images}/2022-08-06-16-57-47.png (100%) rename w1/{ => images}/2022-08-06-17-01-31.png (100%) rename w1/{ => images}/2022-08-06-17-02-00.png (100%) rename w1/{ => images}/2022-08-06-17-05-59.png (100%) rename w1/{ => images}/2022-08-06-17-11-00.png (100%) rename w1/{ => images}/2022-08-06-17-11-30.png (100%) rename w1/{ => images}/2022-08-06-17-17-22.png (100%) rename w1/{ => images}/2022-08-06-17-17-49.png (100%) rename w1/{ => images}/deployment_add_buildpack.gif (100%) rename w1/{ => images}/vscode_access_terminal.gif (100%) rename w1/{ => images}/vscode_create_main_and_env_files.gif (100%) diff --git a/w1/2022-07-27-20-48-43.png b/w1/images/2022-07-27-20-48-43.png similarity index 100% rename from w1/2022-07-27-20-48-43.png rename to w1/images/2022-07-27-20-48-43.png diff --git a/w1/2022-07-27-20-50-00.png b/w1/images/2022-07-27-20-50-00.png similarity index 100% rename from w1/2022-07-27-20-50-00.png rename to w1/images/2022-07-27-20-50-00.png diff --git a/w1/2022-07-27-20-51-23.png b/w1/images/2022-07-27-20-51-23.png similarity index 100% rename from w1/2022-07-27-20-51-23.png rename to w1/images/2022-07-27-20-51-23.png diff --git a/w1/2022-08-06-16-47-20.png b/w1/images/2022-08-06-16-47-20.png similarity index 100% rename from w1/2022-08-06-16-47-20.png rename to w1/images/2022-08-06-16-47-20.png diff --git a/w1/2022-08-06-16-52-41.png b/w1/images/2022-08-06-16-52-41.png similarity index 100% rename from w1/2022-08-06-16-52-41.png rename to w1/images/2022-08-06-16-52-41.png diff --git a/w1/2022-08-06-16-57-29.png b/w1/images/2022-08-06-16-57-29.png similarity index 100% rename from w1/2022-08-06-16-57-29.png rename to w1/images/2022-08-06-16-57-29.png diff --git a/w1/2022-08-06-16-57-47.png b/w1/images/2022-08-06-16-57-47.png similarity index 100% rename from w1/2022-08-06-16-57-47.png rename to w1/images/2022-08-06-16-57-47.png diff --git a/w1/2022-08-06-17-01-31.png b/w1/images/2022-08-06-17-01-31.png similarity index 100% rename from w1/2022-08-06-17-01-31.png rename to w1/images/2022-08-06-17-01-31.png diff --git a/w1/2022-08-06-17-02-00.png b/w1/images/2022-08-06-17-02-00.png similarity index 100% rename from w1/2022-08-06-17-02-00.png rename to w1/images/2022-08-06-17-02-00.png diff --git a/w1/2022-08-06-17-05-59.png b/w1/images/2022-08-06-17-05-59.png similarity index 100% rename from w1/2022-08-06-17-05-59.png rename to w1/images/2022-08-06-17-05-59.png diff --git a/w1/2022-08-06-17-11-00.png b/w1/images/2022-08-06-17-11-00.png similarity index 100% rename from w1/2022-08-06-17-11-00.png rename to w1/images/2022-08-06-17-11-00.png diff --git a/w1/2022-08-06-17-11-30.png b/w1/images/2022-08-06-17-11-30.png similarity index 100% rename from w1/2022-08-06-17-11-30.png rename to w1/images/2022-08-06-17-11-30.png diff --git a/w1/2022-08-06-17-17-22.png b/w1/images/2022-08-06-17-17-22.png similarity index 100% rename from w1/2022-08-06-17-17-22.png rename to w1/images/2022-08-06-17-17-22.png diff --git a/w1/2022-08-06-17-17-49.png b/w1/images/2022-08-06-17-17-49.png similarity index 100% rename from w1/2022-08-06-17-17-49.png rename to w1/images/2022-08-06-17-17-49.png diff --git a/w1/deployment_add_buildpack.gif b/w1/images/deployment_add_buildpack.gif similarity index 100% rename from w1/deployment_add_buildpack.gif rename to w1/images/deployment_add_buildpack.gif diff --git a/w1/vscode_access_terminal.gif b/w1/images/vscode_access_terminal.gif similarity index 100% rename from w1/vscode_access_terminal.gif rename to w1/images/vscode_access_terminal.gif diff --git a/w1/vscode_create_main_and_env_files.gif b/w1/images/vscode_create_main_and_env_files.gif similarity index 100% rename from w1/vscode_create_main_and_env_files.gif rename to w1/images/vscode_create_main_and_env_files.gif From f26c0964afb9f1b22008b0daaf22f975f6cb45d4 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 09:05:47 +1000 Subject: [PATCH 04/15] Update README --- README.md | 80 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d720813..b2921cc 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,31 @@ - [About DecodED](#about-decoded) - [Dates](#dates) + - [Zoom Link: https://unimelb.zoom.us/j/88528442813?pwd=WlYrQ3pHcm5xMXpGQkZZSllZZTNvQT09](#zoom-link-httpsunimelbzoomusj88528442813pwdwlyrq3phcm5xmxpgqkzzsllzztnvqt09) - [About this repository](#about-this-repository) - [Workshop 1 - Introduction to Discord.py](#workshop-1---introduction-to-discordpy) -- [Workshop 2 - Tic Tac Toe](#workshop-2---tic-tac-toe) -- [Workshop 3 - Polling Bot](#workshop-3---polling-bot) -- [Workshop 4 - Meme Bot](#workshop-4---meme-bot) -- [Workshop 5 - Music Bot](#workshop-5---music-bot) +- [Workshop 2 - Meme Bot](#workshop-2---meme-bot) +- [Workshop 3 - Music Bot](#workshop-3---music-bot) +- [Workshop 4 - Polling Bot](#workshop-4---polling-bot) +- [Workshop 5 - Tic Tac Toe](#workshop-5---tic-tac-toe) --- ## About DecodED -* DecodED is a series of workshops run every year by HackMelbourne to help students/teach students with introductory programming background to/how to... TODO -* This year we are trying a new teaching style with DecodED so let us know if you like it TODO add a feedback link + +* DecodED is a series of workshops run every year by HackMelbourne to help students/teach students with introductory + programming background to create some cool things with code. In past years, we created full-stack websites and a Space Invaders game. This year, we will be creating a Discord bot! +* We are also trying a new teaching style this year so let us know if you like it! + ### Dates Lesson | Location | People | Date | -- | -- | -- | -- | -Foundations (Basic) | Alan Gilbert 101 and Zoom | Xin Yu | 2pm, Wednesday 24th August -Meme bot (easy) | Alan Gilbert G20 and Zoom | Aly, Minh | Wednesday 31st August -Music bot | Alan Gilbert 121 (Theatre) and Zoom | Aryan, Ryan | Wednesday 7th September +Foundations (Basic) | Alan Gilbert 101 and Zoom | Xin Yu | 2pm, Wednesday 24th August +Meme bot (easy) | Zoom | Aly, Minh | Wednesday 31st August +Music bot | Alan Gilbert 121 (Theatre) and Zoom | Aryan, Ryan | Wednesday 7th September Poll | Alan Gilbert G20 and Zoom | Jerry, Hoan | Wednesday 14th September TicTacToe bot | Alan Gilbert 103 and Zoom | Warren, Daniel | 2pm, Wednesday 21st September @@ -44,41 +48,63 @@ Password: 323737 ## About this repository * This repository contains: * Participant Workbook - * each workbook is made up of multiple Parts which are then made up of multiple **✅ Tasks** that the participant should complete, with **🧩 Hints** that help guide them in completing the Task, and **💡 Extensions** that are optional tasks that can be done if the participant has completed the Task ahead of schedule - * The flow: - 1. host presents/teaches about Part 1 - 2. participants follow the workbook to complete the ✅ Tasks in Part 1 of the workbook - participants who are ahead of the tasks can start do the 💡 Extensions and participants who are behind can use the 🧩 Hints or ask for help - 3. once most of the participants are done with Part 1, host starts presenting/teaching Part 2 and the cycle continues - * Slides (if any) + * each workbook is made up of multiple Parts which are then made up of multiple **✅ Tasks** that the participant + should complete, with **🧩 Hints** that help guide them in completing the Task, and **💡 Extensions** that are + optional tasks that can be done if the participant has completed the Task ahead of schedule * Workshop Recordings * Code for the Discord Bot being created ## Workshop 1 - Introduction to Discord.py -> About DecodED3, Covers basics of Discord.py, create a basic bot that says "Hello, World!", learn about the basic structure of bots, -* [📔Participant Workbook](/w1/README.md) + +> About DecodED3, Covers basics of Discord.py, create a basic bot that says "Hello, World!", learn about the basic +> structure of Discord bots. +> +> Hosted by Xin Yu + +* [📔Participant Workbook](/w1/) * [🐍Python Cheatsheet](/w1/python_cheatsheet.md) * [🐍Python Setup](/w1/python_setup.md) * [👾Discord.py Cheatsheet](/w1/discord_py_cheatsheet.md) * [🔗Discord.py Documentation](https://discordpy.readthedocs.io/en/stable/index.html) -* [Workshop Recording] TBD! +* [Workshop Recording] Coming Soon! ## Workshop 2 - Meme Bot -> Who doesn’t love a good meme? Join us and create the functionality to meme on your friends so hard that they wished they had their own meme bot. -* Aly and Minh + +> Who doesn’t love a good meme? Join us and create the functionality to meme on your friends so hard that they wished +> they had their own meme bot. +> +> Hosted by Aly and Minh + +* [📔Participant Workbook](/w2/) +* [Workshop Recording] ## Workshop 3 - Music Bot -> Ever since YouTube banned music bots, discord servers have been desperately lacking some tunes. Impress your friends by bringing them back by building your own bot with the ability to play music, plus additional music controls! Now you can resume your lo-fi beat study sessions with your mates! -* Aryan and Ryan +> Ever since YouTube banned music bots, discord servers have been desperately lacking some tunes. Impress your friends +> by bringing them back by building your own bot with the ability to play music, plus additional music controls! Now you +> can resume your lo-fi beat study sessions with your mates! +> +> Hosted by Aryan and Ryan + +* [📔Participant Workbook](/w3/) +* [Workshop Recording] ## Workshop 4 - Polling Bot -> Caught up in your server arguing why Minecraft is (definitively) the best game? Why not run a poll on your server and prove your friends wrong? Learn to build your own polling bot to settle your arguments in style 😎 -* Jerry and Hoan +> Caught up in your server arguing why Minecraft is (definitively) the best game? Why not run a poll on your server and +> prove your friends wrong? Learn to build your own polling bot to settle your arguments in style 😎 +> +> Jerry and Hoan -## Workshop 5 - Tic Tac Toe -> Fancy a game, but don’t want to leave your friends in discord? In this lesson, you’ll learn to implement a tic tac toe game within the Discord bot so you can vs your friends whenever you wish! +* [📔Participant Workbook](/w4/) +* [Workshop Recording] -* Warren and Weng Jae (Daniel) +## Workshop 5 - Tic Tac Toe +> Fancy a game, but don’t want to leave your friends in discord? In this lesson, you’ll learn to implement a tic tac toe +> game within the Discord bot so you can vs your friends whenever you wish! +> +> Warren and Weng Jae (Daniel) +* [📔Participant Workbook](/w5/) +* [Workshop Recording] From e99ceef6a3886038c46066336750edad18356eae Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 09:23:27 +1000 Subject: [PATCH 05/15] Touch up w1 README --- w1/README.md | 49 +++++++++++-------------- w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png | Bin 0 -> 35238 bytes 2 files changed, 21 insertions(+), 28 deletions(-) create mode 100644 w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png diff --git a/w1/README.md b/w1/README.md index b661b06..e46145f 100644 --- a/w1/README.md +++ b/w1/README.md @@ -45,13 +45,13 @@ ### ✅ Task: Create a Discord Account > 📝 NOTE: If you already have a Discord account, you can skip this task * You can register for a Discord account [here](https://discord.com/register). - * ![](2022-08-06-16-47-20.png) + * ![](./images/2022-08-06-16-47-20.png) ### ✅ Task: Create a Discord Server > 📝 NOTE: Discord servers are sometimes refered to as **'guilds'** in some documentation (because some people confuse the word 'server' with computer servers 🗄️ XD) * this server will be used for you to test your bot * Follow Discord's documentation on [How do I create a server?](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-) 1. Click on the "+" button at the bottom of the left hand column on Discord - * ![](2022-08-06-16-52-41.png) + * ![](./images/2022-08-06-16-52-41.png) 2. Fill in the server details * You can follow these options: Create My Own > For me and my friends > Name your server > "Create" @@ -63,22 +63,22 @@ ### ✅ Task: Create a Discord Application and Bot, and copy your Token * Login to the [Developer Portal's Applications Page](https://discord.com/developers/applications) * Click on "New Application" - * ![](2022-08-06-16-57-29.png) + * ![](./images/2022-08-06-16-57-29.png) * Give the application a name and click "Create" - * ![](2022-08-06-16-57-47.png) + * ![](./images/2022-08-06-16-57-47.png) * Go to the "Bot" tab and then click "Add Bot" and "Yes, do it!" - * ![](2022-08-06-17-01-31.png) + * ![](./images/2022-08-06-17-01-31.png) * Now your bot has been created! Next step is to copy the token and paste it in a notepad or create a file called `.env` [(Environment Variables)](#environment-variables) and paste it in there for now - * ![](2022-08-06-17-02-00.png) + * ![](./images/2022-08-06-17-02-00.png) > 📝 NOTE: This token is your bot's password so don't share it with anybody. It could allow someone to log in to your bot and do all sorts of bad things. > > You are only able to see this token once (on the creation of the bot) but you can regenerate the token if it accidentally gets shared. ### ✅ Task: Invite your bot to your server * Go to the "OAuth2 > URL Generator" tab. Then select "bot" under the "scopes" section - * ![](2022-08-06-17-11-00.png) - * Now choose the permissions you want for the bot. For now, you can give it minimal permissions as we are making a simple bot but you might have to add more permissions as you progress through the other workshops and add more features! - * ![](2022-08-06-17-11-30.png) + * ![](./images/2022-08-06-17-11-00.png) + * Now choose the permissions you want for the bot. You can either give it minimal permissions as we are making a simple bot for this workshop but might have to add more permissions as you progress through the other workshops and add more features or you could give it many permissions upfront and not have to worry about permissions later. + * ![](./images/2022-08-06-17-11-30.png) * Then copy the generated URL, open it in a new tab and add your bot to your server. > 📝 NOTE: Be careful when giving your bot "Administrator" permissions @@ -95,7 +95,7 @@ * There are 2 ways to access the terminal in VS Code: 1. using the shortcut ```Ctrl + Shift + ` ``` 2. Terminal > New Terminal - * ![](vscode_access_terminal.gif) + * ![](./images/vscode_access_terminal.gif) @@ -126,7 +126,7 @@
🧩 Creating Files in VS Code -* ![](vscode_create_main_and_env_files.gif) +* ![](./images/vscode_create_main_and_env_files.gif)
@@ -134,7 +134,9 @@
❓ What are environment variables? -> When a program is run, it may need information from the operating system to configure its behaviour. This might include the operating system, current running folder, or more important things like passwords to various services (Discord here!). Basically, environment variables are variables/information about the environment its running on. They are a useful tool in providing information to your program, which is separate from your code. Developers commonly use `.env` files to specify these variables. +When a program is run, it may need information from the operating system to configure its behaviour. This might include the operating system, current running folder, or more important things like passwords to various services (Discord here!). Basically, environment variables are variables/information about the environment its running on. They are a useful tool in providing information to your program, which is separate from your code. Developers commonly use `.env` files to specify these variables. + +![](./images/1_BIgXzxgolWVDBNq5F_eZpg.png)
@@ -147,7 +149,7 @@ TOKEN=example.token.abc123 ``` * [Replit] If you're using Replit to develop your bot, Replit does not let you create a `.env` file so go to the "Secrets" tab and create a key-value pair for your token - * ![](2022-08-06-17-17-49.png) + * ![](./images/2022-08-06-17-17-49.png) * Then in your code file: ```python # ./main.py @@ -188,7 +190,7 @@ 🔍 What are events and callbacks? * **events**: actions or occurrences recognised by the program - * eg. when the bot is done setting up and is ready to operate, when the + * eg. when the bot is done setting up and is ready to operate, when a message is sent, etc. * **callback**: a function that is called when an event happens * eg. the `on_ready()` event is called when the bot has finished logging in and setting things up * to register an event, we use a decorator, `@client.event` on the callback function's definition @@ -241,8 +243,6 @@ > * `client.user.name` > * `client.user.id` > * try printing these `on_ready()`, what do these attributes store? -> -> TODO "Navigating Documentation" (extra) ### Receiving Messages * As said before, there are many events that we can register and 'listen' to. You can check out [Discord Docs > Event Reference](https://discordpy.readthedocs.io/en/stable/api.html#event-reference) for more events. @@ -273,7 +273,7 @@ return await msg.channel.send("Good Morning!") ``` -* ❓ Question for participants: What is the `if` statement doing? Why do we need it? What happens if we remove it? (you might get a special prize if you answer this correctly!) +* ❓ Question for participants: What is the `if` statement doing? Why do we need it? What happens if we remove it? ### ✅ Task: Respond to "!hello" with "Hello, {username}" * Given that we know how to receive and send messages, try amending/adding some code in the `on_message()` to make your bot send "Hello, {username}" to anyone who sends messages starting with `$hello` (but replace {username} with the actual sender's username). @@ -286,8 +286,6 @@ -> 🙋 Don't hesitate to let us know you need any help! - --- ## 5. Cogs @@ -389,16 +387,11 @@ ## 7. [💡 Extension] Host your bot on Heroku > this allows your bot to run continuously without having to open VSCode or keep your repl.it tab running -* Create a Heroku Account -* -* ![](2022-07-27-20-48-43.png) -* Choose an App name () - * ![](2022-07-27-20-50-00.png) -* Once you're in your app's page, go over to your settings, scroll down to **Buildpack** and add the **Python** buildpack - * ![](./deployment_add_buildpack.gif) + +* [Hosting your discord.py bot on Heroku](https://github.com/squid/discord-py-heroku) +* [How to host a discord.py bot with Heroku and GitHub](https://medium.com/analytics-vidhya/how-to-host-a-discord-py-bot-on-heroku-and-github-d54a4d62a99e) ## Related Links: * [Creating a Bot Account | discord.py](https://discordpy.readthedocs.io/en/stable/discord.html) * [Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free | freeCodeCamp](https://www.freecodecamp.org/news/create-a-discord-bot-with-python/) -* [Hosting your discord.py bot on Heroku](https://github.com/squid/discord-py-heroku) -* [How to host a discord.py bot with Heroku and GitHub](https://medium.com/analytics-vidhya/how-to-host-a-discord-py-bot-on-heroku-and-github-d54a4d62a99e) + diff --git a/w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png b/w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png new file mode 100644 index 0000000000000000000000000000000000000000..2307a3b66fc31eae9447fbb26845b9e83e6e1f1c GIT binary patch literal 35238 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 literal 0 HcmV?d00001 From 7c6b0dff9bc8c1cfb09a9d1539eac4d0d128a682 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 09:35:33 +1000 Subject: [PATCH 06/15] Update README --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index b2921cc..d74092b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@

DecodED 3

-> 👋 Hello and welcome to DecodED 3! Over the next 6 weeks, we will be creating a discord bot with [Discord.py](https://discordpy.readthedocs.io/en/stable/index.html) - -

Demo

- -* TODO add gif/screenshot of Discord bot in action +> 👋 Hello and welcome to DecodED 3! Over the next 5 weeks, we will be creating a discord bot with [Discord.py](https://discordpy.readthedocs.io/en/stable/index.html) --- From 3b802e454e6084da4e6646f6aba4348f72a19505 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 09:56:10 +1000 Subject: [PATCH 07/15] Restructure folders and files --- README.md | 437 +++++++++++++++--- cogless.py | 22 + .../1_BIgXzxgolWVDBNq5F_eZpg.png | Bin {w1/images => images}/2022-07-27-20-48-43.png | Bin {w1/images => images}/2022-07-27-20-50-00.png | Bin {w1/images => images}/2022-07-27-20-51-23.png | Bin {w1/images => images}/2022-08-06-16-47-20.png | Bin {w1/images => images}/2022-08-06-16-52-41.png | Bin {w1/images => images}/2022-08-06-16-57-29.png | Bin {w1/images => images}/2022-08-06-16-57-47.png | Bin {w1/images => images}/2022-08-06-17-01-31.png | Bin {w1/images => images}/2022-08-06-17-02-00.png | Bin {w1/images => images}/2022-08-06-17-05-59.png | Bin {w1/images => images}/2022-08-06-17-11-00.png | Bin {w1/images => images}/2022-08-06-17-11-30.png | Bin {w1/images => images}/2022-08-06-17-17-22.png | Bin {w1/images => images}/2022-08-06-17-17-49.png | Bin .../deployment_add_buildpack.gif | Bin .../vscode_access_terminal.gif | Bin .../vscode_create_main_and_env_files.gif | Bin main.py | 22 +- main2.py | 16 - ...thon_cheatsheet.md => python_cheatsheet.md | 0 w1/python_setup.md => python_setup.md | 0 w1/README.md | 397 ---------------- w1/discord_py_cheatsheet.md | 6 - workbook-template.md | 28 -- 27 files changed, 393 insertions(+), 535 deletions(-) create mode 100644 cogless.py rename {w1/images => images}/1_BIgXzxgolWVDBNq5F_eZpg.png (100%) rename {w1/images => images}/2022-07-27-20-48-43.png (100%) rename {w1/images => images}/2022-07-27-20-50-00.png (100%) rename {w1/images => images}/2022-07-27-20-51-23.png (100%) rename {w1/images => images}/2022-08-06-16-47-20.png (100%) rename {w1/images => images}/2022-08-06-16-52-41.png (100%) rename {w1/images => images}/2022-08-06-16-57-29.png (100%) rename {w1/images => images}/2022-08-06-16-57-47.png (100%) rename {w1/images => images}/2022-08-06-17-01-31.png (100%) rename {w1/images => images}/2022-08-06-17-02-00.png (100%) rename {w1/images => images}/2022-08-06-17-05-59.png (100%) rename {w1/images => images}/2022-08-06-17-11-00.png (100%) rename {w1/images => images}/2022-08-06-17-11-30.png (100%) rename {w1/images => images}/2022-08-06-17-17-22.png (100%) rename {w1/images => images}/2022-08-06-17-17-49.png (100%) rename {w1/images => images}/deployment_add_buildpack.gif (100%) rename {w1/images => images}/vscode_access_terminal.gif (100%) rename {w1/images => images}/vscode_create_main_and_env_files.gif (100%) delete mode 100644 main2.py rename w1/python_cheatsheet.md => python_cheatsheet.md (100%) rename w1/python_setup.md => python_setup.md (100%) delete mode 100644 w1/README.md delete mode 100644 w1/discord_py_cheatsheet.md delete mode 100644 workbook-template.md diff --git a/README.md b/README.md index d74092b..b96ef68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -

DecodED 3

- -> 👋 Hello and welcome to DecodED 3! Over the next 5 weeks, we will be creating a discord bot with [Discord.py](https://discordpy.readthedocs.io/en/stable/index.html) +

[Participant's Workbook] Introduction to Discord.py

--- @@ -8,99 +6,390 @@
Table of Contents -- [About DecodED](#about-decoded) - - [Dates](#dates) - - [Zoom Link: https://unimelb.zoom.us/j/88528442813?pwd=WlYrQ3pHcm5xMXpGQkZZSllZZTNvQT09](#zoom-link-httpsunimelbzoomusj88528442813pwdwlyrq3phcm5xmxpgqkzzsllzztnvqt09) -- [About this repository](#about-this-repository) -- [Workshop 1 - Introduction to Discord.py](#workshop-1---introduction-to-discordpy) -- [Workshop 2 - Meme Bot](#workshop-2---meme-bot) -- [Workshop 3 - Music Bot](#workshop-3---music-bot) -- [Workshop 4 - Polling Bot](#workshop-4---polling-bot) -- [Workshop 5 - Tic Tac Toe](#workshop-5---tic-tac-toe) +- [0. Create a Discord Account and Discord Server](#0-create-a-discord-account-and-discord-server) + - [✅ Task: Create a Discord Account](#-task-create-a-discord-account) + - [✅ Task: Create a Discord Server](#-task-create-a-discord-server) +- [1. Create an your Bot and Add it to your Server](#1-create-an-your-bot-and-add-it-to-your-server) + - [✅ Task: Create a Discord Application and Bot, and copy your Token](#-task-create-a-discord-application-and-bot-and-copy-your-token) + - [✅ Task: Invite your bot to your server](#-task-invite-your-bot-to-your-server) +- [2. Installing Modules: `discord.py` and `python-dotenv`](#2-installing-modules-discordpy-and-python-dotenv) + - [✅ Task: Install `discord.py`](#-task-install-discordpy) + - [✅ Task: Install `python-dotenv`](#-task-install-python-dotenv) +- [3. Creating a Bot and Adding it to your server](#3-creating-a-bot-and-adding-it-to-your-server) + - [✅ Task: Create 2 files: `main.py` and `.env`](#-task-create-2-files-mainpy-and-env) + - [Environment Variables](#environment-variables) +- [4. Basic Bot](#4-basic-bot) + - [✅ Task: Bringing the bot to life](#-task-bringing-the-bot-to-life) + - [Events and Callbacks](#events-and-callbacks) + - [✅ Task: "Hello, World!"](#-task-hello-world) + - [💡 Challenge: `Client` attributes](#-challenge-client-attributes) + - [Receiving Messages](#receiving-messages) + - [Sending Messages](#sending-messages) + - [✅ Task: Respond to "!hello" with "Hello, {username}"](#-task-respond-to-hello-with-hello-username) +- [5. Cogs](#5-cogs) + - [✅ Task: Refactor `main.py` to support Cogs (part 1)](#-task-refactor-mainpy-to-support-cogs-part-1) + - [✅ Task: Create a Hello Cog](#-task-create-a-hello-cog) + - [✅ Task: Refactor `main.py` to support Cogs (part 2)](#-task-refactor-mainpy-to-support-cogs-part-2) +- [6. ✅ Task: Create a Cog for all your 'Goodbye' commands](#6--task-create-a-cog-for-all-your-goodbye-commands) +- [7. [💡 Extension] Host your bot on Heroku](#7--extension-host-your-bot-on-heroku) +- [Related Links:](#related-links) + +
+ +--- + +## 0. Create a Discord Account and Discord Server +* Before creating our bot, please make sure you create a Discord Account (it's free!) and a Discord Server to test your bot in +### ✅ Task: Create a Discord Account + > 📝 NOTE: If you already have a Discord account, you can skip this task + * You can register for a Discord account [here](https://discord.com/register). + * ![](./images/2022-08-06-16-47-20.png) +### ✅ Task: Create a Discord Server + > 📝 NOTE: Discord servers are sometimes refered to as **'guilds'** in some documentation (because some people confuse the word 'server' with computer servers 🗄️ XD) + * this server will be used for you to test your bot + * Follow Discord's documentation on [How do I create a server?](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-) + 1. Click on the "+" button at the bottom of the left hand column on Discord + * ![](./images/2022-08-06-16-52-41.png) + 2. Fill in the server details + * You can follow these options: Create My Own > For me and my friends > Name your server > "Create" + +--- + +## 1. Create an your Bot and Add it to your Server + > Images and instructions for this part were sourced from [Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free | freeCodeCamp](https://www.freecodecamp.org/news/create-a-discord-bot-with-python/) + +### ✅ Task: Create a Discord Application and Bot, and copy your Token + * Login to the [Developer Portal's Applications Page](https://discord.com/developers/applications) + * Click on "New Application" + * ![](./images/2022-08-06-16-57-29.png) + * Give the application a name and click "Create" + * ![](./images/2022-08-06-16-57-47.png) + * Go to the "Bot" tab and then click "Add Bot" and "Yes, do it!" + * ![](./images/2022-08-06-17-01-31.png) + * Now your bot has been created! Next step is to copy the token and paste it in a notepad or create a file called `.env` [(Environment Variables)](#environment-variables) and paste it in there for now + * ![](./images/2022-08-06-17-02-00.png) + > 📝 NOTE: This token is your bot's password so don't share it with anybody. It could allow someone to log in to your bot and do all sorts of bad things. + > + > You are only able to see this token once (on the creation of the bot) but you can regenerate the token if it accidentally gets shared. + +### ✅ Task: Invite your bot to your server + * Go to the "OAuth2 > URL Generator" tab. Then select "bot" under the "scopes" section + * ![](./images/2022-08-06-17-11-00.png) + * Now choose the permissions you want for the bot. You can either give it minimal permissions as we are making a simple bot for this workshop but might have to add more permissions as you progress through the other workshops and add more features or you could give it many permissions upfront and not have to worry about permissions later. + * ![](./images/2022-08-06-17-11-30.png) + * Then copy the generated URL, open it in a new tab and add your bot to your server. + > 📝 NOTE: Be careful when giving your bot "Administrator" permissions + +--- + +## 2. Installing Modules: `discord.py` and `python-dotenv` + > 📝 NOTE: You can skip this step if you're using **Replit** as it can [automatically import](https://docs.replit.com/programming-ide/installing-packages#direct-imports) packages/modules it for you + * Now we're going to start writing the code for our bot! + * Before we begin creating the bot, we have to install a few modules + +
+ 🧩 Hint: Accessing the Terminal in VS Code + + * There are 2 ways to access the terminal in VS Code: + 1. using the shortcut ```Ctrl + Shift + ` ``` + 2. Terminal > New Terminal + * ![](./images/vscode_access_terminal.gif) + +
+ + ### ✅ Task: Install `discord.py` + * `discord.py` is basically a set of tools which will allow us to control our bot with simple function calls. + * you can find the documentation for `discord.py` over [here](https://discordpy.readthedocs.io/en/stable/index.html#) + * to install it, type this into your terminal: + ``` + pip install -U discord.py + ``` + ### ✅ Task: Install `python-dotenv` + * `python-dotenv` is used to access our secret Discord token, which we will store in a `.env` file + * you can find the documentation for it over [here](https://pypi.org/project/python-dotenv/) + * to install it, type this into your terminal: + ``` + pip install -U python-dotenv + ``` + > 🙋 **Ask for help**: Let us know if you run into any errors during installation and we'll try to help you out! + +--- + +## 3. Creating a Bot and Adding it to your server + +### ✅ Task: Create 2 files: `main.py` and `.env` + * `main.py` is the file that we will run when we want to run our bot - it's the main file for our bot's code + * `.env` is an [environment variable](#environment-variables) file + +
+🧩 Creating Files in VS Code + +* ![](./images/vscode_create_main_and_env_files.gif) + +
+ +### Environment Variables +
+❓ What are environment variables? + +When a program is run, it may need information from the operating system to configure its behaviour. This might include the operating system, current running folder, or more important things like passwords to various services (Discord here!). Basically, environment variables are variables/information about the environment its running on. They are a useful tool in providing information to your program, which is separate from your code. Developers commonly use `.env` files to specify these variables. + +![](./images/1_BIgXzxgolWVDBNq5F_eZpg.png)
+* `.env` have several advantages: + 1. They help different developers to keep their passwords separate from each other. + 2. When using a VCS (GitHub), you can prevent your `.env` file from being uploaded to the internet, thus protecting all of your passwords. +* [VS Code] To use a `.env` file, first make a `.env` file in the same folder as your code: + ```python + # ./.env + TOKEN=example.token.abc123 + ``` +* [Replit] If you're using Replit to develop your bot, Replit does not let you create a `.env` file so go to the "Secrets" tab and create a key-value pair for your token + * ![](./images/2022-08-06-17-17-49.png) +* Then in your code file: + ```python + # ./main.py + import os + from dotenv import load_dotenv + load_dotenv() + TOKEN = os.getenv('TOKEN') + ``` +* Try changing the content of your `.env` file and doing `print(TOKEN)`, what happens? + +> **💡 Challenge**
+> Can you try **defining your own environment variable** (besides `TOKEN`), and **printing it** to the console? How about **printing the current user** using only environment variables? (Hint: Try printing all environment variables with `print(os.environ)`) + --- -## About DecodED +## 4. Basic Bot +* Cool, now we're ready to start coding up our bot! We'll start simple by making it log something to our console when it's online and repond to messages that start with `!hello` +### ✅ Task: Bringing the bot to life +* Our bot is still offline, so let's start by making it come online +* First, we need to import `discord` to be able to use all of its modules + ```python + # ./main.py, add this at the top with other imports + import discord + ``` +* Then we create an instance of [`Client`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client), which is out connection to discord, and run it + ```python + # ./main.py, after defining TOKEN + client = discord.Client() # creates the bot + + client.run(TOKEN) # runs the bot + ``` +* Now if you run your code, your bot should come online in your Discord server - it's alive! + +### Events and Callbacks +* Now the bot is online but it can't do anything yet, we haven't added any functionality to our bot +* `discord.py` is an asynchronous library, which means we do things using *events* and *callbacks* +
+ 🔍 What are events and callbacks? + + * **events**: actions or occurrences recognised by the program + * eg. when the bot is done setting up and is ready to operate, when a message is sent, etc. + * **callback**: a function that is called when an event happens + * eg. the `on_ready()` event is called when the bot has finished logging in and setting things up + * to register an event, we use a decorator, `@client.event` on the callback function's definition -* DecodED is a series of workshops run every year by HackMelbourne to help students/teach students with introductory - programming background to create some cool things with code. In past years, we created full-stack websites and a Space Invaders game. This year, we will be creating a Discord bot! -* We are also trying a new teaching style this year so let us know if you like it! +
+
+ 🔍 What is async and await? -### Dates + Often in coding, you will need to perform a task, and wait for the response before you can do anything. An example would be Gmail, the website needs to wait for the mail to send, before telling you it's sent. + Using `async` on a function lets Python know that this task involves waiting for something: + ```python + async def send_mail(): + await login() + await send() + ``` + and `await` tells Python to wait for an `async` function to finish before proceeding: + ```python + await send_mail() + print("Your mail was sent!") + # As opposed to + send_mail() + print("This will be printed immediately") + ``` + In the context of discord.py, we can use `async` on our functions to tell discord.py it's going to do a long-running task, and `await` to do that task: + ```python + async def on_join(self, ctx): + await ctx.send("Welcome to the server!") + ``` + > 🔗 More about asynchronous programming: [Getting Started With Async Features in Python | Real Python](https://realpython.com/python-async-features/) -Lesson | Location | People | Date -| -- | -- | -- | -- | -Foundations (Basic) | Alan Gilbert 101 and Zoom | Xin Yu | 2pm, Wednesday 24th August -Meme bot (easy) | Zoom | Aly, Minh | Wednesday 31st August -Music bot | Alan Gilbert 121 (Theatre) and Zoom | Aryan, Ryan | Wednesday 7th September -Poll | Alan Gilbert G20 and Zoom | Jerry, Hoan | Wednesday 14th September -TicTacToe bot | Alan Gilbert 103 and Zoom | Warren, Daniel | 2pm, Wednesday 21st September +
+ + ```python + # ./main.py, in between defining the client and running it + @client.event # 👈 this is a function decorator + async def on_ready(): # 👈 on_ready() is a callback + # code in on_ready() will be run after the bot is done logging in and setting up -### Zoom Link: https://unimelb.zoom.us/j/88528442813?pwd=WlYrQ3pHcm5xMXpGQkZZSllZZTNvQT09 -Password: 323737 + await message.channel.send("Hi") + ``` -## About this repository -* This repository contains: - * Participant Workbook - * each workbook is made up of multiple Parts which are then made up of multiple **✅ Tasks** that the participant - should complete, with **🧩 Hints** that help guide them in completing the Task, and **💡 Extensions** that are - optional tasks that can be done if the participant has completed the Task ahead of schedule - * Workshop Recordings - * Code for the Discord Bot being created +### ✅ Task: "Hello, World!" +* Add some code into the `on_ready()` function to print "Hello, World!" when your bot is ready (note, this will appear in the terminal, not on Discord) +> 🙋 Try doing this on your own but do let us know if you are stuck or need help understanding the idea of *events* and *callbacks* (we can try to explain it differently!) -## Workshop 1 - Introduction to Discord.py +### 💡 Challenge: `Client` attributes +> Try printing some information about your bot when it is ready. You can access information about your bot through the `client` variable you've created. You can find a list of attributes in for the `Client` class [here](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client.users). +> * for example, [`client.user`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client.user) represents the connected client (printing this will give us the bot's username and id). +> * `client.user` is of the [`ClientUser`](https://discordpy.readthedocs.io/en/stable/api.html#discord.ClientUser) class which has a bunch of attributes itself and you can access as well; for example: +> * `client.user.name` +> * `client.user.id` +> * try printing these `on_ready()`, what do these attributes store? -> About DecodED3, Covers basics of Discord.py, create a basic bot that says "Hello, World!", learn about the basic -> structure of Discord bots. -> -> Hosted by Xin Yu +### Receiving Messages +* As said before, there are many events that we can register and 'listen' to. You can check out [Discord Docs > Event Reference](https://discordpy.readthedocs.io/en/stable/api.html#event-reference) for more events. +* We'll introduce another important event in this workshop: [`on_message()`](https://discordpy.readthedocs.io/en/stable/api.html#discord.on_message), which is called whenever a message is sent +* Here's an example of how to print every message's content into the console + ```python + # ./main.py, in between defining the client and running it + @client.event + async def on_message(msg): + print(msg.content) + ``` +* The message being sent is stored in the `msg` variable, which is of the [Message](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message) class. + * This means that we can access the `msg`'s attributes as laid out in the [Message documentation]((https://discordpy.readthedocs.io/en/stable/api.html#discord.Message)). + * A few key attributes that you might want to use are: + * `msg.author` + * `msg.channel` + * `msg.content` + * `msg.reactions` + * ✅ Task: Try printing some of these attributes out within the `on_message()` function and sending some messages. What values do each of these attributes hold? -* [📔Participant Workbook](/w1/) -* [🐍Python Cheatsheet](/w1/python_cheatsheet.md) -* [🐍Python Setup](/w1/python_setup.md) -* [👾Discord.py Cheatsheet](/w1/discord_py_cheatsheet.md) -* [🔗Discord.py Documentation](https://discordpy.readthedocs.io/en/stable/index.html) -* [Workshop Recording] Coming Soon! +### Sending Messages +* To send messages, we can use the [.send()](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel.send) method on [TextChannels](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel) + ```python + # ./main.py, in between defining the client and running it + @client.event + async def on_message(msg): + if msg.author == client.user: # ❓ Question for participants: What is this if statement for? + return + await msg.channel.send("Good Morning!") + ``` +* ❓ Question for participants: What is the `if` statement doing? Why do we need it? What happens if we remove it? -## Workshop 2 - Meme Bot +### ✅ Task: Respond to "!hello" with "Hello, {username}" +* Given that we know how to receive and send messages, try amending/adding some code in the `on_message()` to make your bot send "Hello, {username}" to anyone who sends messages starting with `$hello` (but replace {username} with the actual sender's username). -> Who doesn’t love a good meme? Join us and create the functionality to meme on your friends so hard that they wished -> they had their own meme bot. -> -> Hosted by Aly and Minh +
+🧩 Hint(s) + +* you should check the contents of messages being sent; maybe with another conditional (`if` statement)? +* note that `msg.content` is a string, what string methods do you know of? (🔗[Python String Methdos | w3schools](https://www.w3schools.com/python/python_ref_string.asp)) + +
+ +--- -* [📔Participant Workbook](/w2/) -* [Workshop Recording] +## 5. Cogs +> 🔗 [Cogs Documentation](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html) +* Now we have a basic working bot as well a basic idea for how to expand our bot's functionality. But all of our code is currently located in 1 file, the `main.py` file and as we add more and more functionality to our bot, this file can grow to become very large. We should make our code more modular and compartmentalise/group each similar parts of our bot into their own files and `discord.py` allows us to do that via **Cogs** +* A cog is a collection of commands, listeners and state + * commands: functions that will be called when `[command_prefix][command_function_name]` is sent + * the `[command_prefix]` will be defined when we create our Bot instance + * (event) listeners + * state -## Workshop 3 - Music Bot +### ✅ Task: Refactor `main.py` to support Cogs (part 1) +* You can either edit your existing `main.py` file or rename the old one `main-no-cogs.py` and create a new `main.py` to keep the cogless code +* Instead of importing all of the modules form the discord library, we can import a specific module called `command` + ```python + # ./main.py, replace `import discord` with... + from discord.ext import commands + ``` +* We will still require getting the TOKEN from environment variables so you can leave that part as is +* Instead of a Client instance, we will create a Bot instance instead + ```python + # ./main.py, replace `client = discord.Client()` with... + client = commands.Bot(command_prefix = "!") # instead of a client, we create a Bot instance + ``` +### ✅ Task: Create a Hello Cog +* create a folder called `cogs`, this is where you will store your cogs +* within that folder, create a file called `hello.py`, this is a cog, ie. a file containing listeners, commands and states related to 'hello-ing' +* to create a cog, we have to import the `commands` module from `discord.ext` again +* then define the cog as a class, with the class name corresponding to the cog's name (since we want to call our cog `Hello`, we name our file `hello.py` and create a class called `Hello()`) + * you don't have to understand too much about classes, objects etc. (these are all concepts of Object-Oriented Programming), just get the gist that cogs are classes and they store a collection of functions + ```python + # ./cogs/hello.py + from discord.ext import commands -> Ever since YouTube banned music bots, discord servers have been desperately lacking some tunes. Impress your friends -> by bringing them back by building your own bot with the ability to play music, plus additional music controls! Now you -> can resume your lo-fi beat study sessions with your mates! -> -> Hosted by Aryan and Ryan + class Hello(commands.Cog): # 👈 cog class called `Hello` + ``` +* within the class, we will have a few functions + * the first one is the `__init__()` function which is called whenever an object is created from the class (ie. whenever an object is *init*ialised) + * for now, we are going to give it an attribute called `client`, which will store a client containing information about our bot, same as in `main.py` + ```python + # ./cogs/hello.py, first function inside the Hello class + def __init__(self, client): + self.client = client + ``` +* now we're ready to add our event listeners and commands + * one of the event listenevers from before was the `on_ready()` event, writing it in a cog is very similar to before, just with a slightly different decorator: + ```python + # ./cogs/hello.py, inside the Hello class + @commands.Cog.listener() # 👈 this is a decorator for events/listeners + async def on_ready(self): + print(f'We have logged in as {self.client.user}') + ``` + * writing commands is a bit easier from before, instead of having many `if` statements that check the contents of each message, we define commands, which are functions and the function name will be automatically checked for in messages + ```python + # ./cogs/hello.py, inside the Hello class + @commands.command() # this is for making a command + async def hello(self, ctx): # 👈 called when messages contain `!hello` + await ctx.send(f'Hi!') + ``` + * `ctx` is of the [Context](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html?highlight=context#context) class + * ✅ Task: Try amending the code within the `hello()` function to behave similarly to the `on_message()` function from before. Make it send "Hello, {username}" whenever a user sends "!hello" +
+ 🧩 Hint -* [📔Participant Workbook](/w3/) -* [Workshop Recording] + * you can use it to access the message that triggered the command call using `ctx.message` + * remember that messages have a `.author` attribute that contains the author of the message -## Workshop 4 - Polling Bot +
-> Caught up in your server arguing why Minecraft is (definitively) the best game? Why not run a poll on your server and -> prove your friends wrong? Learn to build your own polling bot to settle your arguments in style 😎 -> -> Jerry and Hoan +* Lastly, we need to have a setup function, which is located **outside** the Hello class + * this function will be called in `main.py` + ```python + # ./cogs/hello.py, outside the Hello class + def setup(bot): # 👈 a extension must have a setup function + bot.add_cog(Hello(bot)) # 👈 adding the cog + ``` + +### ✅ Task: Refactor `main.py` to support Cogs (part 2) +* Now, back in `main.py`, instead of having all of our bot's logic in the file, we have moved them into cogs that are located in the `./cogs` folder and now we should load up all of our cogs + ```python + # ./main.py, after defining the client + # 👇 Looks inside the /cogs/ folder and loads up all of our cogs + for filename in os.listdir("./cogs"): + if filename.endswith(".py"): + client.load_extension("cogs." + filename[:-3]) # calls the cog's `setup()` function + + client.run(TOKEN) + ``` + +* This change might not look very important right now; in fact, using cogs may seem more troublesome at the moment; but as you add more functionality to your bot, you will realise that your `main.py` will grow too large and not-easily-readable. Thus, the importance of encapsulating our different functionalities into different cogs and calling them in the main file + +## 6. ✅ Task: Create a Cog for all your 'Goodbye' commands +* you can call the cog `bye.py` +* add commands such as + * `!goodbye`, which will prompt the bot to send a message: "Goodbye, {username}!" + * `!goodnight`, which will prompt the bot to react to the message with 💤 + +--- -* [📔Participant Workbook](/w4/) -* [Workshop Recording] +## 7. [💡 Extension] Host your bot on Heroku +> this allows your bot to run continuously without having to open VSCode or keep your repl.it tab running -## Workshop 5 - Tic Tac Toe +* [Hosting your discord.py bot on Heroku](https://github.com/squid/discord-py-heroku) +* [How to host a discord.py bot with Heroku and GitHub](https://medium.com/analytics-vidhya/how-to-host-a-discord-py-bot-on-heroku-and-github-d54a4d62a99e) -> Fancy a game, but don’t want to leave your friends in discord? In this lesson, you’ll learn to implement a tic tac toe -> game within the Discord bot so you can vs your friends whenever you wish! -> -> Warren and Weng Jae (Daniel) +## Related Links: +* [Creating a Bot Account | discord.py](https://discordpy.readthedocs.io/en/stable/discord.html) +* [Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free | freeCodeCamp](https://www.freecodecamp.org/news/create-a-discord-bot-with-python/) -* [📔Participant Workbook](/w5/) -* [Workshop Recording] diff --git a/cogless.py b/cogless.py new file mode 100644 index 0000000..04aed33 --- /dev/null +++ b/cogless.py @@ -0,0 +1,22 @@ +import discord +import os + +from dotenv import load_dotenv +load_dotenv() +TOKEN = os.getenv('TOKEN') + +client = discord.Client() + +@client.event +async def on_ready(): + print(f'We have logged in as {client.user}') + +@client.event +async def on_message(msg): + # print(msg.content) + if msg.author == client.user: + return + if msg.content.startswith('$hello'): + await msg.channel.send(f'Hello, {msg.author.name}!') + +client.run(TOKEN) diff --git a/w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png b/images/1_BIgXzxgolWVDBNq5F_eZpg.png similarity index 100% rename from w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png rename to images/1_BIgXzxgolWVDBNq5F_eZpg.png diff --git a/w1/images/2022-07-27-20-48-43.png b/images/2022-07-27-20-48-43.png similarity index 100% rename from w1/images/2022-07-27-20-48-43.png rename to images/2022-07-27-20-48-43.png diff --git a/w1/images/2022-07-27-20-50-00.png b/images/2022-07-27-20-50-00.png similarity index 100% rename from w1/images/2022-07-27-20-50-00.png rename to images/2022-07-27-20-50-00.png diff --git a/w1/images/2022-07-27-20-51-23.png b/images/2022-07-27-20-51-23.png similarity index 100% rename from w1/images/2022-07-27-20-51-23.png rename to images/2022-07-27-20-51-23.png diff --git a/w1/images/2022-08-06-16-47-20.png b/images/2022-08-06-16-47-20.png similarity index 100% rename from w1/images/2022-08-06-16-47-20.png rename to images/2022-08-06-16-47-20.png diff --git a/w1/images/2022-08-06-16-52-41.png b/images/2022-08-06-16-52-41.png similarity index 100% rename from w1/images/2022-08-06-16-52-41.png rename to images/2022-08-06-16-52-41.png diff --git a/w1/images/2022-08-06-16-57-29.png b/images/2022-08-06-16-57-29.png similarity index 100% rename from w1/images/2022-08-06-16-57-29.png rename to images/2022-08-06-16-57-29.png diff --git a/w1/images/2022-08-06-16-57-47.png b/images/2022-08-06-16-57-47.png similarity index 100% rename from w1/images/2022-08-06-16-57-47.png rename to images/2022-08-06-16-57-47.png diff --git a/w1/images/2022-08-06-17-01-31.png b/images/2022-08-06-17-01-31.png similarity index 100% rename from w1/images/2022-08-06-17-01-31.png rename to images/2022-08-06-17-01-31.png diff --git a/w1/images/2022-08-06-17-02-00.png b/images/2022-08-06-17-02-00.png similarity index 100% rename from w1/images/2022-08-06-17-02-00.png rename to images/2022-08-06-17-02-00.png diff --git a/w1/images/2022-08-06-17-05-59.png b/images/2022-08-06-17-05-59.png similarity index 100% rename from w1/images/2022-08-06-17-05-59.png rename to images/2022-08-06-17-05-59.png diff --git a/w1/images/2022-08-06-17-11-00.png b/images/2022-08-06-17-11-00.png similarity index 100% rename from w1/images/2022-08-06-17-11-00.png rename to images/2022-08-06-17-11-00.png diff --git a/w1/images/2022-08-06-17-11-30.png b/images/2022-08-06-17-11-30.png similarity index 100% rename from w1/images/2022-08-06-17-11-30.png rename to images/2022-08-06-17-11-30.png diff --git a/w1/images/2022-08-06-17-17-22.png b/images/2022-08-06-17-17-22.png similarity index 100% rename from w1/images/2022-08-06-17-17-22.png rename to images/2022-08-06-17-17-22.png diff --git a/w1/images/2022-08-06-17-17-49.png b/images/2022-08-06-17-17-49.png similarity index 100% rename from w1/images/2022-08-06-17-17-49.png rename to images/2022-08-06-17-17-49.png diff --git a/w1/images/deployment_add_buildpack.gif b/images/deployment_add_buildpack.gif similarity index 100% rename from w1/images/deployment_add_buildpack.gif rename to images/deployment_add_buildpack.gif diff --git a/w1/images/vscode_access_terminal.gif b/images/vscode_access_terminal.gif similarity index 100% rename from w1/images/vscode_access_terminal.gif rename to images/vscode_access_terminal.gif diff --git a/w1/images/vscode_create_main_and_env_files.gif b/images/vscode_create_main_and_env_files.gif similarity index 100% rename from w1/images/vscode_create_main_and_env_files.gif rename to images/vscode_create_main_and_env_files.gif diff --git a/main.py b/main.py index 04aed33..8d85721 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,16 @@ -import discord +# main.py +from discord.ext import commands import os - from dotenv import load_dotenv + load_dotenv() TOKEN = os.getenv('TOKEN') -client = discord.Client() - -@client.event -async def on_ready(): - print(f'We have logged in as {client.user}') +client = commands.Bot(command_prefix = "!") -@client.event -async def on_message(msg): - # print(msg.content) - if msg.author == client.user: - return - if msg.content.startswith('$hello'): - await msg.channel.send(f'Hello, {msg.author.name}!') +# Looks inside the /cogs/ folder and loads up all of our cogs +for filename in os.listdir("./cogs"): + if filename.endswith(".py"): + client.load_extension("cogs." + filename[:-3]) client.run(TOKEN) diff --git a/main2.py b/main2.py deleted file mode 100644 index 8d85721..0000000 --- a/main2.py +++ /dev/null @@ -1,16 +0,0 @@ -# main.py -from discord.ext import commands -import os -from dotenv import load_dotenv - -load_dotenv() -TOKEN = os.getenv('TOKEN') - -client = commands.Bot(command_prefix = "!") - -# Looks inside the /cogs/ folder and loads up all of our cogs -for filename in os.listdir("./cogs"): - if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) - -client.run(TOKEN) diff --git a/w1/python_cheatsheet.md b/python_cheatsheet.md similarity index 100% rename from w1/python_cheatsheet.md rename to python_cheatsheet.md diff --git a/w1/python_setup.md b/python_setup.md similarity index 100% rename from w1/python_setup.md rename to python_setup.md diff --git a/w1/README.md b/w1/README.md deleted file mode 100644 index e46145f..0000000 --- a/w1/README.md +++ /dev/null @@ -1,397 +0,0 @@ -

[Participant's Workbook] Introduction to Discord.py

- -> Related Pages: [DecodED 3](./README.md) - ---- - -

Table of Contents

-
-Table of Contents - -- [0. Create a Discord Account and Discord Server](#0-create-a-discord-account-and-discord-server) - - [✅ Task: Create a Discord Account](#-task-create-a-discord-account) - - [✅ Task: Create a Discord Server](#-task-create-a-discord-server) -- [1. Create an your Bot and Add it to your Server](#1-create-an-your-bot-and-add-it-to-your-server) - - [✅ Task: Create a Discord Application and Bot, and copy your Token](#-task-create-a-discord-application-and-bot-and-copy-your-token) - - [✅ Task: Invite your bot to your server](#-task-invite-your-bot-to-your-server) -- [2. Installing Modules: `discord.py` and `python-dotenv`](#2-installing-modules-discordpy-and-python-dotenv) - - [✅ Task: Install `discord.py`](#-task-install-discordpy) - - [✅ Task: Install `python-dotenv`](#-task-install-python-dotenv) -- [3. Creating a Bot and Adding it to your server](#3-creating-a-bot-and-adding-it-to-your-server) - - [✅ Task: Create 2 files: `main.py` and `.env`](#-task-create-2-files-mainpy-and-env) - - [Environment Variables](#environment-variables) -- [4. Basic Bot](#4-basic-bot) - - [✅ Task: Bringing the bot to life](#-task-bringing-the-bot-to-life) - - [Events and Callbacks](#events-and-callbacks) - - [✅ Task: "Hello, World!"](#-task-hello-world) - - [💡 Challenge: `Client` attributes](#-challenge-client-attributes) - - [Receiving Messages](#receiving-messages) - - [Sending Messages](#sending-messages) - - [✅ Task: Respond to "!hello" with "Hello, {username}"](#-task-respond-to-hello-with-hello-username) -- [5. Cogs](#5-cogs) - - [✅ Task: Refactor `main.py` to support Cogs (part 1)](#-task-refactor-mainpy-to-support-cogs-part-1) - - [✅ Task: Create a Hello Cog](#-task-create-a-hello-cog) - - [✅ Task: Refactor `main.py` to support Cogs (part 2)](#-task-refactor-mainpy-to-support-cogs-part-2) -- [6. ✅ Task: Create a Cog for all your 'Goodbye' commands](#6--task-create-a-cog-for-all-your-goodbye-commands) -- [7. [💡 Extension] Host your bot on Heroku](#7--extension-host-your-bot-on-heroku) -- [Related Links:](#related-links) - -
- ---- - -## 0. Create a Discord Account and Discord Server -* Before creating our bot, please make sure you create a Discord Account (it's free!) and a Discord Server to test your bot in -### ✅ Task: Create a Discord Account - > 📝 NOTE: If you already have a Discord account, you can skip this task - * You can register for a Discord account [here](https://discord.com/register). - * ![](./images/2022-08-06-16-47-20.png) -### ✅ Task: Create a Discord Server - > 📝 NOTE: Discord servers are sometimes refered to as **'guilds'** in some documentation (because some people confuse the word 'server' with computer servers 🗄️ XD) - * this server will be used for you to test your bot - * Follow Discord's documentation on [How do I create a server?](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-) - 1. Click on the "+" button at the bottom of the left hand column on Discord - * ![](./images/2022-08-06-16-52-41.png) - 2. Fill in the server details - * You can follow these options: Create My Own > For me and my friends > Name your server > "Create" - ---- - -## 1. Create an your Bot and Add it to your Server - > Images and instructions for this part were sourced from [Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free | freeCodeCamp](https://www.freecodecamp.org/news/create-a-discord-bot-with-python/) - -### ✅ Task: Create a Discord Application and Bot, and copy your Token - * Login to the [Developer Portal's Applications Page](https://discord.com/developers/applications) - * Click on "New Application" - * ![](./images/2022-08-06-16-57-29.png) - * Give the application a name and click "Create" - * ![](./images/2022-08-06-16-57-47.png) - * Go to the "Bot" tab and then click "Add Bot" and "Yes, do it!" - * ![](./images/2022-08-06-17-01-31.png) - * Now your bot has been created! Next step is to copy the token and paste it in a notepad or create a file called `.env` [(Environment Variables)](#environment-variables) and paste it in there for now - * ![](./images/2022-08-06-17-02-00.png) - > 📝 NOTE: This token is your bot's password so don't share it with anybody. It could allow someone to log in to your bot and do all sorts of bad things. - > - > You are only able to see this token once (on the creation of the bot) but you can regenerate the token if it accidentally gets shared. - -### ✅ Task: Invite your bot to your server - * Go to the "OAuth2 > URL Generator" tab. Then select "bot" under the "scopes" section - * ![](./images/2022-08-06-17-11-00.png) - * Now choose the permissions you want for the bot. You can either give it minimal permissions as we are making a simple bot for this workshop but might have to add more permissions as you progress through the other workshops and add more features or you could give it many permissions upfront and not have to worry about permissions later. - * ![](./images/2022-08-06-17-11-30.png) - * Then copy the generated URL, open it in a new tab and add your bot to your server. - > 📝 NOTE: Be careful when giving your bot "Administrator" permissions - ---- - -## 2. Installing Modules: `discord.py` and `python-dotenv` - > 📝 NOTE: You can skip this step if you're using **Replit** as it can [automatically import](https://docs.replit.com/programming-ide/installing-packages#direct-imports) packages/modules it for you - * Now we're going to start writing the code for our bot! - * Before we begin creating the bot, we have to install a few modules - -
- 🧩 Hint: Accessing the Terminal in VS Code - - * There are 2 ways to access the terminal in VS Code: - 1. using the shortcut ```Ctrl + Shift + ` ``` - 2. Terminal > New Terminal - * ![](./images/vscode_access_terminal.gif) - -
- - ### ✅ Task: Install `discord.py` - * `discord.py` is basically a set of tools which will allow us to control our bot with simple function calls. - * you can find the documentation for `discord.py` over [here](https://discordpy.readthedocs.io/en/stable/index.html#) - * to install it, type this into your terminal: - ``` - pip install -U discord.py - ``` - ### ✅ Task: Install `python-dotenv` - * `python-dotenv` is used to access our secret Discord token, which we will store in a `.env` file - * you can find the documentation for it over [here](https://pypi.org/project/python-dotenv/) - * to install it, type this into your terminal: - ``` - pip install -U python-dotenv - ``` - > 🙋 **Ask for help**: Let us know if you run into any errors during installation and we'll try to help you out! - ---- - -## 3. Creating a Bot and Adding it to your server - -### ✅ Task: Create 2 files: `main.py` and `.env` - * `main.py` is the file that we will run when we want to run our bot - it's the main file for our bot's code - * `.env` is an [environment variable](#environment-variables) file - -
-🧩 Creating Files in VS Code - -* ![](./images/vscode_create_main_and_env_files.gif) - -
- -### Environment Variables -
-❓ What are environment variables? - -When a program is run, it may need information from the operating system to configure its behaviour. This might include the operating system, current running folder, or more important things like passwords to various services (Discord here!). Basically, environment variables are variables/information about the environment its running on. They are a useful tool in providing information to your program, which is separate from your code. Developers commonly use `.env` files to specify these variables. - -![](./images/1_BIgXzxgolWVDBNq5F_eZpg.png) - -
- -* `.env` have several advantages: - 1. They help different developers to keep their passwords separate from each other. - 2. When using a VCS (GitHub), you can prevent your `.env` file from being uploaded to the internet, thus protecting all of your passwords. -* [VS Code] To use a `.env` file, first make a `.env` file in the same folder as your code: - ```python - # ./.env - TOKEN=example.token.abc123 - ``` -* [Replit] If you're using Replit to develop your bot, Replit does not let you create a `.env` file so go to the "Secrets" tab and create a key-value pair for your token - * ![](./images/2022-08-06-17-17-49.png) -* Then in your code file: - ```python - # ./main.py - import os - from dotenv import load_dotenv - load_dotenv() - TOKEN = os.getenv('TOKEN') - ``` -* Try changing the content of your `.env` file and doing `print(TOKEN)`, what happens? - -> **💡 Challenge**
-> Can you try **defining your own environment variable** (besides `TOKEN`), and **printing it** to the console? How about **printing the current user** using only environment variables? (Hint: Try printing all environment variables with `print(os.environ)`) - ---- - -## 4. Basic Bot -* Cool, now we're ready to start coding up our bot! We'll start simple by making it log something to our console when it's online and repond to messages that start with `!hello` -### ✅ Task: Bringing the bot to life -* Our bot is still offline, so let's start by making it come online -* First, we need to import `discord` to be able to use all of its modules - ```python - # ./main.py, add this at the top with other imports - import discord - ``` -* Then we create an instance of [`Client`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client), which is out connection to discord, and run it - ```python - # ./main.py, after defining TOKEN - client = discord.Client() # creates the bot - - client.run(TOKEN) # runs the bot - ``` -* Now if you run your code, your bot should come online in your Discord server - it's alive! - -### Events and Callbacks -* Now the bot is online but it can't do anything yet, we haven't added any functionality to our bot -* `discord.py` is an asynchronous library, which means we do things using *events* and *callbacks* -
- 🔍 What are events and callbacks? - - * **events**: actions or occurrences recognised by the program - * eg. when the bot is done setting up and is ready to operate, when a message is sent, etc. - * **callback**: a function that is called when an event happens - * eg. the `on_ready()` event is called when the bot has finished logging in and setting things up - * to register an event, we use a decorator, `@client.event` on the callback function's definition - -
-
- 🔍 What is async and await? - - Often in coding, you will need to perform a task, and wait for the response before you can do anything. An example would be Gmail, the website needs to wait for the mail to send, before telling you it's sent. - Using `async` on a function lets Python know that this task involves waiting for something: - ```python - async def send_mail(): - await login() - await send() - ``` - and `await` tells Python to wait for an `async` function to finish before proceeding: - ```python - await send_mail() - print("Your mail was sent!") - # As opposed to - send_mail() - print("This will be printed immediately") - ``` - In the context of discord.py, we can use `async` on our functions to tell discord.py it's going to do a long-running task, and `await` to do that task: - ```python - async def on_join(self, ctx): - await ctx.send("Welcome to the server!") - ``` - > 🔗 More about asynchronous programming: [Getting Started With Async Features in Python | Real Python](https://realpython.com/python-async-features/) - -
- - ```python - # ./main.py, in between defining the client and running it - @client.event # 👈 this is a function decorator - async def on_ready(): # 👈 on_ready() is a callback - # code in on_ready() will be run after the bot is done logging in and setting up - - await message.channel.send("Hi") - ``` - -### ✅ Task: "Hello, World!" -* Add some code into the `on_ready()` function to print "Hello, World!" when your bot is ready (note, this will appear in the terminal, not on Discord) -> 🙋 Try doing this on your own but do let us know if you are stuck or need help understanding the idea of *events* and *callbacks* (we can try to explain it differently!) - -### 💡 Challenge: `Client` attributes -> Try printing some information about your bot when it is ready. You can access information about your bot through the `client` variable you've created. You can find a list of attributes in for the `Client` class [here](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client.users). -> * for example, [`client.user`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client.user) represents the connected client (printing this will give us the bot's username and id). -> * `client.user` is of the [`ClientUser`](https://discordpy.readthedocs.io/en/stable/api.html#discord.ClientUser) class which has a bunch of attributes itself and you can access as well; for example: -> * `client.user.name` -> * `client.user.id` -> * try printing these `on_ready()`, what do these attributes store? - -### Receiving Messages -* As said before, there are many events that we can register and 'listen' to. You can check out [Discord Docs > Event Reference](https://discordpy.readthedocs.io/en/stable/api.html#event-reference) for more events. -* We'll introduce another important event in this workshop: [`on_message()`](https://discordpy.readthedocs.io/en/stable/api.html#discord.on_message), which is called whenever a message is sent -* Here's an example of how to print every message's content into the console - ```python - # ./main.py, in between defining the client and running it - @client.event - async def on_message(msg): - print(msg.content) - ``` -* The message being sent is stored in the `msg` variable, which is of the [Message](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message) class. - * This means that we can access the `msg`'s attributes as laid out in the [Message documentation]((https://discordpy.readthedocs.io/en/stable/api.html#discord.Message)). - * A few key attributes that you might want to use are: - * `msg.author` - * `msg.channel` - * `msg.content` - * `msg.reactions` - * ✅ Task: Try printing some of these attributes out within the `on_message()` function and sending some messages. What values do each of these attributes hold? - -### Sending Messages -* To send messages, we can use the [.send()](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel.send) method on [TextChannels](https://discordpy.readthedocs.io/en/stable/api.html#discord.TextChannel) - ```python - # ./main.py, in between defining the client and running it - @client.event - async def on_message(msg): - if msg.author == client.user: # ❓ Question for participants: What is this if statement for? - return - await msg.channel.send("Good Morning!") - ``` -* ❓ Question for participants: What is the `if` statement doing? Why do we need it? What happens if we remove it? - -### ✅ Task: Respond to "!hello" with "Hello, {username}" -* Given that we know how to receive and send messages, try amending/adding some code in the `on_message()` to make your bot send "Hello, {username}" to anyone who sends messages starting with `$hello` (but replace {username} with the actual sender's username). - -
-🧩 Hint(s) - -* you should check the contents of messages being sent; maybe with another conditional (`if` statement)? -* note that `msg.content` is a string, what string methods do you know of? (🔗[Python String Methdos | w3schools](https://www.w3schools.com/python/python_ref_string.asp)) - -
- ---- - -## 5. Cogs -> 🔗 [Cogs Documentation](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html) -* Now we have a basic working bot as well a basic idea for how to expand our bot's functionality. But all of our code is currently located in 1 file, the `main.py` file and as we add more and more functionality to our bot, this file can grow to become very large. We should make our code more modular and compartmentalise/group each similar parts of our bot into their own files and `discord.py` allows us to do that via **Cogs** -* A cog is a collection of commands, listeners and state - * commands: functions that will be called when `[command_prefix][command_function_name]` is sent - * the `[command_prefix]` will be defined when we create our Bot instance - * (event) listeners - * state - -### ✅ Task: Refactor `main.py` to support Cogs (part 1) -* You can either edit your existing `main.py` file or rename the old one `main-no-cogs.py` and create a new `main.py` to keep the cogless code -* Instead of importing all of the modules form the discord library, we can import a specific module called `command` - ```python - # ./main.py, replace `import discord` with... - from discord.ext import commands - ``` -* We will still require getting the TOKEN from environment variables so you can leave that part as is -* Instead of a Client instance, we will create a Bot instance instead - ```python - # ./main.py, replace `client = discord.Client()` with... - client = commands.Bot(command_prefix = "!") # instead of a client, we create a Bot instance - ``` -### ✅ Task: Create a Hello Cog -* create a folder called `cogs`, this is where you will store your cogs -* within that folder, create a file called `hello.py`, this is a cog, ie. a file containing listeners, commands and states related to 'hello-ing' -* to create a cog, we have to import the `commands` module from `discord.ext` again -* then define the cog as a class, with the class name corresponding to the cog's name (since we want to call our cog `Hello`, we name our file `hello.py` and create a class called `Hello()`) - * you don't have to understand too much about classes, objects etc. (these are all concepts of Object-Oriented Programming), just get the gist that cogs are classes and they store a collection of functions - ```python - # ./cogs/hello.py - from discord.ext import commands - - class Hello(commands.Cog): # 👈 cog class called `Hello` - ``` -* within the class, we will have a few functions - * the first one is the `__init__()` function which is called whenever an object is created from the class (ie. whenever an object is *init*ialised) - * for now, we are going to give it an attribute called `client`, which will store a client containing information about our bot, same as in `main.py` - ```python - # ./cogs/hello.py, first function inside the Hello class - def __init__(self, client): - self.client = client - ``` -* now we're ready to add our event listeners and commands - * one of the event listenevers from before was the `on_ready()` event, writing it in a cog is very similar to before, just with a slightly different decorator: - ```python - # ./cogs/hello.py, inside the Hello class - @commands.Cog.listener() # 👈 this is a decorator for events/listeners - async def on_ready(self): - print(f'We have logged in as {self.client.user}') - ``` - * writing commands is a bit easier from before, instead of having many `if` statements that check the contents of each message, we define commands, which are functions and the function name will be automatically checked for in messages - ```python - # ./cogs/hello.py, inside the Hello class - @commands.command() # this is for making a command - async def hello(self, ctx): # 👈 called when messages contain `!hello` - await ctx.send(f'Hi!') - ``` - * `ctx` is of the [Context](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html?highlight=context#context) class - * ✅ Task: Try amending the code within the `hello()` function to behave similarly to the `on_message()` function from before. Make it send "Hello, {username}" whenever a user sends "!hello" -
- 🧩 Hint - - * you can use it to access the message that triggered the command call using `ctx.message` - * remember that messages have a `.author` attribute that contains the author of the message - -
- -* Lastly, we need to have a setup function, which is located **outside** the Hello class - * this function will be called in `main.py` - ```python - # ./cogs/hello.py, outside the Hello class - def setup(bot): # 👈 a extension must have a setup function - bot.add_cog(Hello(bot)) # 👈 adding the cog - ``` - -### ✅ Task: Refactor `main.py` to support Cogs (part 2) -* Now, back in `main.py`, instead of having all of our bot's logic in the file, we have moved them into cogs that are located in the `./cogs` folder and now we should load up all of our cogs - ```python - # ./main.py, after defining the client - # 👇 Looks inside the /cogs/ folder and loads up all of our cogs - for filename in os.listdir("./cogs"): - if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) # calls the cog's `setup()` function - - client.run(TOKEN) - ``` - -* This change might not look very important right now; in fact, using cogs may seem more troublesome at the moment; but as you add more functionality to your bot, you will realise that your `main.py` will grow too large and not-easily-readable. Thus, the importance of encapsulating our different functionalities into different cogs and calling them in the main file - -## 6. ✅ Task: Create a Cog for all your 'Goodbye' commands -* you can call the cog `bye.py` -* add commands such as - * `!goodbye`, which will prompt the bot to send a message: "Goodbye, {username}!" - * `!goodnight`, which will prompt the bot to react to the message with 💤 - ---- - -## 7. [💡 Extension] Host your bot on Heroku -> this allows your bot to run continuously without having to open VSCode or keep your repl.it tab running - -* [Hosting your discord.py bot on Heroku](https://github.com/squid/discord-py-heroku) -* [How to host a discord.py bot with Heroku and GitHub](https://medium.com/analytics-vidhya/how-to-host-a-discord-py-bot-on-heroku-and-github-d54a4d62a99e) - -## Related Links: -* [Creating a Bot Account | discord.py](https://discordpy.readthedocs.io/en/stable/discord.html) -* [Python Discord Bot Tutorial – Code a Discord Bot And Host it for Free | freeCodeCamp](https://www.freecodecamp.org/news/create-a-discord-bot-with-python/) - diff --git a/w1/discord_py_cheatsheet.md b/w1/discord_py_cheatsheet.md deleted file mode 100644 index cba8b8f..0000000 --- a/w1/discord_py_cheatsheet.md +++ /dev/null @@ -1,6 +0,0 @@ -

Discord.py CheatSheet

- -## Events -* Basic Structure of Event Callbacks -``` -``` diff --git a/workbook-template.md b/workbook-template.md deleted file mode 100644 index 877f6ea..0000000 --- a/workbook-template.md +++ /dev/null @@ -1,28 +0,0 @@ -

Participant's Workbook Template

- -> Related Pages: [DecodED 3](./README.md) | [Participant's Handbook for Workshop 1: Intro to Discord.py](./workshop1/README.md) - - - ---- - -

Table of Contents

-
-Table of Contents - - -
- -## Links - - -## Part 1: - - - -## Part 2: - - - -## Part 3: - From 3cd4b2e45ceb6d2963df07ed4e9b6e9beb295c14 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 10:00:14 +1000 Subject: [PATCH 08/15] Make links go to branches --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d74092b..13ea1df 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,7 @@ Password: 323737 > > Hosted by Xin Yu -* [📔Participant Workbook](/w1/) -* [🐍Python Cheatsheet](/w1/python_cheatsheet.md) -* [🐍Python Setup](/w1/python_setup.md) -* [👾Discord.py Cheatsheet](/w1/discord_py_cheatsheet.md) -* [🔗Discord.py Documentation](https://discordpy.readthedocs.io/en/stable/index.html) +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/foundations-bot) * [Workshop Recording] Coming Soon! ## Workshop 2 - Meme Bot @@ -71,7 +67,7 @@ Password: 323737 > > Hosted by Aly and Minh -* [📔Participant Workbook](/w2/) +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/meme_bot) * [Workshop Recording] ## Workshop 3 - Music Bot @@ -82,7 +78,7 @@ Password: 323737 > > Hosted by Aryan and Ryan -* [📔Participant Workbook](/w3/) +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/music-bot) * [Workshop Recording] ## Workshop 4 - Polling Bot @@ -92,7 +88,7 @@ Password: 323737 > > Jerry and Hoan -* [📔Participant Workbook](/w4/) +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/poll_system) * [Workshop Recording] ## Workshop 5 - Tic Tac Toe @@ -102,5 +98,5 @@ Password: 323737 > > Warren and Weng Jae (Daniel) -* [📔Participant Workbook](/w5/) +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/tictactoe) * [Workshop Recording] From e719d6cae7e9ed38006ffe2f65ae54c4708aa590 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Wed, 24 Aug 2022 10:40:12 +1000 Subject: [PATCH 09/15] Update README - move async await down --- README.md | 61 ++++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index b96ef68..6701077 100644 --- a/README.md +++ b/README.md @@ -193,41 +193,13 @@ When a program is run, it may need information from the operating system to conf * eg. the `on_ready()` event is called when the bot has finished logging in and setting things up * to register an event, we use a decorator, `@client.event` on the callback function's definition - -
- 🔍 What is async and await? - - Often in coding, you will need to perform a task, and wait for the response before you can do anything. An example would be Gmail, the website needs to wait for the mail to send, before telling you it's sent. - Using `async` on a function lets Python know that this task involves waiting for something: - ```python - async def send_mail(): - await login() - await send() - ``` - and `await` tells Python to wait for an `async` function to finish before proceeding: - ```python - await send_mail() - print("Your mail was sent!") - # As opposed to - send_mail() - print("This will be printed immediately") - ``` - In the context of discord.py, we can use `async` on our functions to tell discord.py it's going to do a long-running task, and `await` to do that task: - ```python - async def on_join(self, ctx): - await ctx.send("Welcome to the server!") - ``` - > 🔗 More about asynchronous programming: [Getting Started With Async Features in Python | Real Python](https://realpython.com/python-async-features/) -
```python # ./main.py, in between defining the client and running it @client.event # 👈 this is a function decorator - async def on_ready(): # 👈 on_ready() is a callback + def on_ready(): # 👈 on_ready() is a callback # code in on_ready() will be run after the bot is done logging in and setting up - - await message.channel.send("Hi") ``` ### ✅ Task: "Hello, World!" @@ -249,7 +221,7 @@ When a program is run, it may need information from the operating system to conf ```python # ./main.py, in between defining the client and running it @client.event - async def on_message(msg): + def on_message(msg): print(msg.content) ``` * The message being sent is stored in the `msg` variable, which is of the [Message](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message) class. @@ -267,11 +239,36 @@ When a program is run, it may need information from the operating system to conf # ./main.py, in between defining the client and running it @client.event async def on_message(msg): - if msg.author == client.user: # ❓ Question for participants: What is this if statement for? + if msg.author == client.user: # ❓ Question for participants: Why do you think we need this if statement? return await msg.channel.send("Good Morning!") ``` -* ❓ Question for participants: What is the `if` statement doing? Why do we need it? What happens if we remove it? +
+ 🔍 What is async and await? + + Often in coding, you will need to perform a task, and wait for the response before you can do anything. An example would be Gmail, the website needs to wait for the mail to send, before telling you it's sent. + Using `async` on a function lets Python know that this task involves waiting for something: + ```python + async def send_mail(): + await login() + await send() + ``` + and `await` tells Python to wait for an `async` function to finish before proceeding: + ```python + await send_mail() + print("Your mail was sent!") + # As opposed to + send_mail() + print("This will be printed immediately") + ``` + In the context of discord.py, we can use `async` on our functions to tell discord.py it's going to do a long-running task, and `await` to do that task: + ```python + async def on_join(self, ctx): + await ctx.send("Welcome to the server!") + ``` + > 🔗 More about asynchronous programming: [Getting Started With Async Features in Python | Real Python](https://realpython.com/python-async-features/) + +
### ✅ Task: Respond to "!hello" with "Hello, {username}" * Given that we know how to receive and send messages, try amending/adding some code in the `on_message()` to make your bot send "Hello, {username}" to anyone who sends messages starting with `$hello` (but replace {username} with the actual sender's username). From 8935d4a886eea297fb4efafb7ccf7a014a77c005 Mon Sep 17 00:00:00 2001 From: Ryan Samarakoon Date: Fri, 26 Aug 2022 22:07:02 +1000 Subject: [PATCH 10/15] update main2.py for discord.py 2.0 --- main2.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/main2.py b/main2.py index 8d85721..6e8fb95 100644 --- a/main2.py +++ b/main2.py @@ -1,16 +1,21 @@ # main.py -from discord.ext import commands +import asyncio import os + +import discord +from discord.ext import commands from dotenv import load_dotenv load_dotenv() TOKEN = os.getenv('TOKEN') -client = commands.Bot(command_prefix = "!") +client = commands.Bot(command_prefix="!", intents=discord.Intents.all()) # Looks inside the /cogs/ folder and loads up all of our cogs for filename in os.listdir("./cogs"): if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) + # run function synchronously using subprocess + asyncio.run(client.load_extension(f"cogs.{filename[:-3]}")) + print("Loaded COG {}".format(filename)) client.run(TOKEN) From 286f308a637ce916983fa4194d29162a250e8dc6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Aug 2022 22:08:43 +1000 Subject: [PATCH 11/15] async await cog hello --- cogs/hello.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cogs/hello.py b/cogs/hello.py index 9cbe5d2..cb29569 100644 --- a/cogs/hello.py +++ b/cogs/hello.py @@ -1,18 +1,19 @@ from discord.ext import commands + class Hello(commands.Cog): def __init__(self, client): - self.client = client # defining bot as global var in class + self.client = client # defining bot as global var in class - @commands.Cog.listener() # this is a decorator for events/listeners + @commands.Cog.listener() # this is a decorator for events/listeners async def on_ready(self): print(f'We have logged in as {self.client.user}') - - @commands.command() # this is for making a command + @commands.command() # this is for making a command async def hello(self, ctx): print(ctx) await ctx.send(f'Hi!') - -def setup(bot): # a extension must have a setup function - bot.add_cog(Hello(bot)) # adding a cog \ No newline at end of file + + +async def setup(bot): # a extension must have a setup function + await bot.add_cog(Hello(bot)) # adding a cog From 4415aa5561f054475736ceff8c31e976f53fb488 Mon Sep 17 00:00:00 2001 From: Ryan Samarakoon Date: Sun, 28 Aug 2022 16:44:29 +1000 Subject: [PATCH 12/15] add intents to main --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 04aed33..3edde1d 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ load_dotenv() TOKEN = os.getenv('TOKEN') -client = discord.Client() +client = discord.Client(intents=discord.Intents.all()) @client.event async def on_ready(): From a4f70b442fdd2aa3d5114626aca4bfe8597eb957 Mon Sep 17 00:00:00 2001 From: Xin Yu Date: Sun, 28 Aug 2022 18:15:28 +1000 Subject: [PATCH 13/15] Updated Code to work for new Discordpy version --- README.md | 18 ++++++++++-------- cogless.py | 2 +- cogs/hello.py | 20 +++++++++----------- main.py | 15 ++++++++------- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 6701077..d327105 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ When a program is run, it may need information from the operating system to conf * Then we create an instance of [`Client`](https://discordpy.readthedocs.io/en/stable/api.html#discord.Client), which is out connection to discord, and run it ```python # ./main.py, after defining TOKEN - client = discord.Client() # creates the bot + client = discord.Client(intents=discord.Intents.all()) # creates the bot client.run(TOKEN) # runs the bot ``` @@ -198,7 +198,7 @@ When a program is run, it may need information from the operating system to conf ```python # ./main.py, in between defining the client and running it @client.event # 👈 this is a function decorator - def on_ready(): # 👈 on_ready() is a callback + async def on_ready(): # 👈 on_ready() is a callback # code in on_ready() will be run after the bot is done logging in and setting up ``` @@ -221,7 +221,7 @@ When a program is run, it may need information from the operating system to conf ```python # ./main.py, in between defining the client and running it @client.event - def on_message(msg): + async def on_message(msg): print(msg.content) ``` * The message being sent is stored in the `msg` variable, which is of the [Message](https://discordpy.readthedocs.io/en/stable/api.html#discord.Message) class. @@ -303,7 +303,7 @@ When a program is run, it may need information from the operating system to conf * Instead of a Client instance, we will create a Bot instance instead ```python # ./main.py, replace `client = discord.Client()` with... - client = commands.Bot(command_prefix = "!") # instead of a client, we create a Bot instance + client = commands.Bot(command_prefix = "!", intents = discord.Intents.all()) # instead of a client, we create a Bot instance ``` ### ✅ Task: Create a Hello Cog * create a folder called `cogs`, this is where you will store your cogs @@ -354,18 +354,20 @@ When a program is run, it may need information from the operating system to conf * this function will be called in `main.py` ```python # ./cogs/hello.py, outside the Hello class - def setup(bot): # 👈 a extension must have a setup function - bot.add_cog(Hello(bot)) # 👈 adding the cog + async def setup(bot): # 👈 a extension must have a setup function + await bot.add_cog(Hello(bot)) # 👈 adding the cog ``` ### ✅ Task: Refactor `main.py` to support Cogs (part 2) * Now, back in `main.py`, instead of having all of our bot's logic in the file, we have moved them into cogs that are located in the `./cogs` folder and now we should load up all of our cogs ```python - # ./main.py, after defining the client + # ./main.py, with imports + import asyncio + # 👇 Looks inside the /cogs/ folder and loads up all of our cogs for filename in os.listdir("./cogs"): if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) # calls the cog's `setup()` function + asyncio.run(client.load_extension("cogs." + filename[:-3])) # calls the cog's `setup()` function client.run(TOKEN) ``` diff --git a/cogless.py b/cogless.py index 04aed33..3edde1d 100644 --- a/cogless.py +++ b/cogless.py @@ -5,7 +5,7 @@ load_dotenv() TOKEN = os.getenv('TOKEN') -client = discord.Client() +client = discord.Client(intents=discord.Intents.all()) @client.event async def on_ready(): diff --git a/cogs/hello.py b/cogs/hello.py index 9cbe5d2..e113061 100644 --- a/cogs/hello.py +++ b/cogs/hello.py @@ -1,18 +1,16 @@ from discord.ext import commands -class Hello(commands.Cog): +class Hello(commands.Cog): # 👈 cog class called `Hello` def __init__(self, client): - self.client = client # defining bot as global var in class - - @commands.Cog.listener() # this is a decorator for events/listeners + self.client = client + + @commands.Cog.listener() # 👈 this is a decorator for events/listeners async def on_ready(self): print(f'We have logged in as {self.client.user}') - - + @commands.command() # this is for making a command - async def hello(self, ctx): - print(ctx) + async def hello(self, ctx): # 👈 called when messages contain `!hello` await ctx.send(f'Hi!') - -def setup(bot): # a extension must have a setup function - bot.add_cog(Hello(bot)) # adding a cog \ No newline at end of file + +async def setup(bot): # 👈 a extension must have a setup function + await bot.add_cog(Hello(bot)) # 👈 adding the cog \ No newline at end of file diff --git a/main.py b/main.py index 8d85721..424e4ce 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,17 @@ -# main.py -from discord.ext import commands import os from dotenv import load_dotenv +from discord.ext import commands +import discord +import asyncio load_dotenv() TOKEN = os.getenv('TOKEN') -client = commands.Bot(command_prefix = "!") +client = commands.Bot(command_prefix = "!", intents = discord.Intents.all()) # instead of a client, we create a Bot instance -# Looks inside the /cogs/ folder and loads up all of our cogs +# 👇 Looks inside the /cogs/ folder and loads up all of our cogs for filename in os.listdir("./cogs"): - if filename.endswith(".py"): - client.load_extension("cogs." + filename[:-3]) + if filename.endswith(".py"): + asyncio.run(client.load_extension("cogs." + filename[:-3])) # calls the cog's `setup()` function -client.run(TOKEN) +client.run(TOKEN) \ No newline at end of file From aa504406652a7b98de2853a5298eae67948bb3fa Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 29 Aug 2022 10:20:21 +1000 Subject: [PATCH 14/15] fix structure --- .gitignore | 3 +- README.md => w1/README.md | 0 .../images}/1_BIgXzxgolWVDBNq5F_eZpg.png | Bin {images => w1/images}/2022-07-27-20-48-43.png | Bin {images => w1/images}/2022-07-27-20-50-00.png | Bin {images => w1/images}/2022-07-27-20-51-23.png | Bin {images => w1/images}/2022-08-06-16-47-20.png | Bin {images => w1/images}/2022-08-06-16-52-41.png | Bin {images => w1/images}/2022-08-06-16-57-29.png | Bin {images => w1/images}/2022-08-06-16-57-47.png | Bin {images => w1/images}/2022-08-06-17-01-31.png | Bin {images => w1/images}/2022-08-06-17-02-00.png | Bin {images => w1/images}/2022-08-06-17-05-59.png | Bin {images => w1/images}/2022-08-06-17-11-00.png | Bin {images => w1/images}/2022-08-06-17-11-30.png | Bin {images => w1/images}/2022-08-06-17-17-22.png | Bin {images => w1/images}/2022-08-06-17-17-49.png | Bin w1/images/README.md | 102 ++++++++++++++++++ .../images}/deployment_add_buildpack.gif | Bin .../images}/vscode_access_terminal.gif | Bin .../vscode_create_main_and_env_files.gif | Bin .../python_cheatsheet.md | 0 python_setup.md => w1/python_setup.md | 0 23 files changed, 104 insertions(+), 1 deletion(-) rename README.md => w1/README.md (100%) rename {images => w1/images}/1_BIgXzxgolWVDBNq5F_eZpg.png (100%) rename {images => w1/images}/2022-07-27-20-48-43.png (100%) rename {images => w1/images}/2022-07-27-20-50-00.png (100%) rename {images => w1/images}/2022-07-27-20-51-23.png (100%) rename {images => w1/images}/2022-08-06-16-47-20.png (100%) rename {images => w1/images}/2022-08-06-16-52-41.png (100%) rename {images => w1/images}/2022-08-06-16-57-29.png (100%) rename {images => w1/images}/2022-08-06-16-57-47.png (100%) rename {images => w1/images}/2022-08-06-17-01-31.png (100%) rename {images => w1/images}/2022-08-06-17-02-00.png (100%) rename {images => w1/images}/2022-08-06-17-05-59.png (100%) rename {images => w1/images}/2022-08-06-17-11-00.png (100%) rename {images => w1/images}/2022-08-06-17-11-30.png (100%) rename {images => w1/images}/2022-08-06-17-17-22.png (100%) rename {images => w1/images}/2022-08-06-17-17-49.png (100%) create mode 100644 w1/images/README.md rename {images => w1/images}/deployment_add_buildpack.gif (100%) rename {images => w1/images}/vscode_access_terminal.gif (100%) rename {images => w1/images}/vscode_create_main_and_env_files.gif (100%) rename python_cheatsheet.md => w1/python_cheatsheet.md (100%) rename python_setup.md => w1/python_setup.md (100%) diff --git a/.gitignore b/.gitignore index 9680f40..cbd2202 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .idea/ -*.pyc \ No newline at end of file +*.pyc +/logs/ diff --git a/README.md b/w1/README.md similarity index 100% rename from README.md rename to w1/README.md diff --git a/images/1_BIgXzxgolWVDBNq5F_eZpg.png b/w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png similarity index 100% rename from images/1_BIgXzxgolWVDBNq5F_eZpg.png rename to w1/images/1_BIgXzxgolWVDBNq5F_eZpg.png diff --git a/images/2022-07-27-20-48-43.png b/w1/images/2022-07-27-20-48-43.png similarity index 100% rename from images/2022-07-27-20-48-43.png rename to w1/images/2022-07-27-20-48-43.png diff --git a/images/2022-07-27-20-50-00.png b/w1/images/2022-07-27-20-50-00.png similarity index 100% rename from images/2022-07-27-20-50-00.png rename to w1/images/2022-07-27-20-50-00.png diff --git a/images/2022-07-27-20-51-23.png b/w1/images/2022-07-27-20-51-23.png similarity index 100% rename from images/2022-07-27-20-51-23.png rename to w1/images/2022-07-27-20-51-23.png diff --git a/images/2022-08-06-16-47-20.png b/w1/images/2022-08-06-16-47-20.png similarity index 100% rename from images/2022-08-06-16-47-20.png rename to w1/images/2022-08-06-16-47-20.png diff --git a/images/2022-08-06-16-52-41.png b/w1/images/2022-08-06-16-52-41.png similarity index 100% rename from images/2022-08-06-16-52-41.png rename to w1/images/2022-08-06-16-52-41.png diff --git a/images/2022-08-06-16-57-29.png b/w1/images/2022-08-06-16-57-29.png similarity index 100% rename from images/2022-08-06-16-57-29.png rename to w1/images/2022-08-06-16-57-29.png diff --git a/images/2022-08-06-16-57-47.png b/w1/images/2022-08-06-16-57-47.png similarity index 100% rename from images/2022-08-06-16-57-47.png rename to w1/images/2022-08-06-16-57-47.png diff --git a/images/2022-08-06-17-01-31.png b/w1/images/2022-08-06-17-01-31.png similarity index 100% rename from images/2022-08-06-17-01-31.png rename to w1/images/2022-08-06-17-01-31.png diff --git a/images/2022-08-06-17-02-00.png b/w1/images/2022-08-06-17-02-00.png similarity index 100% rename from images/2022-08-06-17-02-00.png rename to w1/images/2022-08-06-17-02-00.png diff --git a/images/2022-08-06-17-05-59.png b/w1/images/2022-08-06-17-05-59.png similarity index 100% rename from images/2022-08-06-17-05-59.png rename to w1/images/2022-08-06-17-05-59.png diff --git a/images/2022-08-06-17-11-00.png b/w1/images/2022-08-06-17-11-00.png similarity index 100% rename from images/2022-08-06-17-11-00.png rename to w1/images/2022-08-06-17-11-00.png diff --git a/images/2022-08-06-17-11-30.png b/w1/images/2022-08-06-17-11-30.png similarity index 100% rename from images/2022-08-06-17-11-30.png rename to w1/images/2022-08-06-17-11-30.png diff --git a/images/2022-08-06-17-17-22.png b/w1/images/2022-08-06-17-17-22.png similarity index 100% rename from images/2022-08-06-17-17-22.png rename to w1/images/2022-08-06-17-17-22.png diff --git a/images/2022-08-06-17-17-49.png b/w1/images/2022-08-06-17-17-49.png similarity index 100% rename from images/2022-08-06-17-17-49.png rename to w1/images/2022-08-06-17-17-49.png diff --git a/w1/images/README.md b/w1/images/README.md new file mode 100644 index 0000000..e649712 --- /dev/null +++ b/w1/images/README.md @@ -0,0 +1,102 @@ +

DecodED 3

+ +> 👋 Hello and welcome to DecodED 3! Over the next 5 weeks, we will be creating a discord bot with [Discord.py](https://discordpy.readthedocs.io/en/stable/index.html) + +--- + +

Table of Contents

+
+Table of Contents + +- [About DecodED](#about-decoded) + - [Dates](#dates) + - [Zoom Link: https://unimelb.zoom.us/j/88528442813?pwd=WlYrQ3pHcm5xMXpGQkZZSllZZTNvQT09](#zoom-link-httpsunimelbzoomusj88528442813pwdwlyrq3phcm5xmxpgqkzzsllzztnvqt09) +- [About this repository](#about-this-repository) +- [Workshop 1 - Introduction to Discord.py](#workshop-1---introduction-to-discordpy) +- [Workshop 2 - Meme Bot](#workshop-2---meme-bot) +- [Workshop 3 - Music Bot](#workshop-3---music-bot) +- [Workshop 4 - Polling Bot](#workshop-4---polling-bot) +- [Workshop 5 - Tic Tac Toe](#workshop-5---tic-tac-toe) + +
+ +--- + +## About DecodED + +* DecodED is a series of workshops run every year by HackMelbourne to help students/teach students with introductory + programming background to create some cool things with code. In past years, we created full-stack websites and a Space Invaders game. This year, we will be creating a Discord bot! +* We are also trying a new teaching style this year so let us know if you like it! + +### Dates + +Lesson | Location | People | Date +| -- | -- | -- | -- | +Foundations (Basic) | Alan Gilbert 101 and Zoom | Xin Yu | 2pm, Wednesday 24th August +Meme bot (easy) | Zoom | Aly, Minh | Wednesday 31st August +Music bot | Alan Gilbert 121 (Theatre) and Zoom | Aryan, Ryan | Wednesday 7th September +Poll | Alan Gilbert G20 and Zoom | Jerry, Hoan | Wednesday 14th September +TicTacToe bot | Alan Gilbert 103 and Zoom | Warren, Daniel | 2pm, Wednesday 21st September + +### Zoom Link: https://unimelb.zoom.us/j/88528442813?pwd=WlYrQ3pHcm5xMXpGQkZZSllZZTNvQT09 +Password: 323737 + +## About this repository +* This repository contains: + * Participant Workbook + * each workbook is made up of multiple Parts which are then made up of multiple **✅ Tasks** that the participant + should complete, with **🧩 Hints** that help guide them in completing the Task, and **💡 Extensions** that are + optional tasks that can be done if the participant has completed the Task ahead of schedule + * Workshop Recordings + * Code for the Discord Bot being created + +## Workshop 1 - Introduction to Discord.py + +> About DecodED3, Covers basics of Discord.py, create a basic bot that says "Hello, World!", learn about the basic +> structure of Discord bots. +> +> Hosted by Xin Yu + +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/foundations-bot) +* [Workshop Recording] Coming Soon! + +## Workshop 2 - Meme Bot + +> Who doesn’t love a good meme? Join us and create the functionality to meme on your friends so hard that they wished +> they had their own meme bot. +> +> Hosted by Aly and Minh + +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/meme_bot) +* [Workshop Recording] + +## Workshop 3 - Music Bot + +> Ever since YouTube banned music bots, discord servers have been desperately lacking some tunes. Impress your friends +> by bringing them back by building your own bot with the ability to play music, plus additional music controls! Now you +> can resume your lo-fi beat study sessions with your mates! +> +> Hosted by Aryan and Ryan + +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/music-bot) +* [Workshop Recording] + +## Workshop 4 - Polling Bot + +> Caught up in your server arguing why Minecraft is (definitively) the best game? Why not run a poll on your server and +> prove your friends wrong? Learn to build your own polling bot to settle your arguments in style 😎 +> +> Jerry and Hoan + +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/poll_system) +* [Workshop Recording] + +## Workshop 5 - Tic Tac Toe + +> Fancy a game, but don’t want to leave your friends in discord? In this lesson, you’ll learn to implement a tic tac toe +> game within the Discord bot so you can vs your friends whenever you wish! +> +> Warren and Weng Jae (Daniel) + +* [📔Participant Workbook](https://github.com/HackMelbourne/Decoded-3/tree/tictactoe) +* [Workshop Recording] \ No newline at end of file diff --git a/images/deployment_add_buildpack.gif b/w1/images/deployment_add_buildpack.gif similarity index 100% rename from images/deployment_add_buildpack.gif rename to w1/images/deployment_add_buildpack.gif diff --git a/images/vscode_access_terminal.gif b/w1/images/vscode_access_terminal.gif similarity index 100% rename from images/vscode_access_terminal.gif rename to w1/images/vscode_access_terminal.gif diff --git a/images/vscode_create_main_and_env_files.gif b/w1/images/vscode_create_main_and_env_files.gif similarity index 100% rename from images/vscode_create_main_and_env_files.gif rename to w1/images/vscode_create_main_and_env_files.gif diff --git a/python_cheatsheet.md b/w1/python_cheatsheet.md similarity index 100% rename from python_cheatsheet.md rename to w1/python_cheatsheet.md diff --git a/python_setup.md b/w1/python_setup.md similarity index 100% rename from python_setup.md rename to w1/python_setup.md From 7c5a17ec969b2ebef21264cf50be0b08d80d873e Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 29 Aug 2022 10:23:14 +1000 Subject: [PATCH 15/15] fix readme --- w1/images/README.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename w1/images/README.md => README.md (100%) diff --git a/w1/images/README.md b/README.md similarity index 100% rename from w1/images/README.md rename to README.md