Merge pull request #72 from stijndcl/flask-backend

Create basics for backend server, + small fixes
pull/74/head
Stijn De Clercq 2021-06-19 22:29:32 +02:00 committed by GitHub
commit a310d1696c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 357 additions and 97 deletions

5
.gitignore vendored
View File

@ -1,14 +1,11 @@
files/status.txt
files/readyMessage.txt
files/client.txt
files/lastTasks.json
files/c4.json
files/hangman.json
files/stats.json
files/lost.json
files/locked.json
files/database.json
files/ufora_notifications.json
.idea/
__pycache__
.env
/venv/

82
backend/server.py 100644
View File

@ -0,0 +1,82 @@
from discord.ext import ipc
from functions.database import custom_commands
import json
from quart import Quart, jsonify, request
from quart_cors import cors
from time import time
app = Quart(__name__)
# TODO allow_origin=re.compile(r"http://localhost:.*")
# needs higher Python & Quart version
app = cors(app, allow_origin="*")
app.config.from_object(__name__)
ipc_client = ipc.Client(secret_key="SOME_SECRET_KEY")
@app.route("/ping", methods=["GET"])
async def ping():
"""
Send a ping request, monitors bot latency, endpoint time, and PSQL latency
"""
latency = await ipc_client.request("get_bot_latency")
return jsonify({"bot_latency": latency, "response_sent": time()})
@app.route("/dm", methods=["POST"])
async def send_dm():
"""
Send a DM to the given user
"""
data = json.loads((await request.body).decode('UTF-8'))
dm = await ipc_client.request(
"send_dm",
user=int(data["userid"]),
message=data.get("message")
)
return jsonify({"response": dm})
@app.route("/custom", methods=["GET"])
async def get_all_custom_commands():
"""
Return a list of all custom commands in the bot
"""
commands = custom_commands.get_all()
return jsonify(commands)
@app.route("/custom/<command_id>")
async def get_custom_command(command_id):
try:
command_id = int(command_id)
except ValueError:
# Id is not an int
return unprocessable_entity("Parameter id was not a valid integer.")
command = custom_commands.get_by_id(command_id)
if command is None:
return page_not_found("")
return jsonify(command)
@app.errorhandler(404)
def page_not_found(e):
return jsonify({"error": "No resource could be found matching the given URL."}), 404
@app.errorhandler(422)
def unprocessable_entity(e):
return jsonify({"error": e}), 422
if __name__ == "__main__":
app.run()

View File

@ -5,6 +5,7 @@ from discord.ext import commands
from functions import checks, easterEggResponses
from functions.database import stats, muttn, custom_commands, commands as command_stats
import pytz
from settings import READY_MESSAGE, SANDBOX, STATUS_MESSAGE
import time
import traceback
@ -30,20 +31,13 @@ class Events(commands.Cog):
"""
Function called when the bot is ready & done leading.
"""
# Change status
with open("files/status.txt", "r") as statusFile:
status = statusFile.readline()
# Set status
await self.client.change_presence(status=discord.Status.online, activity=discord.Game(STATUS_MESSAGE))
await self.client.change_presence(status=discord.Status.online, activity=discord.Game(str(status)))
# Print a message in the terminal to show that he's ready
with open("files/readyMessage.txt", "r") as readyFile:
readyMessage = readyFile.readline()
print(readyMessage)
print(READY_MESSAGE)
# Add constants to the client as a botvar
self.client.constants = constants.Live if "zandbak" not in readyMessage else constants.Zandbak
self.client.constants = constants.Live if SANDBOX else constants.Zandbak
@commands.Cog.listener()
async def on_message(self, message):

26
cogs/ipc.py 100644
View File

@ -0,0 +1,26 @@
from discord.ext import commands, ipc
class IPC(commands.Cog):
def __init__(self, client):
self.client = client
@ipc.server.route()
async def send_dm(self, data):
print("got here")
user = self.client.get_user(data.user)
await user.send(data.message)
print("sent")
return True
@ipc.server.route()
async def get_bot_latency(self, data):
"""
Get Didier's latency
"""
return self.client.latency * 1000
def setup(client):
client.add_cog(IPC(client))

View File

