From 37ac7dbb0cfd2077af42b353584465607d6f307d Mon Sep 17 00:00:00 2001
From: VD15 <valkyriedev15@gmail.com>
Date: Thu, 29 May 2025 13:27:56 +0100
Subject: [PATCH] Add multi-env support

---
 .gitignore    |  5 +--
 bot/config.py | 21 ++++++++++-
 readme.md     | 99 +++++++++++++++++++++++++++++++++++++++++++++++----
 setup_db.py   | 34 ++++++++++++++++--
 4 files changed, 147 insertions(+), 12 deletions(-)

diff --git a/.gitignore b/.gitignore
index 960a84d..5b5cba0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -181,5 +181,6 @@ cython_debug/
 .cursorindexingignore
 
 # Custom stuff
-gacha_game.db
-config.ini
+gacha_game*.db
+gacha_game*.db.*
+config*.ini
diff --git a/bot/config.py b/bot/config.py
index 643aeb1..16b2f1f 100644
--- a/bot/config.py
+++ b/bot/config.py
@@ -1,7 +1,26 @@
 '''Essentials for the bot to function'''
 import configparser
+from os import environ, path
+
+class ConfigError(Exception):
+    '''Could not find config file'''
+
+def get_config_file() -> str:
+    '''Gets the path to the config file in the current environment'''
+    env: str | None = environ.get('KEMOVERSE_ENV')
+    if not env:
+        raise ConfigError('Error: KEMOVERSE_ENV is unset')
+    if not (env in ['prod', 'dev']):
+        raise ConfigError(f'Error: Invalid environment: {env}')
+
+    config_path: str = f'config_{env}.ini'
+
+    if not path.isfile(config_path):
+        raise ConfigError(f'Could not find {config_path}')
+    return config_path
+
 config = configparser.ConfigParser()
-config.read('config.ini')
+config.read(get_config_file())
 
 # Username for the bot
 USER     = config['credentials']['User']
diff --git a/readme.md b/readme.md
index cf4470c..68a5985 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,10 @@
 # Kemoverse
 
 A gacha-style bot for the Fediverse built with Python. Users can roll for characters, trade, duel, and perhaps engage with popularity-based mechanics. Currently designed for use with Misskey. Name comes from Kemonomimi and Fediverse.
+=======
+## Installation
+
+## Roadmap
 
 ![Fediverse Gacha Bot Logo](./web/static/logo.png)
 
@@ -11,18 +15,23 @@ A gacha-style bot for the Fediverse built with Python. Users can roll for charac
 - ๐ŸŽด Cards stats system
 - ๐Ÿง  Core database structure for characters and stats
 - ๐Ÿ“ฆ Basic support for storing pulls per user
+- โฑ๏ธ Time-based limitations on rolls
 
 ### ๐Ÿงฉ In Progress
 - ๐Ÿ“ Whitelist system to limit access
-- โฑ๏ธ Time-based limitations on rolls
-- โš”๏ธ Dueling system
+- โš ๏ธ  Explicit account creation/deletion
 
