6 Commits

Author SHA1 Message Date
7be6103a91 2024-11-06 16:54:56 +01:00
e71ba34371 2024-10-31 22:38:59 +01:00
2f4e606fbf 2024-10-30 15:31:46 +01:00
e955ef4e28 2024-10-29 15:20:28 +01:00
eb2960aa10 reworking a bunch of stuff 2.0 2024-10-28 14:38:41 +01:00
bc59bf9b05 reworking a bunch of stuff 2024-10-28 13:05:06 +01:00
156 changed files with 3147 additions and 1505 deletions

4
.gitignore vendored
View File

@ -150,9 +150,7 @@ static
.vscode/
token.txt
credentials.txt
options.txt
.env
gwendolyn/resources/star_wars/destinyPoints.txt
gwendolyn/resources/plex/
gwendolyn/resources/games/hilo/

View File

@ -10,7 +10,6 @@ Gwendolyn is a discord bot that I made. It does a bunch of stuff.
* Roll Star Wars RPG dice!
* Keep track of Star Wars RPG character sheets!
* Query Wolfram Alpha
* Invest fake money in the stock market
* Play trivia, connect 4, blackjack, hangman and hex
And much more!!! (not really. That's pretty much all it can do. See help files in resources directory for list of commands)

View File

@ -1,6 +1,5 @@
"""The main module for Gwendolyn."""
# pylint: disable=invalid-name
__all__ = ["funcs", "utils", "Gwendolyn"]
__all__ = ["Gwendolyn"]
from .gwendolyn_client import Gwendolyn

View File

@ -1,11 +1,14 @@
"""Exceptions for Gwendolyn"""
class GameNotInDatabase(Exception):
def __init__(self, game: str, channel: str):
self.message = f"There is no {game} game in channel {channel}"
class NoToken(Exception):
def __init__(self) -> None:
self.message = "No discord bot token has been set in the .env file"
super().__init__(self.message)
class InvalidInteraction(Exception):
def __init__(self, custom_id: str, decoded: str):
self.message = f"{custom_id = }, {decoded = }"
class CannotConnectToService(Exception):
def __init__(self, service: str) -> None:
self.message = f"Cannot connect to {service}"
super().__init__(self.message)
class ServiceNotProvided(Exception):
def __init__(self, service: str) -> None:
self.message = f"A {service} instance was not provided"
super().__init__(self.message)

View File

@ -0,0 +1,12 @@
from interactions import Extension, slash_command, SlashContext
from gwendolyn.utils import PARAMS as params
class BetterNeflixExtension(Extension):
"""Contains the Better Netflix commands."""
@slash_command(**params["better_netflix"]["movie"])
async def movie(self, ctx: SlashContext):
await self.bot.better_netflix.movie(ctx)
@slash_command(**params["better_netflix"]["show"])
async def show(self, ctx: SlashContext):
await self.bot.better_netflix.show(ctx)

21
gwendolyn/ext/events.py Normal file
View File

@ -0,0 +1,21 @@
from interactions import Extension, listen, Status, Activity, ActivityType
from interactions.api.events import Startup, Error
class EventExtension(Extension):
"""Listens to miscellaneous events."""
@listen(Startup)
async def startup(self, _):
"""Log and sets status when it logs in."""
name = self.bot.user.username
userid = str(self.bot.user.id)
logged_in_message = f"Logged in as {name}, {userid}"
self.bot.log(logged_in_message, level=25)
command_list = ",".join([str(i.name) for i in self.bot.application_commands])
self.bot.log(f"Commands: {command_list}")
activity = Activity("custom", ActivityType.CUSTOM, state="Type `/help` for commands")
await self.bot.change_presence(activity=activity, status=Status.ONLINE)
@listen(Error)
async def error(self, err: Error):
self.bot.log(f"Error at {err.source}", level=25)

56
gwendolyn/ext/misc.py Normal file
View File

@ -0,0 +1,56 @@
from interactions import Extension, slash_command, SlashContext, Status
from gwendolyn.utils import PARAMS as params
class MiscExtension(Extension):
"""Contains the miscellaneous commands."""
@slash_command(**params["misc"]["gen_name"])
async def gen_name(self, ctx: SlashContext):
await self.bot.other.generate_name(ctx)
@slash_command(**params["misc"]["hello"])
async def hello(self, ctx: SlashContext):
"""Greet the bot."""
await self.bot.other.hello_func(ctx)
@slash_command(**params["misc"]["help"])
async def help(self, ctx: SlashContext, command=""):
"""Get help for commands."""
await self.bot.other.help_func(ctx, command)
@slash_command(**params["misc"]["ping"])
async def ping(self, ctx: SlashContext):
"""Send the bot's latency."""
latency = self.bot.latency
if latency > 100:
await ctx.send("Cannot measure latency :(")
else:
await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms")
@slash_command(**params["misc"]["roll"])
async def roll(self, ctx: SlashContext, dice: str = "1d20"):
await self.bot.other.roll_dice(ctx, dice)
@slash_command(**params["misc"]["thank"])
async def thank(self, ctx: SlashContext):
"""Thank the bot."""
await ctx.send("You're welcome :blush:")
@slash_command(**params["misc"]["stop"])
async def stop(self, ctx: SlashContext):
if f"{ctx.author.id}" in self.bot.admins:
await ctx.send("Goodnight...")
await self.bot.change_presence(status=Status.INVISIBLE)
# self.bot.database_funcs.wipe_games()
self.bot.log("Logging out", level=25)
await self.bot.stop()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.bot.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
@slash_command(**params["misc"]["echo"])
async def echo(self, ctx: SlashContext, text: str):
await ctx.send(text)

View File

@ -1,11 +1,6 @@
"""A collection of all Gwendolyn functions."""
__all__ = ["Games", "Money", "LookupFuncs", "StarWars"]
from .games import Money, Games
from .lookup import LookupFuncs
__all__ = ["Other","BetterNetflix", "Sonarr", "Radarr", "TMDb", "QBittorrent"]
from .other import Other
from .star_wars_funcs import StarWars
from .better_netflix import Radarr, Sonarr, BetterNetflix, TMDb, QBittorrent

View File

@ -0,0 +1,5 @@
"""Better Netflix functions for Gwendolyn."""
__all__ = ["BetterNetflix", "Sonarr", "Radarr", "TMDb", "QBittorrent"]
from .better_netflix import BetterNetflix, Sonarr, Radarr, TMDb, QBittorrent

View File

@ -0,0 +1,480 @@
from requests import get
from random import choice
import io
from math import floor
from time import time
from PIL import Image
from interactions import SlashContext, Embed, EmbedFooter, EmbedAttachment, File
from gwendolyn.exceptions import CannotConnectToService, ServiceNotProvided
class Service():
def __init__(self, ip: str, port: str, api_key: str) -> None:
self.ip = ip
self.port = port
self.header = {
"accept": "application/json",
"X-Api-Key": api_key
}
def test(self):
return get(f"http://{self.ip}:{self.port}/api", headers=self.header).ok
class Radarr(Service):
def movies(self):
response = get(f"http://{self.ip}:{self.port}/api/v3/movie", headers=self.header)
if not response.ok:
return []
return [m for m in response.json() if m["hasFile"]]
class Sonarr(Service):
def shows(self):
response = get(f"http://{self.ip}:{self.port}/api/v3/series", headers=self.header)
if not response.ok:
return []
return response.json()
class QBittorrent(Service):
def __init__(self, ip: str, port: str, username: str, password: str) -> None:
self.ip = ip
self.port = port
response = get(f"http://{ip}:{port}/api/v2/auth/login?username={username}&password={password}")
self.logged_in = response.ok
self.cookie = {"SID": response.cookies.values()[0]}
def test(self):
return self.logged_in
def get_torrents(self):
response = get(
f"http://{self.ip}:{self.port}/api/v2/torrents/info",
cookies=self.cookie
)
return response.json()
class TMDb():
def __init__(self, api_token: str) -> None:
self.header = {
"accept": "application/json",
"Authorization": "Bearer "+api_token
}
def test(self):
return get(f"https://api.themoviedb.org/3/account", headers=self.header).ok
class BetterNetflix():
def __init__(self, bot, **kwargs) -> None:
self.bot = bot
services = ["radarr","sonarr","tmdb","qbittorrent"]
for service in services:
try:
setattr(self,service,kwargs[service])
except:
raise ServiceNotProvided(service)
if getattr(self,service).test():
self.bot.log(f"Connected to {service}")
else:
raise CannotConnectToService(service)
def _poster_color(self, poster_url: str):
response = get(poster_url, stream=True)
img = Image.open(io.BytesIO(response.content))
img.thumbnail((150, 100))
# Reduce colors (uses k-means internally)
paletted = img.convert('P', palette=Image.ADAPTIVE, colors=6)
buf = io.BytesIO()
img2 = paletted.convert("RGB")
img2.save("gwendolyn/resources/temp.jpg")
# Find the color that occurs most often
palette = paletted.getpalette()
color_counts = sorted(paletted.getcolors(), reverse=True)
palette_index = color_counts[0][1]
dominant_color = palette[palette_index*3:palette_index*3+3]
return dominant_color
async def movie(self, ctx: SlashContext):
msg = await ctx.send("Finding a random movie...")
movies = self.radarr.movies()
if len(movies) == 0:
await msg.edit(content="Unable to find any movies")
return
picked_movie = choice(movies)
description = ""
if "imdb" in picked_movie['ratings']:
description += f"<:imdb:1301506320603676782> {picked_movie['ratings']['imdb']['value']}"
description += "\u1CBC\u1CBC"
if "rottenTomatoes" in picked_movie['ratings']:
rt_value = picked_movie['ratings']['rottenTomatoes']['value']
rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>"
description += f"{rt_icon} {rt_value}%"
if description != "":
description += "\n\n"
year = EmbedFooter(str(picked_movie["year"])) if "year" in picked_movie else None
has_poster = len(picked_movie["images"]) > 0 and "remoteUrl" in picked_movie["images"][0]
poster = EmbedAttachment(picked_movie["images"][0]["remoteUrl"]) if has_poster else None
color = self._poster_color(picked_movie["images"][0]["remoteUrl"]) if has_poster else "#ff0000"
description += picked_movie["overview"]
embed = Embed(
picked_movie["title"],
description,
color,
thumbnail=poster,
footer=year
)
await msg.edit(content="",embed=embed)
async def show(self, ctx: SlashContext):
msg = await ctx.send("Finding a random show...")
shows = self.sonarr.shows()
if len(shows) == 0:
await msg.edit(content="Unable to find any shows")
return
picked_show = choice(shows)
description = ""
if "imdb" in picked_show['ratings']:
description += f"<:imdb:1301506320603676782> {picked_show['ratings']['imdb']['value']}"
description += "\u1CBC\u1CBC"
if "rottenTomatoes" in picked_show['ratings']:
rt_value = picked_show['ratings']['rottenTomatoes']['value']
rt_icon = "<:fresh:1301509701338660894>" if rt_value >= 60 else "<:rotten:1301509685907820607>"
description += f"{rt_icon} {rt_value}%"
if description != "":
description += "\n\n"
year = EmbedFooter(str(picked_show["year"])) if "year" in picked_show else None
images = {i["coverType"]:i for i in picked_show["images"]}
has_poster = "poster" in images and "remoteUrl" in images["poster"]
poster = EmbedAttachment(images["poster"]["remoteUrl"]) if has_poster else None
color = self._poster_color(images["poster"]["remoteUrl"]) if has_poster else "#ff0000"
description += picked_show["overview"]
embed = Embed(
picked_show["title"],
description,
color,
thumbnail=poster,
footer=year
)
await msg.edit(content="",embed=embed)
def _draw_torrent_list(self, title_width: int):
def ratio_to_bar(ratio):
progress_bar = "|"+(""*floor(download_ratio*20))
while len(progress_bar) < 21:
progress_bar += " "
progress_bar += "| "+str(floor(download_ratio*100))+"%"
while len(progress_bar) < 27:
progress_bar += " "
return progress_bar
message = [""]
all_downloaded = True
dm_section_title = "*Torrent Downloads*"
dm_section_title_line = "-"*((title_width-len(dm_section_title))//2)
message.append(
dm_section_title_line+dm_section_title+dm_section_title_line
)
torrent_list = self.qbittorrent.get_torrents()
if len(torrent_list) == 0:
message.append("No torrents currently downloading")
return message, all_downloaded
for torrent in torrent_list:
if torrent['category'] not in ["radarr", "tv-sonarr"]:
break
torrent_name = torrent["name"]
if len(torrent_name) > 30:
if torrent_name[26] == " ":
torrent_name = torrent_name[:26]+"...."
else:
torrent_name = torrent_name[:27]+"..."
while len(torrent_name) < 30:
torrent_name += " "
if torrent["size"] == 0:
download_ratio = 0
elif torrent["amount_left"] == 0:
download_ratio = 1
else:
download_ratio = min(
torrent["downloaded"]/torrent["size"],
1
)
progress_bar = ratio_to_bar(download_ratio)
eta_in_seconds = torrent["eta"]
if eta_in_seconds >= 8640000:
eta = ""
else:
eta = ""
if eta_in_seconds >= 86400:
eta += str(floor(eta_in_seconds/86400))+"d "
if eta_in_seconds >= 3600:
eta += str(floor((eta_in_seconds%86400)/3600))+"h "
if eta_in_seconds >= 60:
eta += str(floor((eta_in_seconds%3600)/60))+"m "
eta += str(eta_in_seconds%60)+"s"
torrent_info = f"{torrent_name} {progress_bar} "
torrent_info += f"(Eta: {eta})"
if torrent["state"] == "stalledDL":
torrent_info += " (Stalled)"
if not (download_ratio == 1 and
torrent["last_activity"] < time()-7200):
message.append(torrent_info)
if download_ratio < 1 and torrent["state"] != "stalledDL":
all_downloaded = False
return message, all_downloaded
async def _generate_download_list(self, show_dm, show_movies, show_shows,
episodes):
"""Generate a list of all torrents.
*Returns*
message_text: str
A formatted list of all torrents
all_downloaded: bool
Whether all torrents are downloaded
"""
self.bot.log("Generating torrent list")
title_width = 100
message = []
if show_dm:
m, all_downloaded = self._draw_torrent_list(title_width)
message += m
# if show_movies:
# message.append("")
# movies_section_title = "*Missing movies not downloading*"
# movies_section_line = (
# "-"*((title_width-len(movies_section_title))//2)
# )
# message.append(
# movies_section_line+movies_section_title+movies_section_line
# )
# movie_list = requests.get(
# self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
# ).json()
# print(
# self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
# )
# movie_queue = requests.get(
# self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"]
# ).json()
# movie_queue_ids = []
# for queue_item in movie_queue["records"]:
# movie_queue_ids.append(queue_item["movieId"])
# for movie in movie_list:
# if (not movie["hasFile"] and
# movie["id"] not in movie_queue_ids):
# movie_name = movie["title"]
# if len(movie_name) > 40:
# if movie_name[36] == " ":
# movie_name = movie_name[:36]+"...."
# else:
# movie_name = movie_name[:37]+"..."
# while len(movie_name) < 41:
# movie_name += " "
# if movie["monitored"]:
# movie_info = movie_name+"Could not find a torrent"
# else:
# movie_info = self.long_strings["No torrent"].format(
# movie_name
# )
# message.append(movie_info)
# if show_shows:
# message.append("")
# show_section_title = "*Missing shows not downloading*"
# show_section_line = "-"*((title_width-len(show_section_title))//2)
# message.append(
# show_section_line+show_section_title+show_section_line
# )
# show_list = requests.get(
# self.sonarr_url+"series?apiKey="+self.credentials["sonarr_key"]
# ).json()
# for show in show_list:
# if show["seasons"][0]["seasonNumber"] == 0:
# seasons = show["seasons"][1:]
# else:
# seasons = show["seasons"]
# if any(
# (
# i["statistics"]["episodeCount"] !=
# i["statistics"]["totalEpisodeCount"]
# ) for i in seasons):
# if all(
# i["statistics"]["episodeCount"] == 0 for i in seasons
# ):
# message.append(show["title"] + " (all episodes)")
# else:
# if episodes:
# missing_episodes = sum(
# (i["statistics"]["totalEpisodeCount"] -
# i["statistics"]["episodeCount"])
# for i in seasons)
# message.append(
# f"{show['title']} ({missing_episodes} episodes)"
# )
message.append("-"*title_width)
message_text = "```"+"\n".join(message[1:])+"```"
if message_text == "``````":
message_text = self.long_strings["No torrents downloading"]
return message_text, all_downloaded
# async def downloading(self, ctx, content):
# """Send message with list of all downloading torrents."""
# async def send_long_message(ctx,message_text):
# if len(message_text) <= 1994:
# await ctx.send("```"+message_text+"```")
# else:
# cut_off_index = message_text[:1994].rfind("\n")
# await ctx.send("```"+message_text[:cut_off_index]+"```")
# await send_long_message(ctx,message_text[cut_off_index+1:])
# await self.bot.defer(ctx)
# # showDM, showMovies, showShows, episodes
# parameters = [False, False, False, False]
# show_dm_args = ["d", "dm", "downloading", "downloadmanager"]
# show_movies_args = ["m", "movies"]
# show_shows_args = ["s", "shows", "series"]
# show_episode_args = ["e", "episodes"]
# arg_list = [
# show_dm_args, show_movies_args, show_shows_args, show_episode_args
# ]
# input_args = []
# valid_arguments = True
# while content != "" and valid_arguments:
# if content[0] == " ":
# content = content[1:]
# elif content[0] == "-":
# if content[1] == "-":
# arg_start = 2
# if " " in content:
# arg_stop = content.find(" ")
# else:
# arg_stop = None
# else:
# arg_start = 1
# arg_stop = 2
# input_args.append(content[arg_start:arg_stop])
# if arg_stop is None:
# content = ""
# else:
# content = content[arg_stop:]
# else:
# valid_arguments = False
# if valid_arguments:
# for arg_index, arg_aliases in enumerate(arg_list):
# arg_in_input = [i in input_args for i in arg_aliases]
# if any(arg_in_input):
# input_args.remove(arg_aliases[arg_in_input.index(True)])
# parameters[arg_index] = True
# if len(input_args) != 0 or (not parameters[2] and parameters[3]):
# valid_arguments = False
# show_anything = any(i for i in parameters)
# if not (valid_arguments and show_anything):
# await ctx.send(self.long_strings["Invalid parameters"])
# else:
# message_text, all_downloaded = await self.__generate_download_list(
# *parameters
# )
# if not message_text.startswith("```"):
# await ctx.send(message_text)
# elif len(message_text) > 2000:
# message_text = message_text[3:-3]
# await send_long_message(ctx,message_text)
# elif all_downloaded:
# await ctx.send(message_text)
# else:
# updates_left = 60
# message_text = self.long_strings["Update"].format(
# message_text[:-3], ceil(updates_left/6)
# )
# old_message = await ctx.send(message_text)
# while ((not all_downloaded) and updates_left > 0):
# await asyncio.sleep(10)
# updates_left -= 1
# message_text, all_downloaded = await (
# self.__generate_download_list(*parameters)
# )
# message_text = self.long_strings["Update"].format(
# message_text[:-3],
# ceil(updates_left/6)
# )
# await old_message.edit(content = message_text)
# message_text, all_downloaded = await (
# self.__generate_download_list(*parameters)
# )
# if message_text.startswith("```"):
# if all_downloaded:
# self.bot.log("All torrents are downloaded")
# else:
# message_text = self.long_strings["No updates"].format(
# message_text[:-3]
# )
# self.bot.log("The message updated 20 times")
# await old_message.edit(content = message_text)

View File

@ -1,286 +0,0 @@
"""
Contains functions relating to invest commands.
*Classes*
---------
Invest
Contains all the code for the invest commands.
"""
import discord # Used for embeds
from discord_slash.context import SlashContext # Used for type hints
class Invest():
"""
Contains all the invest functions.
*Methods*
---------
getPrice(symbol: str) -> int
getPortfolio(user: str) -> str
buyStock(user: str, stock: str: buyAmount: int) -> str
sellStock(user: str, stock: str, buyAmount: int) -> str
parseInvest(ctx: discord_slash.context.SlashContext,
parameters: str)
"""
def __init__(self, bot):
"""Initialize the class."""
self.bot = bot
def getPrice(self, symbol: str):
"""
Get the price of a stock.
*Parameters*
------------
symbol: str
The symbol of the stock to get the price of.
*Returns*
---------
price: int
The price of the stock.
"""
res = self.bot.finnhub_client.quote(symbol.upper())
if res == {}:
return 0
else:
return int(res["c"] * 100)
def getPortfolio(self, user: str):
"""
Get the stock portfolio of a user.
*Parameters*
------------
user: str
The id of the user to get the portfolio of.
*Returns*
---------
portfolio: str
The portfolio.
"""
investments_database = self.bot.database["investments"]
userInvestments = investments_database.find_one({"_id": user})
user_name = self.bot.database_funcs.get_name(user)
if userInvestments in [None, {}]:
return f"{user_name} does not have a stock portfolio."
else:
portfolio = f"**Stock portfolio for {user_name}**"
for key, value in list(userInvestments["investments"].items()):
purchaseValue = value["purchased for"]
stockPrice = self.getPrice(key)
valueAtPurchase = value["value at purchase"]
purchasedStock = value["purchased"]
valueChange = (stockPrice / valueAtPurchase)
currentValue = int(valueChange * purchasedStock)
portfolio += f"\n**{key}**: ___{currentValue} GwendoBucks___"
if purchaseValue != "?":
portfolio += f" (purchased for {purchaseValue})"
return portfolio
def buyStock(self, user: str, stock: str, buyAmount: int):
"""
Buy an amount of a specific stock.
*Paramaters*
------------
user: str
The id of the user buying.
stock: str
The symbol of the stock to buy.
buyAmount: int
The amount of GwendoBucks to use to buy the stock.
*Returns*
---------
send_message: str
The message to return to the user.
"""
if buyAmount < 100:
return "You cannot buy stocks for less than 100 GwendoBucks"
elif self.bot.money.checkBalance(user) < buyAmount:
return "You don't have enough money for that"
elif self.getPrice(stock) <= 0:
return f"{stock} is not traded on the american market."
else:
investments_database = self.bot.database["investments"]
stockPrice = self.getPrice(stock)
userInvestments = investments_database.find_one({"_id": user})
self.bot.money.addMoney(user, -1*buyAmount)
stock = stock.upper()
if userInvestments is not None:
userInvestments = userInvestments["investments"]
if stock in userInvestments:
value = userInvestments[stock]
valueChange = (stockPrice / value["value at purchase"])
currentValue = int(valueChange * value["purchased"])
newAmount = currentValue + buyAmount
value_path = f"investments.{stock}.value at purchase"
updater = {"$set": {value_path: stockPrice}}
investments_database.update_one({"_id": user}, updater)
purchased_path = f"investments.{stock}.purchased"
updater = {"$set": {purchased_path: newAmount}}
investments_database.update_one({"_id": user}, updater)
if value["purchased for"] != "?":
purchasedFor_path = f"investments.{stock}.purchased for"
updater = {"$set": {purchasedFor_path: buyAmount}}
investments_database.update_one({"_id": user}, updater)
else:
updater = {
"$set": {
"investments.{stock}": {
"purchased": buyAmount,
"value at purchase": stockPrice,
"purchased for": buyAmount
}
}
}
investments_database.update_one({"_id": user}, updater)
else:
new_user = {
"_id": user,
"investments": {
stock: {
"purchased": buyAmount,
"value at purchase": stockPrice,
"purchased for": buyAmount
}
}
}
investments_database.insert_one(new_user)
user_name = self.bot.database_funcs.get_name(user)
send_message = "{} bought {} GwendoBucks worth of {} stock"
send_message = send_message.format(user_name, buyAmount, stock)
return send_message
def sellStock(self, user: str, stock: str, sellAmount: int):
"""
Sell an amount of a specific stock.
*Paramaters*
------------
user: str
The id of the user selling.
stock: str
The symbol of the stock to sell.
buyAmount: int
The amount of GwendoBucks to sell for.
*Returns*
---------
send_message: str
The message to return to the user.
"""
if sellAmount <= 0:
return "no"
else:
investments_database = self.bot.database["investments"]
user_data = investments_database.find_one({"_id": user})
userInvestments = user_data["investments"]
stock = stock.upper()
if userInvestments is not None and stock in userInvestments:
value = userInvestments[stock]
stockPrice = self.getPrice(stock)
priceChange = (stockPrice / value["value at purchase"])
purchasedAmount = int(priceChange * value["purchased"])
purchased_path = f"investments.{stock}.purchased"
updater = {"$set": {purchased_path: purchasedAmount}}
investments_database.update_one({"_id": user}, updater)
valueAtPurchase_path = f"investments.{stock}.value at purchase"
updater = {"$set": {valueAtPurchase_path: stockPrice}}
investments_database.update_one({"_id": user}, updater)
if value["purchased"] >= sellAmount:
self.bot.money.addMoney(user, sellAmount)
if sellAmount < value["purchased"]:
purchased_path = f"investments.{stock}.purchased"
updater = {"$inc": {purchased_path: -sellAmount}}
investments_database.update_one({"_id": user}, updater)
purchasedFor_path = f"investments.{stock}.purchased for"
updater = {"$set": {purchasedFor_path: "?"}}
investments_database.update_one({"_id": user}, updater)
else:
updater = {"$unset": {f"investments.{stock}": ""}}
investments_database.update_one({"_id": user}, updater)
user_name = self.bot.database_funcs.get_name(user)
send_message = "{} sold {} GwendoBucks worth of {} stock"
return send_message.format(user_name, sellAmount, stock)
else:
return f"You don't have enough {stock} stocks to do that"
else:
return f"You don't have any {stock} stock"
async def parseInvest(self, ctx: SlashContext, parameters: str):
"""
Parse an invest command. TO BE DELETED.
*Parameters*
------------
ctx: discord_slash.context.SlashContext
The context of the slash command.
parameters: str
The parameters of the command.
"""
await self.bot.defer(ctx)
user = f"#{ctx.author.id}"
if parameters.startswith("check"):
commands = parameters.split(" ")
if len(commands) == 1:
response = self.getPortfolio(user)
else:
price = self.getPrice(commands[1])
if price == 0:
response = "{} is not traded on the american market."
response = response.format(commands[0].upper())
else:
price = f"{price:,}".replace(",", ".")
response = self.bot.long_strings["Stock value"]
response = response.format(commands[1].upper(), price)
elif parameters.startswith("buy"):
commands = parameters.split(" ")
if len(commands) == 3:
response = self.buyStock(user, commands[1], int(commands[2]))
else:
response = self.bot.long_strings["Stock parameters"]
elif parameters.startswith("sell"):
commands = parameters.split(" ")
if len(commands) == 3:
response = self.sellStock(user, commands[1], int(commands[2]))
else:
response = self.bot.long_strings["Stock parameters"]
else:
response = "Incorrect parameters"
if response.startswith("**"):
responses = response.split("\n")
text = "\n".join(responses[1:])
embedParams = {
"title": responses[0],
"description": text,
"colour": 0x00FF00
}
em = discord.Embed(**embedParams)
await ctx.send(embed=em)
else:
await ctx.send(response)

View File

@ -0,0 +1,44 @@
import random
class NameGenerator():
def __init__(self) -> None:
with open("gwendolyn/resources/names.txt", "r", encoding="UTF-8") as file_pointer:
self._names = file_pointer.read().lower().split("\n")
self._gen_letter_dict()
def _gen_letter_dict(self):
list_of_names = []
for name in self._names:
if (name != ""):
list_of_names.append("__" + name + "__")
dict_of_names = {}
for name in list_of_names:
for i in range(len(name)-3):
combination = name[i:i+2]
if combination not in dict_of_names:
dict_of_names[combination] = []
dict_of_names[combination].append(name[i+2])
self._letter_dict = dict_of_names
def generate(self):
combination = "__"
next_letter = ""
result = ""
while True:
number_of_letters = len(self._letter_dict[combination])
index = random.randint(0,number_of_letters - 1)
next_letter = self._letter_dict[combination][index]
if next_letter == "_":
break
else:
result = result + next_letter
combination = combination[1] + next_letter
return result.title()
if __name__ == "__main__":
print(NameGenerator().generate())

View File

@ -1,69 +1,22 @@
import random # Used in movie_func
import datetime # Used in hello_func
import urllib # Used in image_func
import ast
import imdb # Used in movie_func
import discord # Used in movie_func
import lxml # Used in image_func
import fandom # Used in find_wiki_page
import d20 # Used in roll_dice
from interactions import SlashContext, Embed, SlashCommand
from .plex import Plex
from .nerd_shit import NerdShit
from .generators import Generators
from .name_generator import NameGenerator
fandom.set_lang("da")
fandom.set_wiki("senkulpa")
from .roll import DieRoller
class MyStringifier(d20.MarkdownStringifier):
def _str_expression(self, node):
if node.comment is None:
result_text = "Result"
else:
result_text = node.comment.capitalize()
return f"**{result_text}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}"
def gen_help_text(commands: list[SlashCommand]):
return '\n'.join([f"`/{i.name}`\t{i.description}" for i in commands])
class Other():
def __init__(self, bot):
self.bot = bot
self.plex = Plex(self.bot)
self.nerd_shit = NerdShit(self.bot)
self.generators = Generators(self.bot)
# Picks a random movie and returns information about it
async def movie_func(self, ctx):
await self.bot.defer(ctx)
self.bot.log("Creating IMDb object")
imdb_client = imdb.IMDb()
self.bot.log("Picking a movie")
with open("gwendolyn/resources/movies.txt", "r") as file_pointer:
movie_list = file_pointer.read().split("\n")
movie_name = random.choice(movie_list)
self.bot.log(f"Searching for {movie_name}")
search_result = imdb_client.search_movie(movie_name)
self.bot.log("Getting the data")
movie = search_result[0]
imdb_client.update(movie)
self.bot.log("Successfully ran /movie")
title = movie["title"]
plot = movie['plot'][0].split("::")[0]
cover = movie['cover url'].replace("150","600").replace("101","404")
cast = ", ".join([i["name"] for i in movie['cast'][:5]])
embed = discord.Embed(title=title, description=plot, color=0x24ec19)
embed.set_thumbnail(url=cover)
embed.add_field(name="Cast", value=cast,inline = True)
await ctx.send(embed = embed)
self._name_generator = NameGenerator()
self._roller = DieRoller()
# Responds with a greeting of a time-appropriate maner
async def hello_func(self, ctx):
async def hello_func(self, ctx: SlashContext):
def time_in_range(start, end, i):
# Return true if i is in the range [start, end]
if start <= end:
@ -86,107 +39,27 @@ class Other():
await ctx.send(send_message)
# Finds a random picture online
async def image_func(self, ctx):
# Picks a type of camera, which decides the naming scheme
cams = ("one","two","three","four")
cam = random.choice(cams)
self.bot.log("Chose cam type "+cam)
if cam == "one":
search = "img_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
elif cam == "two":
year = str(random.randint(2012,2016))
month = str(random.randint(1,12)).zfill(2)
day = str(random.randint(1,29)).zfill(2)
search = f"IMG_{year}{month}{day}"
elif cam == "three":
search = f"IMAG_{str(random.randint(1,500)).zfill(4)}"
elif cam == "four":
search = "DSC_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
self.bot.log("Searching for "+search)
# Searches for the image and reads the resulting web page
page = urllib.request.urlopen("https://www.bing.com/images/search?q="+search+"&safesearch=off")
read = page.read()
tree = lxml.etree.HTML(read)
images = tree.xpath('//a[@class = "iusc"]/@m')
if len(images) == 0:
await ctx.send("Found no images")
else:
# Picks an image
number = random.randint(1,len(images))-1
image = ast.literal_eval(str(images[number]))
image_url = image["murl"]
self.bot.log("Picked image number "+str(number))
# Returns the image
self.bot.log("Successfully returned an image")
await ctx.send(image_url)
# Finds a page from the Senkulpa Wikia
async def find_wiki_page(self, ctx, search : str):
await self.bot.defer(ctx)
found_page = False
if search != "":
self.bot.log("Trying to find wiki page for "+search)
search_results = fandom.search(search)
if len(search_results) > 0:
found_page = True
search_result = search_results[0]
else:
self.bot.log("Couldn't find the page")
await ctx.send("Couldn't find page")
else:
found_page = True
self.bot.log("Searching for a random page")
search_result = fandom.random()
if found_page:
self.bot.log(f"Found page \"{search_result[0]}\"")
page = fandom.page(pageid = search_result[1])
content = page.summary
images = page.images
if len(images) > 0:
image = images[0]
else:
image = ""
self.bot.log("Sending the embedded message",str(ctx.channel_id))
content += f"\n[Læs mere]({page.url})"
embed = discord.Embed(title = page.title, description = content, colour=0xDEADBF)
if image != "":
embed.set_thumbnail(url=image)
await ctx.send(embed = embed)
async def roll_dice(self, ctx, roll_string):
user = ctx.author.display_name
while len(roll_string) > 1 and roll_string[0] == " ":
roll_string = roll_string[1:]
roll = d20.roll(roll_string, allow_comments=True, stringifier=MyStringifier())
await ctx.send(f"{user} :game_die:\n{roll}")
async def help_func(self, ctx, command):
async def help_func(self, ctx: SlashContext, command: str):
if command == "":
with open("gwendolyn/resources/help/help.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = "Help", description = text,colour = 0x59f442)
text = gen_help_text([i for i in self.bot.application_commands if isinstance(i,SlashCommand)])
embed = Embed(title = "Help", description = text,color = 0x59f442)
await ctx.send(embed = embed)
else:
self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id))
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = command.capitalize(), description = text,colour = 0x59f442)
await ctx.send(embed = embed)
try:
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = Embed(title = command.capitalize(), description = text,color = 0x59f442)
await ctx.send(embed = embed)
except:
await ctx.send(f"Could not find a help file for the command '/{command}'")
async def generate_name(self, ctx: SlashContext):
await ctx.send(self._name_generator.generate())
async def roll_dice(self, ctx: SlashContext, dice: str):
try:
roll = self._roller.roll(dice)
await ctx.send(f":game_die:Rolling dice `{dice}`\n{roll}")
except:
await ctx.send("There was an error in the rolling")