@ -246,7 +246,7 @@ class Tasks(commands.Cog):
Task that checks for new Ufora announcements every few minutes
"""
# Don't run this when testing
if self.client.user.id == int(constants.coolerDidierId):
if self.client.user.id != int(constants.didierId):
return
# Get new notifications

View File

@ -15,8 +15,8 @@ class Translate(commands.Cog):
def cog_check(self, ctx):
return not self.client.locked
# @commands.command(name="Translate", aliases=["Tl", "Trans"], usage="[Tekst] [Van]* [Naar]*")
# @help.Category(Category.Words)
@commands.command(name="Translate", aliases=["Tl", "Trans"], usage="[Tekst] [Van]* [Naar]*")
@help.Category(Category.Words)
async def translate(self, ctx, query=None, to="nl", fr="auto"):
if query is None:
return await ctx.send("Controleer je argumenten.")
@ -39,9 +39,11 @@ class Translate(commands.Cog):
embed.set_author(name="Didier Translate")
if fr == "auto":
language = translation.extra_data["original-language"]
language = translation.src
embed.add_field(name="Gedetecteerde taal", value=tc(LANGUAGES[language]))
embed.add_field(name="Zekerheid", value="{}%".format(translation.extra_data["confidence"] * 100))
if translation.extra_data["confidence"] is not None:
embed.add_field(name="Zekerheid", value="{}%".format(translation.extra_data["confidence"] * 100))
embed.add_field(name="Origineel ({})".format(translation.src.upper()), value=query, inline=False)
embed.add_field(name="Vertaling ({})".format(to.upper()), value=translation.text)

View File

@ -1,33 +1,19 @@
import discord
from discord.ext import commands
from dotenv import load_dotenv
from functions.prefixes import get_prefix
import os
from settings import TOKEN
from startup.didier import Didier
if __name__ == "__main__":
load_dotenv(verbose=True)
load_dotenv(verbose=True)
# Configure intents (1.5.0)
intents = discord.Intents.default()
intents.members = True
client = Didier(command_prefix=get_prefix, case_insensitive=True, intents=intents)
# Configure intents (1.5.0)
intents = discord.Intents.default()
intents.members = True
if client.ipc is not None:
client.ipc.start()
client = commands.Bot(command_prefix=get_prefix, case_insensitive=True, intents=intents)
# Remove default help because it sucks & I made my own
client.remove_command("help")
# Load utils first so it can be used in other places & it's not None
client.load_extension("cogs.utils")
client.load_extension("cogs.failedchecks")
client.load_extension("cogs.events")
# Load all remaining cogs
for file in os.listdir("./cogs"):
if file.endswith(".py") and not (file.startswith(("utils", "failedchecks", "events"),)):
client.load_extension("cogs.{}".format(file[:-3]))
# Get the token out of the file & run the bot
with open("files/client.txt", "r") as fp:
token = fp.readline()
client.run(token)
client.run(TOKEN)

15
faq.md
View File

@ -1,5 +1,14 @@
# FAQ
Answers to Frequently Asked Questions.
Answers to Frequently Asked Questions and solutions to issues that may arise.
### Table of Contents
A list of all questions (in order) so you can easily find what you're looking for.
## Issues installing dotenv
The name of this package is `python-dotenv`, as listed in `requirements.txt`. The _name_ of the package, however, is just `dotenv`. This confuses PyCharm, which will tell you that you don't have `dotenv` installed as it can't link those two together. You can just click `ignore this requirement` if you don't like the warning.
## Issues installing psycopg2
The `psychopg2` and `psychopg2-binary` packages might cause you some headaches when trying to install them, especially when using PyCharm to install dependencies. The reason for this is because these are `PSQL` packages, and as a result they require you to have `PSQL` installed on your system to function properly.
## Package is not listed in project requirements
This is the exact same case as [Issues installing dotenv](#issues-installing-dotenv), and occurs for packages such as `Quart`. The names of the modules differ from the name used to install it from `pip`. Once again, you can ignore this message.

View File

@ -0,0 +1,5 @@
{
"guessed": [],
"guesses": 0,
"word": ""
}

View File

@ -0,0 +1,9 @@
{
"interest": 0,
"lost": 0,
"poke": 0,
"prison": 0,
"birthdays": 0,
"channels": 0,
"remind": 0
}

View File

@ -0,0 +1,4 @@
{
"locked": false,
"until": -1
}

View File

@ -0,0 +1,4 @@
{
"lost": 0,
"today": 0
}

View File

@ -0,0 +1,19 @@
{
"cf": {
"h": 0,
"t": 0
},
"dice": {
"2": 0,
"5": 0,
"3": 0,
"6": 0,
"1": 0,
"4": 0
},
"rob": {
"robs_success": 0,
"robs_failed": 0,
"bail_paid": 0.0
}
}

View File

@ -0,0 +1,12 @@
{
"Algoritmen en Datastructuren 2": [],
"Communicatienetwerken": [],
"Computerarchitectuur": [],
"Functioneel Programmeren": [],
"Multimedia": [],
"Software Engineering Lab 1": [],
"Statistiek en Probabiliteit": [],
"Systeemprogrammeren": [],
"Webdevelopment": [],
"Wetenschappelijk Rekenen": []
}

View File

@ -127,3 +127,55 @@ def add_alias(command: str, alias: str):
cursor.execute("INSERT INTO custom_command_aliases(command, alias) VALUES(%s, %s)", (command_id, alias,))
connection.commit()
def get_all():
"""
Return a list of all registered custom commands
"""
connection = utils.connect()
cursor = connection.cursor()
cursor.execute("SELECT * FROM custom_commands")
commands = cursor.fetchall()
ret = []
# Create a list of all entries
for command in commands:
dic = {"id": command[0], "name": command[1], "response": command[2]}
# Find and add aliases
cursor.execute("SELECT id, alias FROM custom_command_aliases WHERE command = %s", (command[0],))
aliases = cursor.fetchall()
if aliases:
dic["aliases"] = list(map(lambda x: {"id": x[0], "alias": x[1]}, aliases))
ret.append(dic)
return ret
def get_by_id(command_id: int):
"""
Return a command that matches a given id
"""
connection = utils.connect()
cursor = connection.cursor()
cursor.execute("SELECT * FROM custom_commands WHERE id = %s", (command_id,))
command = cursor.fetchone()
# Nothing found
if not command:
return None
dic = {"id": command[0], "name": command[1], "response": command[2]}
cursor.execute("SELECT id, alias FROM custom_command_aliases WHERE command = %s", (command_id,))
aliases = cursor.fetchall()
if aliases:
dic["aliases"] = list(map(lambda x: {"id": x[0], "alias": x[1]}, aliases))
return dic

View File

@ -1,6 +1,5 @@
import psycopg2
import json
import os
from settings import DB_HOST, DB_NAME, DB_USERNAME, DB_PASSWORD
connection = None
@ -17,15 +16,11 @@ def connect():
def create_connection():
global connection
dir_path = os.path.dirname(os.path.realpath(__file__))
with open(os.path.join(dir_path, "../../files/database.json"), "r") as fp:
db = json.load(fp)
connection = psycopg2.connect(
host=db["host"],
database=db["database"],
user=db["username"],
password=db["password"]
host=DB_HOST,
database=DB_NAME,
user=DB_USERNAME,
password=DB_PASSWORD
)

View File

@ -2,28 +2,7 @@
A list of all ignored files with copy-pastable templates. Useful for when you want to work on commands that use these, for obvious reasons. Every file has a copy-pastable template to make it easy for you to use.
These are usually files which would be overkill to make a PSQL table for. Other possibilities are files that are never edited, but should be different on every machine (Discord token, status message, ...).
### files/client.txt
Contains the application's token to connect to Discord. You can create your own bot & put it's token in this file to run & test Didier code.
token_goes_here
### files/database.json
Contains the credentials needed to connect to the PSQL database. This is ignored so that I don't have to leak my IP address, but also so that you can set up a local database to mess around without affecting the Live one or having to change any code.
```json
{
"username": "username",
"password": "password",
"host": "host_address",
"database": "database_name"
}
```
When connecting to a local PSQL database, `host` should be `"localhost"`.
These are usually files which would be overkill to make a PSQL table for. Other possibilities are files that are never edited, but should be different on every machine.
### files/hangman.json
@ -64,14 +43,6 @@ Contains a boolean indicating whether or not the server is currently locked, and
}
```
### files/readyMessage.txt
Contains the message printed in your terminal when Didier is ready.
I'M READY I'M READY I'M READY I'M READY
In case you were wondering: yes, this is a Spongebob reference.
### files/stats.json
Contains the stats to track for gambling games. Weren't made as a PSQL table because they would be too long (and every game is different).
@ -98,12 +69,6 @@ Contains the stats to track for gambling games. Weren't made as a PSQL table bec
}
```
### files/status.txt
Contains Didier's status message for when he logs in. Keep in mind that his activity is set to `Playing `. This was first used in Didier V1 to show whether or not he was in sandbox mode.
with your Didier Dinks.
### files/ufora_notifications.json
Stores ID's of all received Ufora notifications.

View File

@ -51,13 +51,16 @@ When creating a new Didier command, you can add it to a `Category` by adding a d
```python
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
@commands.command(name="Command Name", aliases=["Cn"])
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def command_name(self, ctx):
# Command code
await ctx.send("Command response")
```
This allows commands across multiple Cogs to be classified under the same category in the help page.

View File

@ -1,6 +1,8 @@
python-dotenv==0.14.0
beautifulsoup4==4.9.1
discord.py==1.7.0
discord.py==1.7.3
git+https://github.com/Rapptz/discord-ext-menus@master
discord-ext-ipc==2.0.0
psycopg2==2.8.5
psycopg2-binary==2.8.5
python-dateutil==2.6.1
@ -10,4 +12,6 @@ requests-unixsocket==0.1.5
tabulate==0.8.7
yarl==1.4.2
feedparser==6.0.2
googletrans==3.0.0
googletrans==4.0.0rc1
quart==0.6.15
Quart-CORS==0.1.3

33
settings.py 100644
View File

@ -0,0 +1,33 @@
from dotenv import load_dotenv
import os
load_dotenv()
def _to_bool(value: str) -> bool:
"""
Env variables are strings so this converts them to booleans
"""
return value.lower() in ["true", "1", "y", "yes"]
# Sandbox or live
SANDBOX = _to_bool(os.getenv("SANDBOX", "true"))
# Tokens & API keys
URBANDICTIONARY = os.getenv("URBANDICTIONARY", "")
IMGFLIP_NAME = os.getenv("IMGFLIPNAME", "")
IMGFLIP_PASSWORD = os.getenv("IMGFLIPPASSWORD", "")
# Database credentials
DB_USERNAME = os.getenv("DBUSERNAME", "")
DB_PASSWORD = os.getenv("DBPASSWORD", "")
DB_HOST = os.getenv("DBHOST", "")
DB_NAME = os.getenv("DBNAME", "")
# Discord-related
TOKEN = os.getenv("TOKEN", "")
HOST_IPC = _to_bool(os.getenv("HOSTIPC", "false"))
READY_MESSAGE = os.getenv("READYMESSAGE", "I'M READY I'M READY I'M READY I'M READY") # Yes, this is a Spongebob reference
STATUS_MESSAGE = os.getenv("STATUSMESSAGE", "with your Didier Dinks.")

View File

46
startup/didier.py 100644
View File

@ -0,0 +1,46 @@
from discord.ext import commands, ipc
from settings import HOST_IPC
from startup.init_files import check_all
import os
class Didier(commands.Bot):
"""
Main Bot class for Didier
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._host_ipc = HOST_IPC
# IPC Server
# TODO secret key
self.ipc = ipc.Server(self, secret_key="SOME_SECRET_KEY") if self._host_ipc else None
# Cogs that should be loaded before the others
self._preload = ("ipc", "utils", "failedchecks", "events",)
# Remove default help command
self.remove_command("help")
# Load all extensions
self.init_extensions()
# Check missing files
check_all()
def init_extensions(self):
# Load initial extensions
for ext in self._preload:
self.load_extension(f"cogs.{ext}")
# Load all remaining cogs
for file in os.listdir("./cogs"):
if file.endswith(".py") and not (file.startswith(self._preload)):
self.load_extension("cogs.{}".format(file[:-3]))
async def on_ipc_ready(self):
print("IPC server is ready.")
async def on_ipc_error(self, endpoint, error):
print(endpoint, "raised", error)

View File

@ -0,0 +1,13 @@
import json
from os import path
def check_all():
files = ["hangman", "lastTasks", "locked", "lost", "stats", "ufora_notifications"]
for f in files:
if not path.isfile(path.join(f"files/{f}.json")):
with open(f"files/{f}.json", "w+") as new_file, open(f"files/default/{f}.json", "r") as default:
content = json.load(default)
json.dump(content, new_file)
print(f"Created missing file: files/{f}.json")