Skip to content

Commit

Permalink
ci cd , record games and players to db (#5)
Browse files Browse the repository at this point in the history
* fixing deploy ci/cd

* SQLITE_DB_FILE_PATH in ci/cd

* Poll retract handling

* playrooms and games recorded to db
  • Loading branch information
Alex-Kopylov authored Feb 6, 2024
1 parent 4d3e8e3 commit b38aa4b
Show file tree
Hide file tree
Showing 18 changed files with 221 additions and 51 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
.venv
venv
6 changes: 6 additions & 0 deletions .github/workflows/action.yml → .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
steps:
- name: SSH into production server and deploy
uses: appleboy/ssh-action@master
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.PROD_TELEGRAM_BOT_TOKEN }}
DEVELOPER_CHAT_ID: ${{ secrets.DEVELOPER_CHAT_ID }}
SQLITE_DB_FILE_PATH: ${{ vars.SQLITE_DB_FILE_PATH }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
Expand All @@ -39,3 +43,5 @@ jobs:
cd ./_work/${{ env.REPO_NAME }}/${{ env.REPO_NAME }}
docker compose down
docker compose up -d
docker compose ps
envs: TELEGRAM_BOT_TOKEN,DEVELOPER_CHAT_ID,SQLITE_DB_FILE_PATH
47 changes: 47 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI/CD with Docker Compose
env:
REPO_NAME: ${{ github.event.repository.name }}

on:
push:
branches:
- develop

jobs:
build:
runs-on: [self-hosted]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
key: ${{ secrets.PRIVATE_KEY }}
script: |
cd ./_work/${{ env.REPO_NAME }}/${{ env.REPO_NAME }}
docker compose build --no-cache
deploy:
needs: [build]
runs-on: [self-hosted]
steps:
- name: SSH into production server and deploy
uses: appleboy/ssh-action@master
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.DEV_TELEGRAM_BOT_TOKEN }}
DEVELOPER_CHAT_ID: ${{ secrets.DEVELOPER_CHAT_ID }}
SQLITE_DB_FILE_PATH: ${{ vars.SQLITE_DB_FILE_PATH }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
key: ${{ secrets.PRIVATE_KEY }}
script: |
cd ./_work/${{ env.REPO_NAME }}/${{ env.REPO_NAME }}
docker compose down
docker compose up -d
docker compose ps
envs: TELEGRAM_BOT_TOKEN,DEVELOPER_CHAT_ID,SQLITE_DB_FILE_PATH
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ To host your own instance of the bot, follow these steps:
```dotenv
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
DEVELOPER_CHAT_ID=your_chat_id_for_getting_logs_and_errors# optional
SQLITE_DB_FILE=database/db.sqlite# or any other path that you prefer
SQLITE_DB_FILE_PATH=db.sqlite# or any other path that you prefer
```

Please note that you need to create a new bot using the [BotFather](https://t.me/BotFather) and get the token for
Expand Down
14 changes: 9 additions & 5 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ services:
bot:
build: .
environment:
- SQLITE_DB_FILE=/database/db.sqlite
# - PYTHONPATH=/bot
- SQLITE_DB_FILE_PATH=${SQLITE_DB_FILE_PATH}
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- DEVELOPER_CHAT_ID=${DEVELOPER_CHAT_ID}
volumes:
- sqlite_data:/database
entrypoint: ["/bin/sh", "-c"]
command:
- python -m sqlite3 ${SQLITE_DB_FILE} < database/init.sql
entrypoint: python src/__main__.py
- |
python -m sqlite3 ${SQLITE_DB_FILE_PATH} < src/sql/init.sql
python src/__main__.py
restart: always

sqlitebrowser:
Expand All @@ -20,12 +23,13 @@ services:
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- TZ=GMT+4
- ENABLE_UWSGI=true
volumes:
- sqlite_data:/database
profiles:
- sqlitebrowser
restart: unless-stopped

volumes:
sqlite_data:
17 changes: 7 additions & 10 deletions src/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
import logging
import traceback

from telegram import (
Update,
)
from telegram.ext import (
Application,
)
Expand All @@ -17,27 +14,27 @@
from src.db import close_db
from src.get_handlers import get_handlers

logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger()


def main() -> None:
if not config.TELEGRAM_BOT_TOKEN:
raise ValueError("TELEGRAM_BOT_TOKEN env variable" "wasn't porpoused.")
raise ValueError("TELEGRAM_BOT_TOKEN env variable" "wasn't purposed.")

application = Application.builder().token(config.TELEGRAM_BOT_TOKEN).build()
application.add_handlers(get_handlers())
application.add_error_handler(handlers.error_handler)
# send a message to the developer when the bot is ready
application.run_polling(allowed_updates=Update.ALL_TYPES)
application.run_polling()


if __name__ == "__main__":
try:
main()
except Exception:
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger()
logger.warning(traceback.format_exc())
finally:
close_db()
31 changes: 25 additions & 6 deletions src/callbacks/receive_poll_answer.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import logging

from telegram import Update
from telegram.ext import ContextTypes

from src.data_models.Player import Player
from src.services.db_service import save_player


async def receive_poll_answer(
update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
# TODO save polls to db and query them instead saving them to bot_data
"""Summarize a users poll vote"""
answer = update.poll_answer
if not answer.option_ids:
return # It's retake poll action, ignore it
if not answer.option_ids: # Poll retract, delete previous vote
del context.bot_data[answer.poll_id]["results"][update.effective_user.id]
return
if context.bot_data:
answered_poll = context.bot_data[answer.poll_id]
user_id = update.effective_user.id
result = answered_poll["questions"][answer.option_ids[0]]
context.bot_data[answer.poll_id]["results"][user_id] = result
await save_player(
Player(
telegram_user_id=user_id,
username=update.effective_user.username,
first_name=update.effective_user.first_name,
full_name=update.effective_user.full_name,
last_name=update.effective_user.last_name,
is_bot=update.effective_user.is_bot,
language_code=update.effective_user.language_code,
)
)
else:
# failed to save poll answer.
# Ussually happens for polls that are sent to the bot before it started and not updated
# TODO save polls to db and query them instead saving them to bot_data
return
logging.error(
"Failed to save poll answer. Usually happens for polls that are sent to the bot before it "
"started and not updated",
exc_info=True,
)
6 changes: 3 additions & 3 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
load_dotenv()


TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
DEVELOPER_CHAT_ID = os.getenv("DEVELOPER_CHAT_ID", "")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", None)
DEVELOPER_CHAT_ID = os.getenv("DEVELOPER_CHAT_ID", None)

# Database constants
SQLITE_DB_FILE = os.getenv("SQLITE_DB_FILE", Path().resolve() / "database/db.sqlite")
SQLITE_DB_FILE_PATH = os.getenv("SQLITE_DB_FILE_PATH", Path().resolve() / "db.sqlite")
DATE_FORMAT = "%d.%m.%Y %H:%M:%S"

# Game constants
Expand Down
22 changes: 18 additions & 4 deletions src/data_models/Game.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from datetime import datetime
from typing import Literal

from pydantic import BaseModel
from pydantic import BaseModel, field_validator


class Game(BaseModel):
playroom_id: int
end_time: datetime
result: Literal["Hitler Canceler", "Fascist Law", "Hitler Death", "Liberal Law"]
poll_id: int
chat_id: int
results: dict # Literal["Hitler Canceler", "Fascist Law", "Hitler Death", "Liberal Law"]
creator_id: int

@field_validator("results", mode="after")
@classmethod
def validate_results(cls, v: dict) -> Literal["CH", "DH", "FW", "LW"]:
outcomes = set(v.values())
if "I'm Canceler Hitler" in outcomes:
return "CH"
if "I'm Dead Hitler" in outcomes:
return "DH"
if "I'm Liberal Winner" in outcomes:
return "LW"
if "I'm Fascistic Winner" in outcomes:
return "FW"
15 changes: 11 additions & 4 deletions src/data_models/Player.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
from typing import Optional

from pydantic import BaseModel
from pydantic import BaseModel, field_validator


class Player(BaseModel):
telegram_user_id: int
username: str
first_name: Optional[str] = None
last_name: Optional[str] = None
is_bot: bool = False
first_name: Optional[str | None]
full_name: Optional[str | None]
last_name: Optional[str | None]
is_bot: Optional[bool] = False
language_code: Optional[str | None]

@field_validator("is_bot", mode="after")
@classmethod
def validate_bot(cls, v: bool) -> str:
return "TRUE" if v else "FALSE" # sqlite3 does not support boolean type
4 changes: 3 additions & 1 deletion src/data_models/Record.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ def shorten_role(cls, v: str) -> Literal["CH", "DH", "HL", "LW", "LL", "FW", "FL
case "I'm Fascistic Loser":
return "FL"
case _:
raise ValueError(f"Invalid role '{v}' for Record. Role must be one of {config.GAME_POLL_OUTCOMES}")
raise ValueError(
f"Invalid role '{v}' for Record. Role must be one of {config.GAME_POLL_OUTCOMES}"
)
2 changes: 1 addition & 1 deletion src/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

async def get_db() -> aiosqlite.Connection:
if not getattr(get_db, "db", None):
db = await aiosqlite.connect(config.SQLITE_DB_FILE)
db = await aiosqlite.connect(config.SQLITE_DB_FILE_PATH)
get_db.db = db

return get_db.db
Expand Down
2 changes: 1 addition & 1 deletion src/get_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ def get_handlers() -> tuple:
CommandHandler("save", handlers.save),
# Poll answer handler
PollAnswerHandler(receive_poll_answer),
)
)
21 changes: 13 additions & 8 deletions src/handlers/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from telegram.ext import ContextTypes




async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
# set a higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
Expand All @@ -24,7 +22,9 @@ async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> N

# traceback.format_exception returns the usual python message about an exception, but as a
# list of strings rather than a single string, so we have to join them together.
tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
tb_list = traceback.format_exception(
None, context.error, context.error.__traceback__
)
tb_string = "".join(tb_list)

# Build the message with some markup and additional information about what happened.
Expand All @@ -38,8 +38,13 @@ async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> N
f"<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n"
f"<pre>{html.escape(tb_string)}</pre>"
)

# Finally, send the message
await context.bot.send_message(
chat_id=config.DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML
)
if config.DEVELOPER_CHAT_ID:
# Finally, send the message
await context.bot.send_message(
chat_id=config.DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML
)
else:
logger.error(
"DEVELOPER_CHAT_ID env variable wasn't purposed. Please set it to your chat id if you want to "
"receive error messages in telegram chat."
)
8 changes: 7 additions & 1 deletion src/handlers/game.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src import config
from src.data_models.Playroom import Playroom
from src.services.db_service import save_playroom
from src.utils import message_is_poll, is_message_from_group_chat


Expand Down Expand Up @@ -30,5 +32,9 @@ async def game(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"results": {},
}
}

await save_playroom(
Playroom(
telegram_chat_id=update.effective_chat.id, name=update.effective_chat.title
)
)
context.bot_data.update(game_metadata)
12 changes: 10 additions & 2 deletions src/handlers/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from telegram import Update
from telegram.ext import ContextTypes

from src.data_models.Game import Game
from src.data_models.Record import Record
from src.utils import message_is_poll, is_message_from_group_chat
from src import db
from src.services.db_service import save_record
from src.services.db_service import save_record, save_game


async def _pass_checks(
Expand Down Expand Up @@ -88,7 +89,14 @@ async def save(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
for player_id, result in poll_data["results"].items()
]
)

await save_game(
Game(
poll_id=poll_data["message_id"],
chat_id=poll_data["chat_id"],
creator_id=poll_data["creator_id"],
results=poll_data["results"].copy(),
)
)
else:
await update.effective_message.reply_text(
"Something went wrong. Can't process your request."
Expand Down
Loading

0 comments on commit b38aa4b

Please sign in to comment.