View File

@ -0,0 +1 @@
from .roll import DieRoller

View File

@ -0,0 +1,737 @@
from __future__ import annotations
from random import randint
from rply.token import BaseBox
def show_exp(exp, vtable):
r = exp.show(vtable)
if type(exp) not in [ExpInt,ExpRoll,ExpVar]:
r = f"({r})"
return r
class Exp(BaseBox):
def eval(self, vtable, *args):
return self._eval(vtable.copy(), *args)
def show(self, vtable, *args):
return self._show(vtable.copy(), *args)
def _eval(self, _):
return None
def _show(self, _):
return ""
def includes(self, _):
return False
def __repr__(self) -> str:
return "exp()"
def __eq__(self, other: Exp) -> bool:
return False
class ExpInt(Exp):
def __init__(self, value: int):
self.value = value
def _eval(self, _):
return self.value
def _show(self, _):
return str(self.value)
def includes(self, _):
return False
def __repr__(self) -> str:
return f"exp_int({self.value})"
def __eq__(self, other: Exp) -> bool:
return isinstance(other, ExpInt) and self.value == other.value
class ExpMin(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
return max(r1, r2)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}min{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_min({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpMin) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpMax(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
return min(r1, r2)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}max{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_max({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpMax) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpBinop(Exp):
def __init__(self, op: str, exp1: Exp, exp2: Exp):
self.op = op
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return None
if self.op == "+":
return r1+r2
elif self.op == "-":
return r1-r2
elif self.op == "*":
return r1*r2
elif self.op == "/":
return r1//r2
else:
raise Exception(f"Unknown binop {self.op}")
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
return f"{r1}{self.op}{r2}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_binop({self.op}, {self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpBinop) and
self.op == other.op and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpNeg(Exp):
def __init__(self, exp: Exp):
self.exp = exp
def _eval(self, vtable):
r = self.exp.eval(vtable)
if not isinstance(r,int):
return None
return -r
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"-{r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"exp_neg({self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpNeg) and
self.exp == other.exp
)
class ExpLet(Exp):
def __init__(self, var: str, exp1: Exp, exp2: Exp):
self.var = var
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
vtable[self.var] = r1
return self.exp2.eval(vtable)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
vtable[self.var] = self.exp1.eval(vtable)
r2 = show_exp(self.exp2, vtable)
if self.exp2.includes(self.var):
return f"let {self.var}={r1} in {r2}"
else:
return r2
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_let({self.var}, {self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpLet) and
self.var == other.var and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpIf(Exp):
def __init__(self, exp1: Exp, exp2: Exp, exp3: Exp):
self.exp1 = exp1
self.exp2 = exp2
self.exp3 = exp3
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
if r1 > 0:
return self.exp2.eval(vtable)
else:
return self.exp3.eval(vtable)
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = show_exp(self.exp2, vtable)
r3 = self.exp3.show(vtable)
return f"if {r1} then {r2} else {r3}"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var) or self.exp3.includes(var)
def __repr__(self) -> str:
return f"exp_if({self.exp1}, {self.exp2}, {self.exp3})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpIf) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2 and
self.exp3 == other.exp3
)
class ExpLambda(Exp):
def __init__(self, var: str, exp: Exp):
self.var = var
self.exp = exp
def _eval(self, _):
return (self.exp, self.var)
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"\\{self.var} -> {r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"exp_lambda({self.var}, {self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpLambda) and
self.var == other.var and
self.exp == other.exp
)
class ExpApply(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
def _eval(self, vtable):
r1 = self.exp1.eval(vtable)
if isinstance(r1, tuple):
r2 = self.exp2.eval(vtable)
vtable[r1[1]] = r2
return r1[0].eval(vtable)
else:
return None
def _show(self, vtable):
r1 = show_exp(self.exp1, vtable)
r2 = self.exp2.show(vtable)
return f"{r1}({r2})"
def includes(self, var: str):
return self.exp1.includes(var) or self.exp2.includes(var)
def __repr__(self) -> str:
return f"exp_apply({self.exp1}, {self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpApply) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class ExpVar(Exp):
def __init__(self, var: str):
self.var = var
def _eval(self, vtable):
return vtable[self.var] if self.var in vtable else None
def _show(self, vtable):
return self.var
def includes(self, var: str):
return var == self.var
def __repr__(self) -> str:
return f"exp_var({self.var})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpVar) and
self.var == other.var
)
class ComparePoint(Exp):
def __init__(self, comp_op: str, exp: Exp) -> None:
self.comp_op = comp_op
self.exp = exp
def _eval(self, vtable, val: int):
r = self.exp.eval(vtable)
if not isinstance(r,int):
return None
if self.comp_op == "=":
return 1 if val == r else 0
if self.comp_op == "<":
return 1 if val < r else 0
if self.comp_op == "<=":
return 1 if val <= r else 0
if self.comp_op == ">":
return 1 if val <= r else 0
if self.comp_op == ">=":
return 1 if val <= r else 0
else:
raise Exception(f"Unknown binop {self.op}")
def _show(self, vtable):
r = show_exp(self.exp, vtable)
return f"{self.comp_op}{r}"
def includes(self, var: str):
return self.exp.includes(var)
def __repr__(self) -> str:
return f"comp({self.comp_op},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ComparePoint) and
self.comp_op == other.comp_op and
self.exp == other.exp
)
class ExpTest(Exp):
def __init__(self, exp: Exp, comp: ComparePoint) -> None:
self.exp = exp
self.comp = comp
def _eval(self, vtable):
r = self.exp.eval(vtable)
return self.comp.eval(vtable, r)
def _show(self, vtable):
r = show_exp(self.exp, vtable)
c = self.comp.show(vtable)
return f"{r}{c}"
def includes(self, var: str):
return self.exp.includes(var) or self.comp.includes(var)
def __repr__(self) -> str:
return f"test({self.exp},{self.comp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpTest) and
self.exp == other.exp and
self.comp == other.comp
)
class ExpRoll(Exp):
def __init__(self, roll: Roll):
self.roll = roll
def _eval(self, vtable):
return sum(self.roll.eval(vtable))
def _show(self, vtable):
return f"[{','.join(self.roll.show(vtable))}]"
def includes(self, _):
return False
def __repr__(self) -> str:
return f"sum({self.roll})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, ExpRoll) and
self.roll == other.roll
)
class Roll(Exp):
def __init__(self, exp1: Exp, exp2: Exp):
self.exp1 = exp1
self.exp2 = exp2
self.result = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.exp1.eval(vtable)
r2 = self.exp2.eval(vtable)
if not (isinstance(r1,int) and isinstance(r2,int)):
return []
self.die_type = r2
self.result = [randint(1,r2) for _ in range(r1)]
self.show_list = [str(i) for i in self.result]
return self.result
def _show(self, vtable):
self.eval(vtable)
return self.show_list
@property
def die(self) -> int:
if hasattr(self, "die_type"):
return self.die_type
elif hasattr(self, "roll"):
return self.roll.die_type
else:
return 0
def __repr__(self) -> str:
return f"roll({self.exp1},{self.exp2})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, Roll) and
self.exp1 == other.exp1 and
self.exp2 == other.exp2
)
class RollKeepHighest(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
max_indices = []
for i, n in enumerate(r1):
if len(max_indices) < r2:
max_indices.append(i)
elif not all([n <= r1[j] for j in max_indices]):
max_indices.remove(min(max_indices,key=lambda x: r1[x]))
max_indices.append(i)
self.result = [r1[i] for i in max_indices]
self.show_list = [str(n) if i in max_indices else f"~~{n}~~" for i, n in enumerate(r1)]
return self.result
def __repr__(self) -> str:
return f"kep_highest({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollKeepHighest) and
self.roll == other.roll and
self.exp == other.exp
)
class RollKeepLowest(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
min_indices = []
for i, n in enumerate(r1):
if len(min_indices) < r2:
min_indices.append(i)
elif not all([n >= r1[j] for j in min_indices]):
min_indices.remove(max(min_indices,key=lambda x: r1[x]))
min_indices.append(i)
self.result = [r1[i] for i in min_indices]
self.show_list = [str(n) if i in min_indices else f"~~{n}~~" for i, n in enumerate(r1)]
return self.result
def __repr__(self) -> str:
return f"kep_lowest({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollKeepLowest) and
self.roll == other.roll and
self.exp == other.exp
)
class RollMin(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
self.show_list = []
for i in range(len(r1)):
if r1[i] < r2:
r1[i] = r2
self.show_list.append(f"{r2}^")
else:
self.show_list.append(str(r1[i]))
self.result = r1
return self.result
def __repr__(self) -> str:
return f"min({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMin) and
self.roll == other.roll and
self.exp == other.exp
)
class RollMax(Roll):
def __init__(self, roll: Roll, exp: Exp):
self.roll = roll
self.exp = exp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
r2 = self.exp.eval(vtable)
if not ((isinstance(r1,list) and all(isinstance(i,int) for i in r1)) and isinstance(r2,int)):
return []
self.show_list = []
for i in range(len(r1)):
if r1[i] > r2:
r1[i] = r2
self.show_list.append(f"{r2}v")
else:
self.show_list.append(str(r1[i]))
self.result = r1
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)
class RollExplode(Roll):
def __init__(self, roll: Roll, comp: ComparePoint = None):
self.roll = roll
self.comp = comp
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
if not (isinstance(r1,list) and all(isinstance(i,int) for i in r1)):
return []
d = self.die
if self.comp is None:
self.comp = ComparePoint("=", ExpInt(d))
self.result = []
self.show_list = []
def compare(n):
if self.comp.eval(vtable, n):
self.result.append(n)
self.show_list.append(f"{n}!")
compare(randint(1,d))
else:
self.result.append(n)
self.show_list.append(str(n))
for n in r1:
compare(n)
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)
class RollReroll(Roll):
def __init__(self, roll: Roll, comp: ComparePoint = None, once: bool = False):
self.roll = roll
self.comp = comp
self.once = once
self.result = None
self.show_list = None
def _eval(self, vtable):
if self.result is not None:
return self.result
r1 = self.roll.eval(vtable)
if not (isinstance(r1,list) and all(isinstance(i,int) for i in r1)):
return []
d = self.die
if self.comp is None:
self.comp = ComparePoint("=", ExpInt(1))
self.result = []
self.show_list = []
def compare(n, rerolled):
if self.comp.eval(vtable, n) and not (rerolled and self.once):
self.show_list.append(f"~~{n}~~")
compare(randint(1,d), True)
else:
self.result.append(n)
self.show_list.append(str(n))
for n in r1:
compare(n, False)
return self.result
def __repr__(self) -> str:
return f"max({self.roll},{self.exp})"
def __eq__(self, other: Exp) -> bool:
return (
isinstance(other, RollMax) and
self.roll == other.roll and
self.exp == other.exp
)

