Skip to content
Permalink
2f85e7af45
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
370 lines (321 sloc) 14.7 KB
#########################################
# Bartek's code for the chatbot project #
#########################################
###################################################################################
# Template used for this project: #
# NanoDano (2018) Make a Discord Bot with Python [online] available from #
# <https://www.devdungeon.com/content/make-discord-bot-python> [29 November 2018] #
###################################################################################
#############################################################
# Rapptz (2017) discord.py [online] available from #
# <https://github.com/Rapptz/discord.py> [29 November 2018] #
#############################################################
import discord # API used to communicate with Discord servers.
import asyncio # Required to use the "discord" library
############################################################
# NLTK Project (2018) Natural Language Toolkit [online] #
# available from <https://www.nltk.org> [29 November 2018] #
############################################################
import nltk # I only used this API to tokenize strings.
##########################################################
# elyase (2018) GeoText [online] available from #
# <https://github.com/elyase/geotext> [29 November 2018] #
##########################################################
from geotext import GeoText # Used to extract locations from words.
import json
from urllib.request import urlopen
import random
# bartek_keys.py file in the repository.
from bartek_keys import getWeatherKey, getBotKey
# Creating an instance of the discord client.
# (Taken from template by NanoDano (2018))
client = discord.Client()
###############################
# My own functions/coroutines #
###############################
async def chatbot_log(command, message):
"""Creates a message in the terminal, stating the details of the command used."""
msg_1 = str(message.timestamp) + ": " + message.author.nick + " used " + command
msg_2 = "Server: " + message.server.name
print("------")
print(msg_1)
print(msg_2)
print("------")
async def check_weather(message, message_tokens):
"""Checks the weather in a specified location."""
# My personal key for the OpenWeatherMap API
weather_key = getWeatherKey()
# Uses the GeoText library to generate a list...
# ... of locations in the message string.
##########################################################
# Example used for GeoText: #
# elyase (2018) GeoText [online] available from #
# <https://github.com/elyase/geotext> [29 November 2018] #
# Example can be found under "Usage". #
##########################################################
locations = GeoText(message.content)
# If GeoText returns an empty list, it assumes that...
# ...there is no city/town, so the bot asks for input.
if not locations.cities:
question = "Sure! What city(s) would you like me to check in?"
await client.send_message(message.channel, question)
locations = await client.wait_for_message(author=message.author, channel=message.channel)
locations_str = locations.content
locations = GeoText(locations_str)
while not locations.cities and not "cancel" in locations_str.lower():
question = "Sorry, I didn't quite get where you want me to check. Try again?"
await client.send_message(message.channel, question)
locations = await client.wait_for_message(author=message.author, channel=message.channel)
locations_str = locations.content
locations = GeoText(locations_str)
if "cancel" in locations_str.lower():
msg = "Sure thing!"
await client.send_message(message.channel, msg)
return
# Occasional bugs occur if you dont wait...
# ...based on my debugging. No idea why.
await asyncio.sleep(1)
final_msg = "Here you go, {0.author.mention}\n".format(message)
# Iterating through all the cities the user asked for.
for city in locations.cities:
# Formatting the city name so that it works...
# ...in hyperlinks, if it has a space in it.
if " " in city:
city_words = city.split(" ")
new_string = ""
for i in range(len(city_words)-1):
# %20 indicates a space in a hyperlink.
new_string += city_words[i] + "%20"
new_string += city_words[len(city_words)-1]
# Constructing a hyperlink to extract data from the...
# ...OpenWeatherMap API.
###################################################################
# Weather API used: #
# OpenWeather (n.d.) Current weather data [online] available from #
# <https://openweathermap.org/current> [29 November 2018] #
###################################################################
url_str = "https://api.openweathermap.org/data/2.5/weather?q="
url_str += new_string
url_str += "&appid=" + weather_key
else:
url_str = "https://api.openweathermap.org/data/2.5/weather?q="
url_str += city
url_str += "&appid=" + weather_key
print(url_str)
# Sometimes there's errors with city names.
try:
###################################################################
# Example used for opening JSON from URL: #
# Montmons (2017) How to get JSON from webpage into Python script #
# [online] available from <https://bit.ly/2DR5yZa> #
# [29 November 2018] #
# Found under "Python3 Example" in the top answer. #
###################################################################
# The OpenWeatherMap API uses JSON.
url = urlopen(url_str)
# Creating a list from the API JSON file.
weather_data = json.loads(url.read())
for i in range(len(message_tokens)):
message_tokens[i] = message_tokens[i].lower()
msg_to_print = []
# Prints appropriate data, depending on...
# ...the words the user used.
if "weather" in message_tokens:
description = weather_data["weather"][0]
temperature = weather_data["main"]
wind = weather_data["wind"]
msg_to_print = [
"Location: " + city,
"Weather: " + description["main"],
"Description: " + description["description"],
# I take away 273.15 from the temperatures, as OpenWeatherMap API...
# ...provides the data to us in K (Kelvin) instead of °C (Degrees Celsius).
"Current temperature: " + str(round(temperature["temp"] - 273.15,2)) + "°C",
"Today's high: " + str(round(temperature["temp_max"] - 273.15,2)) + "°C",
"Today's low: " + str(round(temperature["temp_min"] - 273.15,2)) + "°C"
]
elif "temperature" in message_tokens or "hot" in message_tokens or "cold" in message_tokens:
temperature = weather_data["main"]
msg_to_print = [
"Location: " + city,
"Current temperature: " + str(round(temperature["temp"] - 273.15,2)) + "°C",
"Today's high: " + str(round(temperature["temp_max"] - 273.15,2)) + "°C",
"Today's low: " + str(round(temperature["temp_min"] - 273.15,2)) + "°C"
]
elif "sunny" in message_tokens or "rain" in message_tokens or "rainy" in message_tokens or "rainfall" in message_tokens or "drizzle" in message_tokens:
description = weather_data["weather"][0]
msg_to_print = [
"Location: " + city,
"Weather: " + description["main"],
"Description: " + description["description"],
]
elif "wind" in message_tokens or "windy" in message_tokens:
wind = weather_data["wind"]
msg_to_print = [
"Location: " + city,
"Wind speed: " + str(wind["speed"]) + " m/s",
"Wind direction: " + str(wind["deg"]) + "°"
]
# In case the function is called when it shouldn't be.
else:
error_msg = "Sorry, something went wrong!"
await client.send_message(message.channel, error_msg)
return
final_msg += "```\n"
for to_print in msg_to_print:
final_msg += to_print + "\n"
final_msg += "```"
# This prints instead if something goes wrong.
except:
error_msg = "Whoops! Something went wrong. Did you write the location name correctly?"
await client.send_message(message.channel, error_msg)
await client.send_message(message.channel, final_msg)
await chatbot_log("Weather", message)
async def tell_fact(message):
"""Sends a random fact from a chosen (or random) category to the Discord channel."""
###############################################################################
# Facts used: #
# livin3 (n.d.) Top 155 Interesting and Weird Fun Facts [online] #
# available from <Top 155 Interesting and Weird Fun Facts> [29 November 2018] #
###############################################################################
print("Opening JSON...")
random_facts = open("facts.json", "r")
random_facts = json.loads(random_facts.read())
print("JSON opened!")
# change_string will determine whether the original message or the reply message...
# ... from the code below will be used in the final check for what sort of...
# ... fact to send back to the Discord channel.
change_string = False
user_message = ""
type_in_message = False
for fact_type in ("interesting", "fun", "funny", "animal", "random"):
if fact_type in message.content.lower():
type_in_message = True
break
if not type_in_message:
change_string = True
await client.send_message(message.channel, "Sure! What sort of fact? (interesting, fun, funny, animal or random)")
user_message = await client.wait_for_message(author=message.author, channel=message.channel)
user_message = nltk.word_tokenize(user_message.content.lower())
# Returns when "fact" is in the reply, so that this function call doesn't interfere...
# ... with the new call caused by the "on_message" coroutine below.
if "fact" in user_message:
return
for fact_type in ("interesting", "fun", "funny", "animal", "random"):
if fact_type in user_message:
type_in_message = True
break
while not type_in_message and not "cancel" in user_message:
await client.send_message(message.channel, "Sorry, what type?")
user_message = await client.wait_for_message(author=message.author, channel=message.channel)
user_message = nltk.word_tokenize(user_message.content.lower())
if "fact" in user_message:
return
for fact_type in ("interesting", "fun", "funny", "animal", "random"):
if fact_type in user_message:
type_in_message = True
# Gives the user validation that the operation was cancelled.
if "cancel" in user_message:
await client.send_message(message.channel, "Sure thing!")
return
# I do this so that I don't have to write out the below checks twice.
# ... Instead I just use the same variable to hold the string.
if not change_string:
user_message = nltk.word_tokenize(message.content.lower())
if "interesting" in user_message:
facts = random_facts["interesting"]
fact = facts[random.randint(0,len(facts)-1)]
msg_to_print = "Here you go, " + message.author.mention + "\n"
msg_to_print += fact
await client.send_message(message.channel, msg_to_print)
# "funny" needs to come before "fun", otherwise "funny" will...
# ... always be skipped.
elif "funny" in user_message:
facts = random_facts["funny"]
fact = facts[random.randint(0,len(facts)-1)]
msg_to_print = "Here you go, " + message.author.mention + "\n"
msg_to_print += fact
await client.send_message(message.channel, msg_to_print)
elif "fun" in user_message:
facts = random_facts["fun"]
fact = facts[random.randint(0,len(facts)-1)]
msg_to_print = "Here you go, " + message.author.mention + "\n"
msg_to_print += fact
await client.send_message(message.channel, msg_to_print)
elif "animal" in message.content:
facts = random_facts["animal"]
fact = facts[random.randint(0,len(facts)-1)]
msg_to_print = "Here you go, " + message.author.mention + "\n"
msg_to_print += fact
await client.send_message(message.channel, msg_to_print)
elif "random" in user_message:
types_of_facts = (
{"type": "interesting"},
{"type": "fun"},
{"type": "funny"},
{"type": "animal"}
)
# Picking a random fact type
random_type = types_of_facts[random.randint(0,len(types_of_facts)-1)]
print(random_type)
facts = random_facts[random_type["type"]]
fact = facts[random.randint(0,len(facts)-1)]
print(fact)
msg_to_print = "Here you go, " + message.author.mention + "\n"
msg_to_print += fact
await client.send_message(message.channel, msg_to_print)
#########################
# discord.py coroutines #
#########################
# discord.py coroutine triggered when the bot joins a server.
@client.event
async def on_server_join(server):
print("Joining " + server.name)
await client.create_channel(server, "chatbot")
msg = "Hello @everyone ! Please use this channel to use the Chatbot."
await asyncio.sleep(2)
chatbot_channel = discord.utils.get(server.channels, name="chatbot")
await client.send_message(chatbot_channel, msg)
print("Finished joining " + server.name)
print("----------")
# discord.py coroutine triggered when a message is sent to a channel...
# ... on a server the bot is in.
@client.event
async def on_message(message):
if message.author == client.user or message.channel.name != "chatbot":
return
# List of words that will trigger the bot to run my weather coroutine.
weather_words = (
"weather",
"temperature",
"hot",
"cold",
"sun",
"sunny",
"rainy",
"rain",
"rainfall",
"drizzle",
"wind",
"windy"
)
# Using the NLTK library to break the user's message...
# ... as this separates words and punctuation, which...
# ... is necessary for comparisons below to work properly.
message_tokens = nltk.word_tokenize(message.content)
for token in message_tokens:
if token.lower() in weather_words:
await check_weather(message, message_tokens)
break
elif "fact" in token.lower():
await tell_fact(message)
break
# discord.py coroutine that is triggered when the script is ran.
@client.event
async def on_ready():
print("Logged in as " + client.user.name + ", ID: " + client.user.id)
print("----------")
# Random numbers needed for the "tell_fact" coroutine.
random.seed()
# Starting the bot.
client.run(getBotKey())