Wrote a Discord Bot in Python, and Loved It!

Shaffan
Shaffan’s Blog
Published in
5 min readSep 15, 2019
Image source: discord.py documentation

I wrote a Discord bot in Python that can give you either one Fact Sphere quote (from Portal 2) at a time, or can give you a whole bunch if you say !moarfacts and a number. (e.g. !moarfacts 5). It says some pretty hilarious things, and makes for a good laugh in the server! :)

I’m frankly really proud of myself for finally finishing something! Usually, I give myself a giant project that I end up not being able to finish it because there’s too much new stuff to learn and deal with. I did have to get some help from my friends on the Discord server I was building this thing for. In fact, I recently read that if you’re coding alone, you’re doing it wrong! That advice has stuck with me ever since.

In order to get the quotes I needed, I used a web-scraping library called BeautifulSoup4 (I realized later that pandas has easier to use webscraping functionality in certain cases). What I did was I ripped the html I needed from the Portal 2 wiki and Chrome Dev Tools, got all the <li> elements and their text using BS4, then I saved that parsed text to a file. I had to manually format stuff a little bit so all the facts would be on their own line. The actual code for the bot takes a random line from the file and reads it back to the user.

The code for the scraper and plain-text list of quotes is also included in the link at the end. I used this library to write the actual bot. I also used PRAW (Python Reddit API Wrapper) to allow the bot to quickly get posts from Reddit without the user having to leave Discord. You can tell it how many posts to get from a subreddit, and if you want posts that are sorted by hot, new, controversial, or other category.

Hope you all enjoy this and see it as inspiration to make your own goofy little thing!

Some of the code used for the actual bot (fact_bot.py)(note that the tokens and secrets are in a seperate .env file).

import os, random, sys, praw, discord
from dotenv import load_dotenv
from discord.ext import commands
from textwrap import wrap
load_dotenv()
token = os.getenv('DISCORD_TOKEN')
redditSecret = os.getenv('REDDIT_CLIENT_SECRET')
redditID = os.getenv('REDDIT_CLIENT_ID')
redditAgent = os.getenv('REDDIT_USER_AGENT')
redditPW = os.getenv('REDDIT_PW')
redditUName = os.getenv('REDDIT_USERNAME')
reddit = praw.Reddit(client_id=redditID,
client_secret=redditSecret,
user_agent=redditAgent,
username=redditUName,
password=redditPW)
# Discord client
client = commands.Bot(command_prefix='!') # !COMMAND_NAME args
# Bot events
# Function name must equal a valid event name
@client.event
async def on_ready():
# client is the bot
# client.user is the bot in the user object
# Docs here for client https://discordpy.readthedocs.io/en/async/api.html#client
print('Logged on as {0}!'.format(client.user.name))
# Bot commands
# Function name is the command name
@client.command(pass_context=True)
# c = context.*
async def ping(c):
await c.channel.send('Wadup')
@client.command(pass_context=True)
async def fact(c):
# c.message = message YOU sent (!fact)
# c.channel = the channel the message was sent in
facts = open("parsedfacts.txt", "r")
facts_list = [line.split() for line in facts]
fact_arr = random.choice(facts_list)
fact = ' '.join(fact_arr)
await c.channel.send(fact)@client.command(pass_context=True)
async def heart(c):
await c.channel.send("i :heart: u {0}! :D".format(c.message.author.name))
@client.command(pass_context=True)
async def moarfacts(c, times):
"""Repeats a message multiple times, then combines it into one message"""
msg = ""
facts = open("parsedfacts.txt", "r")
fact_list = []
for line in facts:
fact_list.append(line)
for i in range(int(times)):
msg += random.choice(fact_list) + '\n'
await c.channel.send(msg)@client.command(pass_context=True)
async def fetchposts(c, sub, sort, post_count):
msg = ""
# longMsg is an array that will hold a bunch of strings that represent a
# message that is broken up into pieces because it is over 2000 chars
# in length
longMsg = []
sub = reddit.subreddit(sub)
if 'new' in sort:
posts = sub.new(limit=int(post_count))
if 'hot' in sort:
posts = sub.hot(limit=int(post_count))
if 'top' in sort:
posts = sub.new(limit=int(post_count))
if 'contro' in sort:
posts = sub.controversial(limit=int(post_count))
if 'gilded' in sort:
posts = sub.gilded(limit=int(post_count))
for p in posts:
msg += "**" + p.title + "**" + '\n' "Body: " + p.selftext + '\n'
if len(msg) > 2000:
longMsg = wrap(msg, 2000)
for i in longMsg:
await c.channel.send(i)
else:
await c.channel.send(msg)

@client.command(pass_context=True)
async def onionhelp(c):
await c.channel.send("!fact gives you one quote of dubious accuracy from the Fact Sphere" + '\n' +
"!moarfacts x gives you (x) number of facts" + '\n' +
"!fetchposts will get you posts from reddit. Pass in the name of a subreddit" + '\n' +
"the way you want to get the posts[new,hot,top,contro,gilded], and how many posts you want to see" + '\n' +
"Please note that the reddit API only allows requests every 20 seconds or so" + '\n' +
"!heart will say 'i (heart) you username!'" + '\n' +
"!summon gets posts from the SummonSign subreddit to see if any PC Dark Souls 3 players need help")
@client.command(pass_context=True)
async def summon(c):
posts = reddit.subreddit("summonsign").new(limit=150)
msg = ""
for p in posts:
if "pc" in p.title.lower() and "ds3" in p.title.lower():
msg += "**" + p.title + "**" + '\n'
if len(msg) > 2000:
longMsg = wrap(msg, 2000)
for i in longMsg:
await c.channel.send(i)
else:
await c.channel.send(msg)

client.run(token)

Here is the raw HTML I got from the Wiki (factlist.txt):


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h3><span class="mw-headline" id="Fact_core">Fact core</span></h3>
<div style="font-style: italic; padding-left: 2em; margin-bottom: 0.5em;">See also:&#160;<a href="/wiki/Fact_Sphere"
title="Fact Sphere">Fact Sphere</a><i></div></i>
<ul>
<li><a href="https://i1.theportalwiki.net/img/2/27/Fact_core_attachedfact01.wav" class="internal"
title="Fact core attachedfact01.wav">"The situation you are in is very dangerous."</a>
</li>
<!--> and many more bullet points, but they all follow the <ul><li></ul> format.....
</ul>
</body>
</html>

Code used for scraping some of the raw HTML from the Portal wiki, after pasting the raw list elements in their own file:

import bs4, requestsfrom bs4 import BeautifulSoup# our html is taken from# https://theportalwiki.com/wiki/Core_voice_lines#Fact_corehtml = open("factlist.txt","r")# html data becomes "soup" that has been parsed by bs4
# that we can now interact with in Python, like so:
soup = BeautifulSoup(html, "html.parser")lis = soup.findAll('li')facts = [i.text.strip() for i in lis]# the output of this code is in parsedfacts.txt# I still had to hit backspace a few times to put each complege fact on one line

To see more stories like this when they come out, follow my publication on Medium and check the “Receive emails from this publication” box!

--

--

Shaffan
Shaffan’s Blog

BS in Geographical and Information Science, The Ohio State University. I like to learn about and write code, poetry, and other writing