View File

@ -0,0 +1,52 @@
from string import ascii_letters, digits
from rply import LexerGenerator
VALID_CHARACTERS = ascii_letters+"_"+digits
TOKENS = [
("ROLL_DIE", r"d"),
("ROLL_KEEP_LOWEST", r"kl"),
("ROLL_KEEP_HIGHEST", r"kh?"),
("ROLL_REROLL_ONCE", r"ro"),
("ROLL_REROLL", r"r"),
("ROLL_MIN", r"min"),
("ROLL_MAX", r"max"),
("ROLL_EXPLODE", r"!"),
("SYMBOL_LE", r"\<\="),
("SYMBOL_GE", r"\>\="),
("SYMBOL_LT", r"\<"),
("SYMBOL_GT", r"\>"),
("SYMBOL_EQUALS", r"\="),
("SYMBOL_ARROW", r"\-\>"),
("SYMBOL_BACKSLASH", r"\\"),
("SYMBOL_LPARENS", r"\("),
("SYMBOL_RPARENS", r"\)"),
("SYMBOL_PLUS", r"\+"),
("SYMBOL_MINUS", r"\-"),
("SYMBOL_TIMES", r"\*"),
("SYMBOL_DIVIDE", r"\/"),
("KEYWORD_LET", r"let"),
("KEYWORD_IN", r"in"),
("KEYWORD_IF", r"if"),
("KEYWORD_THEN", r"then"),
("KEYWORD_ELSE", r"else"),
("INT", r"\d+"),
("ID", f"[{ascii_letters}][{VALID_CHARACTERS}]*")
]
class Lexer():
def __init__(self):
self.lexer = LexerGenerator()
def _add_tokens(self):
for token in TOKENS:
self.lexer.add(*token)
self.lexer.ignore(r"\s+")
def get_lexer(self):
self._add_tokens()
return self.lexer.build()