-## ๐Ÿง  Planned Features (Long Term)
+## ๐Ÿง  Roadmap
+
+[See our v2.0 board for more details](https://git.waifuism.life/waifu/kemoverse/projects/3)
 
 ### ๐Ÿ›’ Gameplay & Collection
 - ๐Ÿ” **Trading system** between users
 - โญ **Favorite characters** (pin them or set profiles)
 - ๐Ÿ“ข **Public post announcements** for rare card pulls
+- ๐Ÿ“Š **Stats** for cards
+- ๐ŸŽฎ **Games** to play
+  - โš”๏ธ Dueling
 - ๐Ÿงฎ **Leaderboards**
   - Most traded Characters
   - Most owned Characters
@@ -39,7 +48,7 @@ A gacha-style bot for the Fediverse built with Python. Users can roll for charac
 
 ## ๐Ÿ—ƒ๏ธ Tech Stack
 
-- Python (3.11+)
+- Python (3.12+)
 - SQLite
 - Fediverse API integration (via Misskey endpoints)
 - Flask
@@ -49,10 +58,88 @@ A gacha-style bot for the Fediverse built with Python. Users can roll for charac
 
 The bot is meant to feel *light, fun, and competitive*. Mixing social, gacha and duel tactics.
 
-## ๐Ÿงช Getting Started (coming soon)
+## ๐Ÿงช Installation
 
-Instructions on installing dependencies, initializing the database, and running the bot locally will go here.
+1. Download and install dependencies
 
+Clone the repo
+
+```sh
+git clone https://git.waifuism.life/waifu/kemoverse.git
+cd kemoverse
+```
+
+Setup a virtual environment (Optional, recommended)
+
+```sh
+python3 -m venv venv
+source venv/bin/activate
+```
+
+Install project dependencies via pip
+
+```sh
+python3 -m pip install -r requirements.txt
+```
+
+2. Setup config file
+
+A sample config file is included with the project as a template: `example_config.ini`
+
+Create a copy of this file and replace its' values with your own. Consult the
+template for more information about individual config values and their meaning.
+
+Config files are environment-specific. Use `config_dev.ini` for development and
+`config_prod.ini` for production. Switch between environments using the
+`KEMOVERSE_ENV` environment variable.
+
+```sh
+cp example_config.ini config_dev.ini
+# Edit config_dev.ini
+```
+
+4. Setup database
+
+To set up the database, run:
+
+```sh
+KEMOVERSE_ENV=dev python3 setup_db.py
+```
+
+5. Run the bot
+
+```sh
+KEMOVERSE_ENV=dev ./startup.sh
+```
+
+If all goes well, you should now be able to interact with the bot.
+
+6. Running in production
+
+To run the the in a production environment, use `KEMOVERSE_ENV=prod`. You will
+also need to create a `config_prod.ini` file and run the database setup step
+again if pointing prod to a different database. (you are pointing dev and prod
+to different databases, right? ๐Ÿคจ)
+
+7. Updating
+
+To update the bot, first pull new changes from upstream:
+
+```sh
+git pull
+```
+
+Then run any database migrations. We recommend testing in dev beforehand to
+make sure nothing breaks in the update process.
+
+**Always backup your prod database before running any migrations!**
+
+```sh
+# Backup database file
+cp gacha_game_dev.db gacha_game_dev.db.bak
+# Run migrations
+KEMOVERSE_ENV=dev python3 setup_db.py
+```
 
 ```mermaid
 flowchart TD
diff --git a/setup_db.py b/setup_db.py
index fbb264e..241bb4e 100644
--- a/setup_db.py
+++ b/setup_db.py
@@ -11,6 +11,12 @@ class DBNotFoundError(Exception):
 class InvalidMigrationError(Exception):
     '''Migration file has an invalid name'''
 
+class KemoverseEnvUnset(Exception):
+    '''KEMOVERSE_ENV is not set or has an invalid value'''
+
+class ConfigError(Exception):
+    '''Could not find the config file for the current environment'''
+
 def get_migrations() -> List[Tuple[int, str]] | InvalidMigrationError:
     '''Returns a list of migration files in numeric order.'''
     # Store transaction id and filename separately
@@ -50,11 +56,22 @@ def perform_migration(cursor: sqlite3.Cursor, migration: tuple[int, str]) -> Non
 
 def get_db_path() -> str | DBNotFoundError:
     '''Gets the DB path from config.ini'''
+    env = os.environ.get('KEMOVERSE_ENV')
+    if not (env and env in ['prod', 'dev']):
+        raise KemoverseEnvUnset
+
+    print(f'Running in "{env}" mode')
+
+    config_path = f'config_{env}.ini'
+
+    if not os.path.isfile(config_path):
+        raise ConfigError(f'Could not find {config_path}')
+
     config = ConfigParser()
-    config.read('config.ini')
+    config.read(config_path)
     db_path = config['application']['DatabaseLocation']
     if not db_path:
-        raise DBNotFoundError
+        raise DBNotFoundError()
     return db_path
 
 def get_current_migration(cursor: sqlite3.Cursor) -> int:
@@ -71,7 +88,18 @@ def get_current_migration(cursor: sqlite3.Cursor) -> int:
 def main():
     '''Does the thing'''
     # Connect to the DB
-    db_path = get_db_path()
+    db_path = ''
+    try:
+        db_path = get_db_path()
+    except ConfigError as ex:
+        print(ex)
+        return
+    except KemoverseEnvUnset:
+        print('Error: KEMOVERSE_ENV is either not set or has an invalid value.')
+        print('Please set KEMOVERSE_ENV to either "dev" or "prod" before running.')
+        print(traceback.format_exc())
+        return
+
     conn = sqlite3.connect(db_path, autocommit=False)
     conn.row_factory = sqlite3.Row
     cursor = conn.cursor()