No content has been defined for this page.
+ + {% endblock %} +diff --git a/.gitignore b/.gitignore index b006529..e808aad 100644 --- a/.gitignore +++ b/.gitignore @@ -175,4 +175,5 @@ cython_debug/ #.idea/ # ---> StarfallBot -config.toml \ No newline at end of file +config.toml +web/static/style/*.scss \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..67122e9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "configurations": [ + { + "console": "integratedTerminal", + "name": "Python: Current File", + "program": "${workspaceFolder}/app.py", + "request": "launch", + "type": "debugpy" + } + ], + "version": "0.2.0" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8084b5b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "always", + "source.organizeImports.ruff": "always", + "source.sort.json": "always" + }, + "python.languageServer": "None", + "scss-to-css-compile.browsers": [ + "ie > 9", + "iOS > 8", + "Android >= 4.4", + "ff > 38", + "Chrome > 38" + ], + "scss-to-css-compile.compileOnSave": false, + "scss-to-css-compile.outDir": "./", + "scss-to-css-compile.output": "compressed" +} \ No newline at end of file diff --git a/app.py b/app.py index 089f7aa..50e8fd7 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,32 @@ +import logging +import os +from threading import Thread + from starfall.config import Config +from starfall.log import Log +from starfall.web import App if __name__ == "__main__": + Log.setup() + try: Config.load() except FileNotFoundError: - pass + dst = Config.create() + logging.getLogger().critical( + msg="\n".join( + [ + "*** Config file was not found ***", + "", + "Sample config.toml file has been created in the project's root directory:", + "[b]" + os.path.realpath("./%s" % dst) + "[/b]", + "Edit it and run the program again.", + ] + ), + extra={"markup": True}, + ) + exit() + + App.__init__() + + threads = [Thread(App.run())] diff --git a/config-sample.toml b/config-sample.toml index 302e007..0c028b9 100644 --- a/config-sample.toml +++ b/config-sample.toml @@ -1,12 +1,17 @@ # == StarfallBot configuration file == [web] +# What IP the web server is bound to. +# Use "localhost" until the bot is production ready. +host = "localhost" # What port the web server uses. port = 5000 # Connection database url. # sqlite is only production safe to a point - consult the SQLAlchemy # documentation to see how to set up other databases. database_url = "sqlite:///starfallbot.db" +# Secret key, used to sign the session cookie. +secret_key = "PRIVATE SECRET KEY CHANGEME" [discord] token = "YOUR BOT TOKEN HERE" diff --git a/requirements.txt b/requirements.txt index df0de29..e6f4e24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ +discord-py-interactions flask flask-sqlalchemy -discord-py-interactions +rich twitchio \ No newline at end of file diff --git a/starfall/config.py b/starfall/config.py index 74e4b5f..2f812c1 100644 --- a/starfall/config.py +++ b/starfall/config.py @@ -1,14 +1,24 @@ import os +from shutil import copy +from typing import Any + import tomllib -from typing import Annotated, Optional class Config: - data: Annotated[Optional[dict], - "Configuration imported from config.toml"] = None + data: dict[str, Any] = dict() + """Configuration imported from config.toml.""" @classmethod - def load(cls): + def create(cls) -> str: + """Copy config-sample.toml file to config.toml. + + Does not alert the user to the modification. + """ + return copy(src="config-sample.toml", dst="config.toml") + + @classmethod + def load(cls) -> None: """Loads the contents of the config.toml file into the class. Raises: @@ -16,14 +26,23 @@ class Config: """ if not cls.file_exists(): raise FileNotFoundError("config.toml does not exist") - with open("config.toml", "r") as f: + with open("config.toml", "rb") as f: cls.data = tomllib.load(f) @classmethod def file_exists(cls) -> bool: """Whether the config.toml file exists or needs to be created. - + Returns: bool: True if path exists and is a file, False otherwise. """ return os.path.exists("config.toml") and os.path.isfile("config.toml") + + @classmethod + def get(cls, key: str, default: Any): + """Get config key, if it exists, or return a default. + + Returns: + Any: Result, or default if key does not exist. + """ + return cls.data.get(key, default) diff --git a/starfall/db/__init__.py b/starfall/db/__init__.py new file mode 100644 index 0000000..2e1eeb6 --- /dev/null +++ b/starfall/db/__init__.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/starfall/log.py b/starfall/log.py new file mode 100644 index 0000000..02d4d5c --- /dev/null +++ b/starfall/log.py @@ -0,0 +1,21 @@ +import logging + +import werkzeug +from rich.logging import RichHandler + + +class Log: + @classmethod + def setup(cls) -> None: + handler = RichHandler(level=logging.DEBUG, rich_tracebacks=True) + logging.basicConfig( + level=logging.DEBUG, + format="%(message)s", + datefmt="[%Y-%m-%d %H:%M:%S]", + handlers=[handler], + ) + + # NOTE: pyright's reportPrivateUsage is suppressed here. + # We disable ANSI terminal codes from being added to Werkzeug (Flask) logs; + # RichHandler already handles all the fancy color stuff. + werkzeug.serving._log_add_style = False # pyright: ignore[reportPrivateUsage] diff --git a/starfall/web/__init__.py b/starfall/web/__init__.py new file mode 100644 index 0000000..3621aba --- /dev/null +++ b/starfall/web/__init__.py @@ -0,0 +1,36 @@ +import os +from typing import Any + +from flask import Flask + +from starfall.config import Config +from starfall.db import db +from starfall.web.home import home_blueprint + + +class App: + app: Flask = Flask(__name__) + config: dict[str, Any] = dict() # pyright: ignore[reportExplicitAny] + + @classmethod + def __init__(cls): + cls.load_config() + cls.app.config.update( # pyright: ignore[reportUnknownMemberType] + SECRET_KEY=cls.config.get("secret_key"), + SQLALCHEMY_DATABASE_URI=cls.config.get("database_url"), + ) + + cls.app.root_path = os.path.realpath(".") + cls.app.static_folder = os.path.realpath("./web/static") + cls.app.template_folder = "web" + + db.init_app(cls.app) + cls.app.register_blueprint(home_blueprint) + + @classmethod + def load_config(cls): + cls.config = Config.get("web", {}) + + @classmethod + def run(cls): + cls.app.run(host=cls.config.get("host"), port=cls.config.get("port")) diff --git a/starfall/web/home.py b/starfall/web/home.py new file mode 100644 index 0000000..c3a6ff5 --- /dev/null +++ b/starfall/web/home.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template + +home_blueprint: Blueprint = Blueprint("main", __name__) + + +@home_blueprint.route("/") +def index(): + return render_template("home.html") diff --git a/web/home.html b/web/home.html new file mode 100644 index 0000000..0dc5321 --- /dev/null +++ b/web/home.html @@ -0,0 +1,11 @@ +{% extends "index.html" %} + +{% block titleSuffix %}Overview{% endblock %} + +{% block body %} + +
No content has been defined for this page.
+ + {% endblock %} +