View File

@ -0,0 +1,153 @@
from rply import ParserGenerator
from lexer import TOKENS
from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpMin, ExpMax, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, RollExplode, ComparePoint, ExpIf, ExpTest, ExpApply, ExpLambda, ExpNeg, RollReroll
class Parser():
def __init__(self):
self.pg = ParserGenerator(
[i[0] for i in TOKENS],
precedence=[
('left', ["SYMBOL_BACKSLASH","SYMBOL_ARROW"]),
('left', ["KEYWORD_LET", "KEYWORD_IN", "KEYWORD_IF", "KEYWORD_THEN", "KEYWORD_ELSE"]),
('left', ["SYMBOL_EQUALS", "SYMBOL_LT", "SYMBOL_LE", "SYMBOL_GT", "SYMBOL_GE"]),
('left', ["SYMBOL_PLUS", "SYMBOL_MINUS"]),
('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]),
('left', ["ROLL_DIE"]),
('right', ["ROLL_KEEP_HIGHEST","ROLL_KEEP_LOWEST","ROLL_MIN","ROLL_MAX","ROLL_EXPLODE","ROLL_REROLL","ROLL_REROLL_ONCE"])
]
)
self._get_parser()
def parse(self, token_input):
return self.parser.parse(token_input)
def _get_parser(self) -> Exp:
# Expressions
@self.pg.production('exp : exp SYMBOL_PLUS exp')
@self.pg.production('exp : exp SYMBOL_MINUS exp')
@self.pg.production('exp : exp SYMBOL_TIMES exp')
@self.pg.production('exp : exp SYMBOL_DIVIDE exp')
def exp_mul_div(tokens):
return ExpBinop(tokens[1].value, tokens[0], tokens[2])
@self.pg.production('exp : atom')
def exp_atom(tokens):
return tokens[0]
@self.pg.production("exp : SYMBOL_MINUS atom")
def exp_neg(tokens):
return ExpNeg(tokens[1])
@self.pg.production('exp : KEYWORD_LET ID SYMBOL_EQUALS exp KEYWORD_IN exp')
def exp_let(tokens):
return ExpLet(tokens[1].value, tokens[3], tokens[5])
@self.pg.production('exp : roll')
def exp_roll(tokens):
return ExpRoll(tokens[0])
@self.pg.production("exp : atom ROLL_MIN atom")
def exp_min(tokens):
return ExpMin(tokens[0], tokens[2])
@self.pg.production("exp : atom ROLL_MAX atom")
def exp_max(tokens):
return ExpMax(tokens[0], tokens[2])
@self.pg.production("exp : atom comp")
def exp_test(tokens):
return ExpTest(tokens[0], tokens[1])
@self.pg.production("exp : KEYWORD_IF exp KEYWORD_THEN exp KEYWORD_ELSE exp")
def exp_if(tokens):
return ExpIf(tokens[1],tokens[3],tokens[5])
@self.pg.production("exp : SYMBOL_BACKSLASH ID SYMBOL_ARROW exp")
def exp_lamda(tokens):
return ExpLambda(tokens[1].value, tokens[3])
@self.pg.production("exp : atom atom")
def apply(tokens):
return ExpApply(tokens[0],tokens[1])
# Rolls
@self.pg.production('roll : roll ROLL_KEEP_HIGHEST atom')
def roll_keep_highest(tokens):
return RollKeepHighest(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_KEEP_LOWEST atom')
def roll_keep_lowest(tokens):
return RollKeepLowest(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_MIN atom')
def roll_min(tokens):
return RollMin(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_MAX atom')
def roll_max(tokens):
return RollMax(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_EXPLODE ')
def roll_explode(tokens):
return RollExplode(tokens[0])
@self.pg.production('roll : roll ROLL_EXPLODE comp')
def roll_explode_comp(tokens):
return RollExplode(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_REROLL ')
def roll_reroll(tokens):
return RollReroll(tokens[0])
@self.pg.production('roll : roll ROLL_REROLL comp')
def roll_reroll_comp(tokens):
return RollReroll(tokens[0], tokens[2])
@self.pg.production('roll : roll ROLL_REROLL_ONCE')
def roll_reroll_once(tokens):
return RollReroll(tokens[0], None, True)
@self.pg.production('roll : roll ROLL_REROLL_ONCE comp')
def roll_reroll_once_comp(tokens):
return RollReroll(tokens[0], tokens[2], True)
@self.pg.production('roll : atom ROLL_DIE atom')
def roll(tokens):
return Roll(tokens[0], tokens[2])
@self.pg.production('roll : ROLL_DIE atom')
def roll_no_amount(tokens):
return Roll(ExpInt(1), tokens[1])
# Compare Points
@self.pg.production("comp : SYMBOL_EQUALS atom")
@self.pg.production("comp : SYMBOL_LT atom")
@self.pg.production("comp : SYMBOL_LE atom")
@self.pg.production("comp : SYMBOL_GT atom")
@self.pg.production("comp : SYMBOL_GE atom")
def comp_point(tokens):
return ComparePoint(tokens[0].value,tokens[1])
# Atoms
@self.pg.production("atom : INT")
def atom_int(tokens):
return ExpInt(int(tokens[0].value))
@self.pg.production("atom : SYMBOL_LPARENS exp SYMBOL_RPARENS")
def atom_exp(tokens):
return tokens[1]
@self.pg.production('atom : ID')
def atom_var(tokens):
return ExpVar(tokens[0].value)
## Error Handling ##
@self.pg.error
def error_handle(token):
raise Exception(f"Unexpected token '{token.value}' ({token.name}) at line {token.source_pos.lineno}, column {token.source_pos.colno}.")
## Finish ##
self.parser = self.pg.build()

View File

@ -0,0 +1,58 @@
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from lexer import Lexer
from parser import Parser
from ast_nodes import Exp, ExpInt, ExpBinop, ExpLet, ExpVar, ExpRoll, Roll, RollKeepHighest, RollKeepLowest, RollMin, RollMax, ExpMin, ExpMax
class DieRoller():
def __init__(self) -> None:
self.lexer = Lexer().get_lexer()
self.parser = Parser()
def parse(self, string: str) -> Exp:
tokens = self.lexer.lex(string)
expression = self.parser.parse(tokens)
return expression
def roll(self, string: str):
exp = self.parse(string)
# print(exp)
# exit()
show = exp.show({})
eval = exp.eval({})
if show is None or eval is None or isinstance(eval,tuple):
raise Exception("Something went wrong")
return f"**Result**: {show}\n**Total**: {eval}"
if __name__ == "__main__":
d = DieRoller()
test_strings = [
("4", ExpInt(4)),
("1 + 1", ExpBinop("+",ExpInt(1),ExpInt(1))),
("5*5", ExpBinop("*",ExpInt(5),ExpInt(5))),
("1+5/5", ExpBinop("+",ExpInt(1),ExpBinop("/",ExpInt(5),ExpInt(5)))),
("(1+5)/6", ExpBinop("/",ExpBinop("+",ExpInt(1),ExpInt(5)),ExpInt(6))),
("let x=5 in x*2", ExpLet("x",ExpInt(5),ExpBinop("*",ExpVar("x"),ExpInt(2)))),
("let x= let y = 2 in y in let z = 5 in x*z", ExpLet("x",ExpLet("y",ExpInt(2),ExpVar("y")),ExpLet("z",ExpInt(5),ExpBinop("*",ExpVar("x"),ExpVar("z"))))),
("4d6", ExpRoll(Roll(ExpInt(4),ExpInt(6)))),
("d6", ExpRoll(Roll(ExpInt(1),ExpInt(6)))),
("(2+2)d(3*2)", ExpRoll(Roll(ExpBinop("+",ExpInt(2),ExpInt(2)),ExpBinop("*",ExpInt(3),ExpInt(2))))),
("4d3*2", ExpBinop("*",ExpRoll(Roll(ExpInt(4),ExpInt(3))),ExpInt(2))),
("4d6kh3", ExpRoll(RollKeepHighest(Roll(ExpInt(4),ExpInt(6)),ExpInt(3)))),
("2d20kl1", ExpRoll(RollKeepLowest(Roll(ExpInt(2),ExpInt(20)),ExpInt(1)))),
("11d10kh6kl1", ExpRoll(RollKeepLowest(RollKeepHighest(Roll(ExpInt(11),ExpInt(10)),ExpInt(6)),ExpInt(1)))),
("3d6min6", ExpRoll(RollMin(Roll(ExpInt(3),ExpInt(6)),ExpInt(6)))),
("3d6max1", ExpRoll(RollMax(Roll(ExpInt(3),ExpInt(6)),ExpInt(1)))),
("let x = 1d4 in (x)min3", ExpLet("x",ExpRoll(Roll(ExpInt(1),ExpInt(4))),ExpMin(ExpVar("x"),ExpInt(3)))),
("10max(2d10)",ExpMax(ExpInt(10),ExpRoll(Roll(ExpInt(2),ExpInt(10)))))
]
for s, t in test_strings:
r = d.parse(s)
correct = r == t
print(f"{str(s) : <20}: {'' if correct else ''} ({r.eval({})})")
if not correct:
print(f"Expected: {t}")
print(f"Actual: {r}\n")
print(d.roll(input(":")))

View File

@ -1,135 +1,76 @@
"""
Contains the Gwendolyn class, a subclass of the discord command bot.
*Classes*
---------
Gwendolyn(discord.ext.commands.Bot)
"""
import os # Used for loading cogs in Gwendolyn.addCogs
import finnhub # Used to add a finhub client to the bot
import discord # Used for discord.Intents and discord.Status
import discord_slash # Used to initialized SlashCommands object
import git # Used to pull when stopping
from discord.ext import commands # Used to inherit from commands.bot
from pymongo import MongoClient # Used for database management
from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs
from gwendolyn.utils import (get_options, get_credentials, log_this,
DatabaseFuncs, EventHandler, ErrorHandler,
long_strings)
class Gwendolyn(commands.Bot):
"""
A multifunctional Discord bot.
*Methods*
---------
log(messages: Union[str, list], channel: str = "",
level: int = 20)
stop(ctx: discord_slash.context.SlashContext)
defer(ctx: discord_slash.context.SlashContext)
"""
# pylint: disable=too-many-instance-attributes
def __init__(self):
"""Initialize the bot."""
intents = discord.Intents.default()
intents.members = True
initiation_parameters = {
"command_prefix": " ",
"case_insensitive": True,
"intents": intents,
"status": discord.Status.dnd
}
super().__init__(**initiation_parameters)
self._add_clients_and_options()
self._add_util_classes()
self._add_function_containers()
self._add_cogs()
def _add_clients_and_options(self):
"""Add all the client, option and credentials objects."""
self.long_strings = long_strings()
self.options = get_options()
self.credentials = get_credentials()
finnhub_key = self.credentials["finnhub_key"]
self.finnhub_client = finnhub.Client(api_key=finnhub_key)
mongo_user = self.credentials["mongo_db_user"]
mongo_password = self.credentials["mongo_db_password"]
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
database_clint = MongoClient(mongo_url)
if self.options["testing"]:
self.log("Testing mode")
self.database = database_clint["Gwendolyn-Test"]
else:
self.database = database_clint["Gwendolyn"]
def _add_util_classes(self):
"""Add all the classes used as utility."""
self.database_funcs = DatabaseFuncs(self)
self.event_handler = EventHandler(self)
self.error_handler = ErrorHandler(self)
slash_parameters = {
"sync_commands": True,
"sync_on_cog_reload": True,
"override_type": True
}
self.slash = discord_slash.SlashCommand(self, **slash_parameters)
def _add_function_containers(self):
"""Add all the function containers used for commands."""
self.star_wars = StarWars(self)
self.other = Other(self)
self.lookup_funcs = LookupFuncs(self)
self.games = Games(self)
self.money = Money(self)
def _add_cogs(self):
"""Load cogs."""
for filename in os.listdir("./gwendolyn/cogs"):
if filename.endswith(".py"):
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}")
def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use
"""Log a message. Described in utils/util_functions.py."""
log_this(messages, channel, level)
async def stop(self, ctx: discord_slash.context.SlashContext):
"""
Stop the bot, and stop running games.
Only stops the bot if the user in ctx.author is one of the
admins given in options.txt.
*parameters*
------------
ctx: discord_slash.context.SlashContext
The context of the "/stop" slash command.
"""
if f"#{ctx.author.id}" in self.options["admins"]:
await ctx.send("Pulling git repo and restarting...")
await self.change_presence(status=discord.Status.offline)
self.database_funcs.wipe_games()
if not self.options["testing"]:
git_client = git.cmd.Git("")
git_client.pull()
self.log("Logging out", level=25)
await self.close()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
async def defer(self, ctx: discord_slash.context.SlashContext):
"""Send a "Gwendolyn is thinking" message to the user."""
try:
await ctx.defer()
except discord_slash.error.AlreadyResponded:
self.log("defer failed")
from dotenv import load_dotenv
from os import getenv, listdir
from interactions import Client, Status
from pymongo import MongoClient # Used for database management
from gwendolyn.utils import log
from gwendolyn.exceptions import NoToken, CannotConnectToService
from gwendolyn.funcs import Other, BetterNetflix, Sonarr, Radarr, TMDb, QBittorrent
class Gwendolyn(Client):
def __init__(self, testing: bool = True):
"""Initialize the bot."""
initiation_parameters = {
"status": Status.DND,
"delete_unused_application_cmds": True
}
super().__init__(**initiation_parameters)
self.testing = testing
self._add_clients_and_options()
self._add_functions()
self._add_extensions()
def _add_clients_and_options(self):
"""Add all the client, option and credentials objects."""
load_dotenv()
self.bot_token = getenv("DISCORD_TOKEN")
if self.bot_token == "TOKEN":
raise NoToken()
self.admins = getenv("ADMINS").split(",")
mongo_user = getenv("MONGODB_USER")
mongo_password = getenv("MONGODB_PASSWORD")
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
database_client = MongoClient(mongo_url)
try:
database_client.admin.command("ping")
self.log("Connected to Mango Client")
except:
raise CannotConnectToService("Mango Client")
if self.testing:
self.log("Testing mode")
self.database = database_client["Gwendolyn-Test"]
self.load_extension("interactions.ext.jurigged")
else:
self.database = database_client["Gwendolyn"]
def _add_functions(self):
self.other = Other(self)
self.better_netflix = BetterNetflix(
self,
radarr=Radarr(getenv("RADARR_IP"),getenv("RADARR_PORT"),getenv("RADARR_API_KEY")),
sonarr=Sonarr(getenv("SONARR_IP"),getenv("SONARR_PORT"),getenv("SONARR_API_KEY")),
tmdb=TMDb(getenv("TMDB_API_ACCESS_TOKEN")),
qbittorrent=QBittorrent(getenv("QBITTORRENT_IP"),getenv("QBITTORRENT_PORT"),getenv("QBITTORRENT_USERNAME"),getenv("QBITTORRENT_PASSWORD"))
)
def _add_extensions(self):
"""Load cogs."""
for filename in listdir("./gwendolyn/ext"):
if filename.endswith(".py"):
self.load_extension(f"gwendolyn.ext.{filename[:-3]}")
def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use
"""Log a message. Described in utils/util_functions.py."""
log(messages, channel, level)
def start(self):
super().start(self.bot_token)

View File

@ -1,3 +0,0 @@
`/invest` vil vise dig hvilke aktier du har. `/invest check [symbol]` viser dig en akties nuværende pris, hvor [symbol] er forkortelsen for firmaet. GwendoBucks er lig med 1 amerikans cent.
`/invest buy [symbol] [pris]` lader dig købe aktier. [pris] er mængden af GwendoBucks du bruger på at købe. Du kan købe for færre GwendoBucks end en enkelt akties pris, men ikke for mindre end 100 GwendoBucks.
`/invest buy [symbol] [pris]` lader dig sælge dine aktier. Du kan godt sælge for mindre end 100 GwendoBucks.

View File

@ -1,413 +1,70 @@
{
"add_movie" : {
"name" : "add_movie",
"description" : "Request a movie for Plex",
"options" : [
{
"name" : "movie",
"description" : "The movie to request",
"type" : 3,
"required" : "true"
}
]
"misc": {
"hello" : {
"name" : "hello",
"description" : "Greet Gwendolyn"
},
"help" : {
"name" : "help",
"description" : "Get help with a command",
"options" : [
{
"name" : "command",
"description" : "The command you want help with",
"type" : 3,
"required" : "false"
}
]
},
"echo" : {
"name": "echo",
"description": "Make Gwendolyn repeat something",
"options" : [
{
"name" : "text",
"description" : "The text you want Gwendolyn to repeat",
"type" : 3,
"required" : "true"
}
]
},
"gen_name" : {
"name": "gen_name",
"description": "Generate a random name"
},
"ping" : {
"name" : "ping",
"description" : "Get the Gwendolyn's latency to the server"
},
"roll" : {
"name": "roll",
"description": "Roll dice",
"options": [
{
"name": "dice",
"description": "The dice to be rolled",
"type": 3,
"required": "false"
}
]
},
"stop" : {
"name" : "stop",
"description" : "Stop Gwendolyn"
},
"thank" : {
"name" : "thank",
"description" : "Thank Gwendolyn for her service"
}
},
"add_show" : {
"name" : "add_show",
"description" : "Request a show for Plex",
"options" : [
{
"name" : "show",
"description" : "The show to request",
"type" : 3,
"required" : "true"
}
]
},
"balance" : {
"name" : "balance",
"description" : "See your balance of GwendoBucks"
},
"blackjack_bet" : {
"base" : "blackjack",
"name" : "bet",
"description" : "Enter the current blackjack game with a bet",
"options" : [
{
"name" : "bet",
"description" : "Your bet",
"type" : 4,
"required" : "true"
}
]
},
"blackjack_cards" : {
"base" : "blackjack",
"name" : "cards",
"description" : "Get a count of the cards used in blackjack games"
},
"blackjack_hilo" : {
"base" : "blackjack",
"name" : "hilo",
"description" : "Get the current hi-lo value for the cards used in blackjack games"
},
"blackjack_shuffle" : {
"base" : "blackjack",
"name" : "shuffle",
"description" : "Shuffle the cards used in blackjack games"
},
"blackjack_start" : {
"base" : "blackjack",
"name" : "start",
"description" : "Start a game of blackjack"
},
"connect_four_start_gwendolyn" : {
"base" : "connect_four",
"subcommand_group" : "start",
"name" : "Gwendolyn",
"description" : "Start a game of connect four against Gwendolyn",
"options" : [
{
"name" : "difficulty",
"description" : "The difficulty of Gwendolyn's AI",
"type" : 4,
"required" : "false"
}
]
},
"connect_four_start_user" : {
"base" : "connect_four",
"subcommand_group" : "start",
"name" : "user",
"description" : "Start a game of connect four against another user",
"options" : [
{
"name" : "user",
"description" : "The user to start a game against",
"type" : 6,
"required" : "true"
}
]
},
"downloading" : {
"name" : "downloading",
"description" : "See current downloads for Plex",
"options" : [
{
"name" : "parameters",
"description" : "Parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"game" : {
"name" : "game",
"description" : "Set the 'playing' text for Gwendolyn",
"options" : [
{
"name" : "game_text",
"description" : "The game to set the 'playing' text to",
"type" : 3,
"required" : "true"
}
]
},
"give" : {
"name" : "give",
"description" : "Give GwendoBucks to another user",
"options" : [
{
"name" : "user",
"description" : "The user you're sending GwendoBucks to",
"type" : 6,
"required" : "true"
},
{
"name" : "amount",
"description" : "The number of GwendoBucks you're sending",
"type" : 4,
"required" : "true"
}
]
},
"hangman" : {
"name" : "hangman",
"description" : "Start a game of hangman"
},
"hello" : {
"name" : "hello",
"description" : "Greet Gwendolyn"
},
"help" : {
"name" : "help",
"description" : "Get help with a command",
"options" : [
{
"name" : "command",
"description" : "The command you want help with",
"type" : 3,
"required" : "false"
}
]
},
"hex_place" : {
"base" : "hex",
"name" : "place",
"description" : "Place a piece on the hex board",
"options" : [
{
"name" : "coordinates",
"description" : "The coordinates to place the piece at",
"type" : 3,
"required" : "true"
}
]
},
"hex_start_gwendolyn" : {
"base" : "hex",
"subcommand_group" : "start",
"name" : "Gwendolyn",
"description" : "Start a game of hex against Gwendolyn",
"options" : [
{
"name" : "difficulty",
"description" : "The difficulty of Gwendolyn's AI",
"type" : 4,
"required" : "false"
}
]
},
"hex_start_user" : {
"base" : "hex",
"subcommand_group" : "start",
"name" : "user",
"description" : "Start a game of hex against another user",
"options" : [
{
"name" : "user",
"description" : "The user to start a game against",
"type" : 6,
"required" : "true"
}
]
},
"hex_surrender" : {
"base" : "hex",
"name" : "surrender",
"description" : "Surrender the game of hex"
},
"hex_swap" : {
"base" : "hex",
"name" : "swap",
"description" : "Perform a hex swap"
},
"hex_undo" : {
"base" : "hex",
"name" : "undo",
"description" : "Undo your last hex move"
},
"invest" : {
"name" : "invest",
"description" : "Invest GwendoBucks in the stock market",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"image" : {
"name" : "image",
"description" : "Get a random image from Bing"
},
"monster" : {
"name" : "monster",
"description" : "Look up a monster",
"options" : [
{
"name" : "query",
"description" : "The monster to look up",
"type" : 3,
"required" : "true"
}
]
},
"movie" : {
"name" : "movie",
"description" : "Get the name and information of a random movie"
},
"name" : {
"name" : "name",
"description" : "Generate a random name"
},
"ping" : {
"name" : "ping",
"description" : "Get the Gwendolyn's latency to the server"
},
"roll" : {
"name" : "roll",
"description" : "Roll rpg dice",
"options" : [
{
"name" : "dice",
"description" : "The dice to roll",
"type" : 3,
"required" : "false"
}
]
},
"spell" : {
"name" : "spell",
"description" : "Look up a spell",
"options" : [
{
"name" : "query",
"description" : "The spell to look up",
"type" : 3,
"required" : "true"
}
]
},
"star_wars_character" : {
"name" : "star_wars_character",
"description" : "Manage your Star Wars character sheet",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"star_wars_crit" : {
"name" : "star_wars_crit",
"description" : "Roll a Star Wars critical injury",
"options" : [
{
"name" : "severity",
"description" : "The severity of the hit",
"type" : 4,
"required" : "true"
}
]
},
"star_wars_destiny" : {
"name" : "star_wars_destiny",
"description" : "Use and see Star Wars Destiny points",
"options" : [
{
"name" : "parameters",
"description" : "The parameters for the command",
"type" : 3,
"required" : "false"
}
]
},
"star_wars_roll" : {
"name" : "star_wars_roll",
"description" : "Roll Star Wars dice",
"options" : [
{
"name" : "dice",
"description" : "The dice, or ability, to roll",
"type" : 3,
"required" : "false"
}
]
},
"stop" : {
"name" : "stop",
"description" : "Restart Gwendolyn"
},
"tavern" : {
"name" : "tavern",
"description" : "Generate a random tavern"
},
"thank" : {
"name" : "thank",
"description" : "Thank Gwendolyn for her service"
},
"trivia" : {
"name" : "trivia",
"description" : "Play a game of trivia",
"options" : [
{
"name" : "answer",
"description" : "Your answer to the trivia question",
"type" : 3,
"required" : "false",
"choices" : [
{
"name" : "a",
"value" : "a"
},
{
"name" : "b",
"value" : "b"
},
{
"name" : "c",
"value" : "c"
},
{
"name" : "d",
"value" : "d"
}
]
}
]
},
"wiki" : {
"name" : "wiki",
"description" : "Searches for and gets the info for a wiki page",
"options" : [
{
"name" : "page",
"description" : "The page to find",
"type" : 3,
"required" : "false"
}
]
},
"wolf" : {
"name" : "wolf",
"description" : "Performs a search on Wolfram Alpha",
"options" : [
{
"name" : "query",
"description" : "What to search for on Wolfram Alpha",
"type" : 3,
"required" : "true"
}
]
},
"wordle_start": {
"base": "wordle",
"name" : "start",
"description": "Start a game of wordle",
"options": [
{
"name": "letters",
"description" : "How many letters the word should be",
"type": 4,
"required": "false"
}
]
},
"wordle_guess": {
"base": "wordle",
"name" : "guess",
"description": "Guess a word in wordle",
"options": [
{
"name": "guess",
"description" : "Your guess",
"type": 3,
"required": "true"
}
]
"better_netflix": {
"movie": {
"name": "movie",
"description": "Get a random movie from Better Netflix"
},
"show": {
"name": "show",
"description": "Get a random show from Better Netflix"
}
}
}

View File

@ -56,11 +56,8 @@
]
},
"txt": {
"gwendolyn/resources/star_wars/destinyPoints.txt": "",
"gwendolyn/resources/movies.txt": "The Room",
"gwendolyn/resources/names.txt": "Gandalf\n",
"credentials.txt" : "Bot token: TOKEN\nFinnhub API key: KEY\nWordnik API Key: KEY\nMongoDB user: USERNAME\nMongoDB password: PASSWORD\nWolframAlpha AppID: APPID\nRadarr API key: KEY\nSonarr API key: KEY\nqBittorrent username: USER\nqBittorrent password: PASSWORD",
"options.txt" : "Testing: True\nTesting guild ids:\nAdmins:"
".env" : "# Discord Bot Token\nDISCORD_TOKEN=TOKEN\n\n# Wordnik API Key\nWORDNIK_API_KEY=KEY\n\n# MongoDB Credentials\nMONGODB_USER=USERNAME\nMONGODB_PASSWORD=PASSWORD\n\n# WolframAlpha AppID\nWOLFRAM_APPID=APPID\n\n# Radarr API Key\nRADARR_API_KEY=KEY\n\n# Sonarr API Key\nSONARR_API_KEY=KEY\n\n# qBittorrent Credentials\nQBITTORRENT_USERNAME=USER\nQBITTORRENT_PASSWORD=PASSWORD\n\n# Admins (comma-separated list of Discord user IDs)\nADMINS="
},
"folder" : [
"gwendolyn/resources/lookup",
@ -68,7 +65,6 @@
"gwendolyn/resources/games/connect_four_boards",
"gwendolyn/resources/games/hex_boards",
"gwendolyn/resources/games/hangman_boards",
"gwendolyn/resources/plex",
"gwendolyn/resources/games/old_images"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,12 +1,5 @@
"""A collections of utilities used by Gwendolyn and her functions."""
__all__ = ["get_options", "get_credentials", "DatabaseFuncs", "EventHandler",
"ErrorHandler", "get_params", "log_this", "cap", "make_files",
"replace_multiple", "emoji_to_command"]
__all__ = ["make_files","log","PARAMS"]
from .helper_classes import DatabaseFuncs
from .event_handlers import EventHandler, ErrorHandler
from .util_functions import (get_params, log_this, cap, make_files,
replace_multiple, emoji_to_command, long_strings,
sanitize, get_options, get_credentials, encode_id,
decode_id)
from .util_functions import make_files, log, PARAMS

View File

@ -1,37 +1,7 @@
"""
Contains utility functions used by parts of the bot.
*Functions*
-----------
sanitize(data: str, lower_case_value: bool = false) -> dict
get_options() -> dict
get_credentials() -> dict
long_strings() -> dict
get_params() -> dict
log_this(messages: Union[str, list], channel: str = "",
level: int = 20)
cap(s: str) -> str
make_files()
replace_multiple(main_string: str, to_be_replaced: list,
new_string: str) -> str
emoji_to_command(emoji: str) -> str
"""
import string
import json # Used by long_strings(), get_params() and make_files()
import logging # Used for logging
import os # Used by make_files() to check if files exist
import sys # Used to specify printing for logging
import imdb # Used to disable logging for the module
BASE_37 = ":" + string.digits + string.ascii_uppercase
BASE_128 = list(
string.digits +
string.ascii_letters +
"!#$€£¢¥¤&%()*+,-./;:<=>?@[]_{|}~ `¦§©®«»±µ·¿əʒ" +
"ÆØÅÐÉÈÊÇÑÖ" +
"æøåðéèêçñö"
)
# All of this is logging configuration
FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s"
@ -53,115 +23,6 @@ handler.setFormatter(logging.Formatter(fmt=PRINTFORMAT, datefmt=DATEFORMAT))
printer.addHandler(handler)
printer.propagate = False
imdb._logging.setLevel("CRITICAL") # pylint: disable=protected-access
# Basically disables imdbpy logging, since it's being printed to the
# terminal.
def sanitize(data: str, lower_case_value: bool = False):
"""
Sanitize and create a dictionary from a string.
Each element is created from a line with a : in it. The key is left
of the :, the value is right of it.
*Parameters*
------------
data: str
The string to create a dict from.
lower_case_value: bool = False
Whether the value of each element should be lowercase.
*Returns*
---------
dct: dict
The sanitized dictionary of elements.
"""
data = data.splitlines()
dct = {}
for line in data:
if line[0] != "#" and ":" in line:
line_values = line.split(":")
line_values[0] = line_values[0].lower()
line_values[1] = line_values[1].replace(" ", "")
if lower_case_value:
line_values[1] = line_values[1].lower()
if line_values[0] in ["testing guild ids", "admins"]:
line_values[1] = line_values[1].split(",")
if all(i.isnumeric() for i in line_values[1]):
line_values[1] = [int(i) for i in line_values[1]]
if any(i == line_values[1] for i in ["true", "false"]):
line_values[1] = (line_values[1] == "true")
dct[line_values[0]] = line_values[1]
return dct
def get_options():
"""
Get the bot options as dict.
*Returns*
---------
options: dict
The options of the bot.
"""
with open("options.txt", "r", encoding="utf-8") as file_pointer:
data = sanitize(file_pointer.read(), True)
options = {}
options["testing"] = data["testing"]
options["guild_ids"] = data["testing guild ids"]
options["admins"] = data["admins"]
return options
def get_credentials():
"""
Returns the credentials used by the bot as a dict.
*Returns*
---------
credentials: dict
The credentials used by the bot.
"""
with open("credentials.txt", "r", encoding="utf-8") as file_pointer:
data = sanitize(file_pointer.read())
credentials = {}
credentials["token"] = data["bot token"]
credentials["finnhub_key"] = data["finnhub api key"]
credentials["wordnik_key"] = data["wordnik api key"]
credentials["mongo_db_user"] = data["mongodb user"]
credentials["mongo_db_password"] = data["mongodb password"]
credentials["wolfram_alpha_key"] = data["wolframalpha appid"]
credentials["radarr_key"] = data["radarr api key"]
credentials["sonarr_key"] = data["sonarr api key"]
credentials["qbittorrent_username"] = data["qbittorrent username"]
credentials["qbittorrent_password"] = data["qbittorrent password"]
return credentials
def long_strings():
"""
Get the data from gwendolyn/resources/long_strings.json.
*Returns*
---------
data: dict
The long strings and their keys.
"""
long_strings_path = "gwendolyn/resources/long_strings.json"
with open(long_strings_path, "r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)
return data
def get_params():
"""
Get the slash command parameters.
@ -175,16 +36,11 @@ def get_params():
with open(path, "r", encoding="utf-8") as file_pointer:
slash_parameters = json.load(file_pointer)
options = get_options()
if options["testing"]:
for parameter in slash_parameters:
slash_parameters[parameter]["guild_ids"] = options["guild_ids"]
return slash_parameters
PARAMS = get_params()
def log_this(messages, channel: str = "", level: int = 20):
def log(messages, channel: str = "", level: int = 20):
"""
Log something in Gwendolyn's logs.
@ -222,49 +78,19 @@ def log_this(messages, channel: str = "", level: int = 20):
for log_message in messages:
logger.log(level, log_message)
def cap(input_string: str):
"""
Capitalize a string like a movie title.
That means "of" and "the" are not capitalized.
*Parameters*
------------
input_string: str
The string to capitalized.
*Returns*
---------
return_string: str
The capitalized string.
"""
no_caps_list = ["of", "the"]
word_number = 0
string_list = input_string.split()
return_string = ''
for word in string_list:
word_number += 1
if word not in no_caps_list or word_number == 1:
word = word.capitalize()
return_string += word+" "
return_string = return_string[:-1]
return return_string
def make_files():
"""Create all the files and directories needed by Gwendolyn."""
def make_json_file(path, content):
"""Create json file if it doesn't exist."""
if not os.path.isfile(path):
log_this(path.split("/")[-1]+" didn't exist. Making it now.")
log(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path, "w", encoding="utf-8") as file_pointer:
json.dump(content, file_pointer, indent=4)
def make_txt_file(path, content):
"""Create txt file if it doesn't exist."""
if not os.path.isfile(path):
log_this(path.split("/")[-1]+" didn't exist. Making it now.")
log(path.split("/")[-1]+" didn't exist. Making it now.")
with open(path, "w", encoding="utf-8") as file_pointer:
file_pointer.write(content)
@ -272,121 +98,18 @@ def make_files():
"""Create directory if it doesn't exist."""
if not os.path.isdir(path):
os.makedirs(path)
log_this("The "+path.split("/")[-1]+" directory didn't exist")
log("The "+path.split("/")[-1]+" directory didn't exist")
file_path = "gwendolyn/resources/starting_files.json"
with open(file_path, "r", encoding="utf-8") as file_pointer:
data = json.load(file_pointer)
for path in data["folder"]:
directory(path)
for path, content in data["json"].items():
make_json_file(path, content)
for path, content in data["txt"].items():
make_txt_file(path, content)
for path in data["folder"]:
directory(path)
def replace_multiple(main_string: str, to_be_replaced: list, new_string: str):
"""
Replace multiple substrings in a string with the same substring.
*Parameters*
------------
main_string: str
The string to replace substrings in.
to_be_replaced: list
The substrings to replace.
new_string: str
The string to replace the substrings with.
*Returns*
---------
main_string: str
The string with the substrings replaced.
"""
# Iterate over the strings to be replaced
for elem in to_be_replaced:
# Check if string is in the main string
if elem in main_string:
# Replace the string
main_string = main_string.replace(elem, new_string)
return main_string
def emoji_to_command(emoji: str):
"""
Convert emoji to text.
*Parameters*
------------
emoji: str
The emoji to decipher.
*Returns*
---------
: str
The deciphered string.
"""
if emoji == "1":
return_value = 1
elif emoji == "2":
return_value = 2
elif emoji == "3":
return_value = 3
elif emoji == "4":
return_value = 4
elif emoji == "5":
return_value = 5
elif emoji == "6":
return_value = 6
elif emoji == "7":
return_value = 7
elif emoji == "🎲":
return_value = "roll"
elif emoji == "":
return_value = "none"
elif emoji == "✔️":
return_value = 1
else:
return_value = ""
return return_value
def encode_id(info: list):
letters = list(":".join(info))
dec = 0
for i, letter in enumerate(letters):
try:
dec += (37**i) * BASE_37.index(letter.upper())
except ValueError:
log_this(f"Could not encode letter {letter}", level=30)
custom_id = []
while dec:
custom_id.append(BASE_128[dec % 128])
dec = dec // 128
custom_id = ''.join(custom_id)
log_this(f"Encoded {info} to {custom_id}")
return custom_id
def decode_id(custom_id: str):
letters = list(custom_id)
dec = 0
for i, letter in enumerate(letters):
dec += (128**i) * BASE_128.index(letter)
info_string = []
while dec:
info_string.append(BASE_37[dec % 37])
dec = dec // 37
info = ''.join(info_string).split(':')
log_this(f"Decoded {custom_id} to be {info}")
return ''.join(info_string).split(':')

View File

@ -0,0 +1,6 @@
"""The main module for Gwendolyn."""
# pylint: disable=invalid-name
__all__ = ["funcs", "utils", "Gwendolyn"]
from .gwendolyn_client import Gwendolyn

View File

@ -1,8 +1,8 @@
"""Contains all the cogs that deal with game commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from interactions import cog_ext # Used for slash commands
from gwendolyn.utils import get_params # pylint: disable=import-error
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()
@ -24,11 +24,6 @@ class GamesCog(commands.Cog):
"""Give another user an amount of GwendoBucks."""
await self.bot.money.giveMoney(ctx, user, amount)
@cog_ext.cog_slash(**params["invest"])
async def invest(self, ctx, parameters="check"):
"""Invest GwendoBucks in the stock market."""
await self.bot.games.invest.parseInvest(ctx, parameters)
@cog_ext.cog_slash(**params["trivia"])
async def trivia(self, ctx, answer=""):
"""Run a game of trivia."""

View File

@ -1,8 +1,8 @@
"""Contains the LookupCog, which deals with the lookup commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from interactions import cog_ext # Used for slash commands
from gwendolyn.utils import get_params # pylint: disable=import-error
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()

View File

@ -1,9 +1,9 @@
"""Contains the MiscCog, which deals with miscellaneous commands."""
from discord.ext import commands # Has the cog class
from discord_slash import cog_ext # Used for slash commands
from discord_slash.context import SlashContext
from interactions import cog_ext # Used for slash commands
from interactions import SlashContext
from gwendolyn.utils import get_params # pylint: disable=import-error
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()

View File

@ -1,8 +1,8 @@
"""Contains the StarWarsCog, which deals with Star Wars commands."""
from discord.ext import commands
from discord_slash import cog_ext
from interactions import cog_ext
from gwendolyn.utils import get_params # pylint: disable=import-error
from gwendolyn_old.utils import get_params # pylint: disable=import-error
params = get_params()

View File

@ -0,0 +1,11 @@
"""Exceptions for Gwendolyn"""
class GameNotInDatabase(Exception):
def __init__(self, game: str, channel: str):
self.message = f"There is no {game} game in channel {channel}"
super().__init__(self.message)
class InvalidInteraction(Exception):
def __init__(self, custom_id: str, decoded: str):
self.message = f"{custom_id = }, {decoded = }"
super().__init__(self.message)

View File

@ -0,0 +1,11 @@
"""A collection of all Gwendolyn functions."""
__all__ = ["Games", "Money", "LookupFuncs", "StarWars"]
from .games import Money, Games
from .lookup import LookupFuncs
from .other import Other
from .star_wars_funcs import StarWars

View File

@ -12,13 +12,13 @@ import math # Used for flooring decimal numbers
import datetime # Used to generate the game id
import asyncio # Used for sleeping
from discord_slash.context import InteractionContext as IntCont # Used for
from interactions import InteractionContext as IntCont # Used for
# typehints
from discord_slash.context import ComponentContext
from interactions import ComponentContext
from discord.abc import Messageable
from PIL import Image
from gwendolyn.utils import replace_multiple
from gwendolyn_old.utils import replace_multiple
from .game_base import CardGame, CardDrawer

View File

@ -15,13 +15,13 @@ import discord # Used for typehints, discord.file and to check whether
# the opponent in ConnectFour.start is a discord.User
from PIL import Image, ImageDraw, ImageFont # Used by DrawConnectFour()
from discord_slash.context import SlashContext, ComponentContext # Used for
from interactions import SlashContext, ComponentContext # Used for
# typehints
from discord_slash.utils.manage_components import (create_button,
from interactions.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from interactions.model import ButtonStyle
from gwendolyn.utils import encode_id
from gwendolyn_old.utils import encode_id
from .game_base import BoardGame
ROWCOUNT = 6

View File

@ -4,14 +4,13 @@ from typing import Union
from discord import File, User
from discord.abc import Messageable
from discord_slash.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.context import InteractionContext as IntCont
from interactions import Button, ActionRow
from interactions import InteractionContext as IntCont
from PIL import ImageFont, Image, ImageDraw
from gwendolyn.exceptions import GameNotInDatabase
from gwendolyn.utils import encode_id
from gwendolyn_old.exceptions import GameNotInDatabase
from gwendolyn_old.utils import encode_id
class GameBase():
"""The base class for the games."""
@ -29,7 +28,7 @@ class GameBase():
button_objects = []
for label, data, style in buttons:
custom_id = encode_id([self.game_name] + data)
button_objects.append(create_button(
button_objects.append(Button(
style=style,
label=label,
custom_id=custom_id
@ -37,7 +36,7 @@ class GameBase():
action_rows = []
for i in range(((len(button_objects)-1)//5)+1):
action_rows.append(create_actionrow(*button_objects[i*5:i*5+5]))
action_rows.append(ActionRow(*button_objects[i*5:i*5+5]))
return action_rows

View File

@ -8,7 +8,6 @@ Has a container for game functions.
"""
from .invest import Invest
from .trivia import Trivia
from .blackjack import Blackjack
from .connect_four import ConnectFour
@ -25,8 +24,6 @@ class Games():
------------
bot: Gwendolyn
The instance of Gwendolyn.
invest
Contains investment functions.
blackjack
Contains blackjack functions.
connect_four
@ -41,7 +38,6 @@ class Games():
"""Initialize the container."""
self.bot = bot
self.invest = Invest(bot)
self.trivia = Trivia(bot)
self.blackjack = Blackjack(bot)
self.connect_four = ConnectFour(bot)

View File

@ -16,14 +16,14 @@ import random # Used to draw poorly
import requests # Used for getting the word in Hangman.start()
import discord # Used for discord.file and type hints
from discord_slash.utils.manage_components import (create_button,
from interactions.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from discord_slash.context import SlashContext, ComponentContext
from interactions.model import ButtonStyle
from interactions import SlashContext, ComponentContext
# Used for typehints
from PIL import ImageDraw, Image, ImageFont # Used to draw the image
from gwendolyn.utils import encode_id
from gwendolyn_old.utils import encode_id
class Hangman():
"""

View File

@ -6,7 +6,7 @@ Contains the code that deals with money.
Money
Deals with money.
"""
import discord_slash # Used for typehints
import interactions # Used for typehints
import discord # Used for typehints
@ -17,9 +17,9 @@ class Money():
*Methods*
---------
checkBalance(user: str)
sendBalance(ctx: discord_slash.context.SlashContext)
sendBalance(ctx: interactions.SlashContext)
addMoney(user: str, amount: int)
giveMoney(ctx: discord_slash.context.SlashContext, user: discord.User,
giveMoney(ctx: interactions.SlashContext, user: discord.User,
amount: int)
*Attributes*
@ -58,13 +58,13 @@ class Money():
else:
return 0
async def sendBalance(self, ctx: discord_slash.context.SlashContext):
async def sendBalance(self, ctx: interactions.SlashContext):
"""
Get your own account balance.
*Parameters*
------------
ctx: discord_slash.context.SlashContext
ctx: interactions.SlashContext
The context of the command.
"""
await self.bot.defer(ctx)
@ -104,14 +104,14 @@ class Money():
self.database["users"].insert_one(new_user)
# Transfers money from one user to another
async def giveMoney(self, ctx: discord_slash.context.SlashContext,
async def giveMoney(self, ctx: interactions.SlashContext,
user: discord.User, amount: int):
"""
Give someone else money from your account.
*Parameters*
------------
ctx: discord_slash.context.SlashContext
ctx: interactions.SlashContext
The context of the command.
user: discord.User
The user to give money.

View File

@ -11,7 +11,7 @@ import json # Used to read data from api
import random # Used to shuffle answers
import asyncio # Used to sleep
from discord_slash.context import SlashContext # Used for type hints
from interactions import SlashContext # Used for type hints
class Trivia():

View File

@ -5,7 +5,7 @@ from PIL import Image, ImageDraw
from .game_base import DatabaseGame, BaseDrawer
from gwendolyn.exceptions import GameNotInDatabase
from gwendolyn_old.exceptions import GameNotInDatabase
IMAGE_MARGIN = 90
SQUARE_SIZE = 150

View File

@ -2,7 +2,7 @@ import math
import json
import discord
from gwendolyn.utils import cap
from gwendolyn_old.utils import cap
STATS = [
"strength",

View File

@ -0,0 +1,5 @@
"""Misc. functions for Gwendolyn."""
__all__ = ["Other"]
from .other import Other

View File

@ -0,0 +1,192 @@
import random # Used in movie_func
import datetime # Used in hello_func
import urllib # Used in image_func
import ast
import imdb # Used in movie_func
import discord # Used in movie_func
import lxml # Used in image_func
import fandom # Used in find_wiki_page
import d20 # Used in roll_dice
from .plex import Plex
from .nerd_shit import NerdShit
from .generators import Generators
fandom.set_lang("da")
fandom.set_wiki("senkulpa")
class MyStringifier(d20.MarkdownStringifier):
def _str_expression(self, node):
if node.comment is None:
result_text = "Result"
else:
result_text = node.comment.capitalize()
return f"**{result_text}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}"
class Other():
def __init__(self, bot):
self.bot = bot
self.plex = Plex(self.bot)
self.nerd_shit = NerdShit(self.bot)
self.generators = Generators(self.bot)
# Picks a random movie and returns information about it
async def movie_func(self, ctx):
await self.bot.defer(ctx)
self.bot.log("Creating IMDb object")
imdb_client = imdb.IMDb()
self.bot.log("Picking a movie")
with open("gwendolyn/resources/movies.txt", "r") as file_pointer:
movie_list = file_pointer.read().split("\n")
movie_name = random.choice(movie_list)
self.bot.log(f"Searching for {movie_name}")
search_result = imdb_client.search_movie(movie_name)
self.bot.log("Getting the data")
movie = search_result[0]
imdb_client.update(movie)
self.bot.log("Successfully ran /movie")
title = movie["title"]
plot = movie['plot'][0].split("::")[0]
cover = movie['cover url'].replace("150","600").replace("101","404")
cast = ", ".join([i["name"] for i in movie['cast'][:5]])
embed = discord.Embed(title=title, description=plot, color=0x24ec19)
embed.set_thumbnail(url=cover)
embed.add_field(name="Cast", value=cast,inline = True)
await ctx.send(embed = embed)
# Responds with a greeting of a time-appropriate maner
async def hello_func(self, ctx):
def time_in_range(start, end, i):
# Return true if i is in the range [start, end]
if start <= end:
return start <= i <= end
else:
return start <= i or i <= end
author = ctx.author.display_name
now = datetime.datetime.now()
if time_in_range(now.replace(hour=5, minute=0, second=0, microsecond=0),now.replace(hour=10, minute=0, second=0, microsecond=0), now):
send_message = "Good morning, "+str(author)
elif time_in_range(now.replace(hour=13, minute=0, second=0, microsecond=0),now.replace(hour=18, minute=0, second=0, microsecond=0), now):
send_message = "Good afternoon, "+str(author)
elif time_in_range(now.replace(hour=18, minute=0, second=0, microsecond=0),now.replace(hour=22, minute=0, second=0, microsecond=0), now):
send_message = "Good evening, "+str(author)
elif time_in_range(now.replace(hour=22, minute=0, second=0, microsecond=0),now.replace(hour=23, minute=59, second=59, microsecond=0), now):
send_message = "Good night, "+str(author)
else:
send_message = "Hello, "+str(author)
await ctx.send(send_message)
# Finds a random picture online
async def image_func(self, ctx):
# Picks a type of camera, which decides the naming scheme
cams = ("one","two","three","four")
cam = random.choice(cams)
self.bot.log("Chose cam type "+cam)
if cam == "one":
search = "img_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
elif cam == "two":
year = str(random.randint(2012,2016))
month = str(random.randint(1,12)).zfill(2)
day = str(random.randint(1,29)).zfill(2)
search = f"IMG_{year}{month}{day}"
elif cam == "three":
search = f"IMAG_{str(random.randint(1,500)).zfill(4)}"
elif cam == "four":
search = "DSC_" + ''.join(
[str(random.randint(0,9)) for _ in range(4)]
)
self.bot.log("Searching for "+search)
# Searches for the image and reads the resulting web page
page = urllib.request.urlopen("https://www.bing.com/images/search?q="+search+"&safesearch=off")
read = page.read()
tree = lxml.etree.HTML(read)
images = tree.xpath('//a[@class = "iusc"]/@m')
if len(images) == 0:
await ctx.send("Found no images")
else:
# Picks an image
number = random.randint(1,len(images))-1
image = ast.literal_eval(str(images[number]))
image_url = image["murl"]
self.bot.log("Picked image number "+str(number))
# Returns the image
self.bot.log("Successfully returned an image")
await ctx.send(image_url)
# Finds a page from the Senkulpa Wikia
async def find_wiki_page(self, ctx, search : str):
await self.bot.defer(ctx)
found_page = False
if search != "":
self.bot.log("Trying to find wiki page for "+search)
search_results = fandom.search(search)
if len(search_results) > 0:
found_page = True
search_result = search_results[0]
else:
self.bot.log("Couldn't find the page")
await ctx.send("Couldn't find page")
else:
found_page = True
self.bot.log("Searching for a random page")
search_result = fandom.random()
if found_page:
self.bot.log(f"Found page \"{search_result[0]}\"")
page = fandom.page(pageid = search_result[1])
content = page.summary
images = page.images
if len(images) > 0:
image = images[0]
else:
image = ""
self.bot.log("Sending the embedded message",str(ctx.channel_id))
content += f"\n[Læs mere]({page.url})"
embed = discord.Embed(title = page.title, description = content, colour=0xDEADBF)
if image != "":
embed.set_thumbnail(url=image)
await ctx.send(embed = embed)
async def roll_dice(self, ctx, roll_string):
user = ctx.author.display_name
while len(roll_string) > 1 and roll_string[0] == " ":
roll_string = roll_string[1:]
roll = d20.roll(roll_string, allow_comments=True, stringifier=MyStringifier())
await ctx.send(f"{user} :game_die:\n{roll}")
async def help_func(self, ctx, command):
if command == "":
with open("gwendolyn/resources/help/help.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = "Help", description = text,colour = 0x59f442)
await ctx.send(embed = embed)
else:
self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id))
with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as file_pointer:
text = file_pointer.read()
embed = discord.Embed(title = command.capitalize(), description = text,colour = 0x59f442)
await ctx.send(embed = embed)

View File

@ -7,11 +7,11 @@ import imdb
import discord
import xmltodict
from discord_slash.utils.manage_components import (create_button,
from interactions.utils.manage_components import (create_button,
create_actionrow)
from discord_slash.model import ButtonStyle
from interactions.model import ButtonStyle
from gwendolyn.utils import encode_id
from gwendolyn_old.utils import encode_id
class Plex():
"""Container for Plex functions and commands."""

View File

@ -0,0 +1,132 @@
"""
Contains the Gwendolyn class, a subclass of the discord command bot.
*Classes*
---------
Gwendolyn(discord.ext.commands.Bot)
"""
import os # Used for loading cogs in Gwendolyn.addCogs
import discord # Used for discord.Intents and discord.Status
import interactions # Used to initialized SlashCommands object
import git # Used to pull when stopping
from discord.ext import commands # Used to inherit from commands.bot
from pymongo import MongoClient # Used for database management
from gwendolyn_old.funcs import Money, StarWars, Games, Other, LookupFuncs
from gwendolyn_old.utils import (get_options, get_credentials, log_this,
DatabaseFuncs, EventHandler, ErrorHandler,
long_strings)
class Gwendolyn(commands.Bot):
"""
A multifunctional Discord bot.
*Methods*
---------
log(messages: Union[str, list], channel: str = "",
level: int = 20)
stop(ctx: interactions.SlashContext)
defer(ctx: interactions.SlashContext)
"""
# pylint: disable=too-many-instance-attributes
def __init__(self):
"""Initialize the bot."""
intents = discord.Intents.default()
intents.members = True
initiation_parameters = {
"command_prefix": " ",
"case_insensitive": True,
"intents": intents,
"status": discord.Status.dnd
}
super().__init__(**initiation_parameters)
self._add_clients_and_options()
self._add_util_classes()
self._add_function_containers()
self._add_cogs()
def _add_clients_and_options(self):
"""Add all the client, option and credentials objects."""
self.long_strings = long_strings()
self.options = get_options()
self.credentials = get_credentials()
mongo_user = self.credentials["mongo_db_user"]
mongo_password = self.credentials["mongo_db_password"]
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
database_clint = MongoClient(mongo_url)
if self.options["testing"]:
self.log("Testing mode")
self.database = database_clint["Gwendolyn-Test"]
else:
self.database = database_clint["Gwendolyn"]
def _add_util_classes(self):
"""Add all the classes used as utility."""
self.database_funcs = DatabaseFuncs(self)
self.event_handler = EventHandler(self)
self.error_handler = ErrorHandler(self)
slash_parameters = {
"sync_commands": True,
"sync_on_cog_reload": True,
"override_type": True
}
self.slash = interactions.SlashCommand(self, **slash_parameters)
def _add_function_containers(self):
"""Add all the function containers used for commands."""
self.star_wars = StarWars(self)
self.other = Other(self)
self.lookup_funcs = LookupFuncs(self)
self.games = Games(self)
self.money = Money(self)
def _add_cogs(self):
"""Load cogs."""
for filename in os.listdir("./gwendolyn/cogs"):
if filename.endswith(".py"):
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}")
def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use
"""Log a message. Described in utils/util_functions.py."""
log_this(messages, channel, level)
async def stop(self, ctx: interactions.SlashContext):
"""
Stop the bot, and stop running games.
Only stops the bot if the user in ctx.author is one of the
admins given in options.txt.
*parameters*
------------
ctx: interactions.SlashContext
The context of the "/stop" slash command.
"""
if f"#{ctx.author.id}" in self.options["admins"]:
await ctx.send("Pulling git repo and restarting...")
await self.change_presence(status=discord.Status.offline)
self.database_funcs.wipe_games()
if not self.options["testing"]:
git_client = git.cmd.Git("")
git_client.pull()
self.log("Logging out", level=25)
await self.close()
else:
log_message = f"{ctx.author.display_name} tried to stop me!"
self.log(log_message, str(ctx.channel_id))
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
async def defer(self, ctx: interactions.SlashContext):
"""Send a "Gwendolyn is thinking" message to the user."""
try:
await ctx.defer()
except interactions.error.AlreadyResponded:
self.log("defer failed")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Some files were not shown because too many files have changed in this diff Show More