Compare commits
6 Commits
44e60fd535
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 983c7cde9d | |||
| 08069d660d | |||
| c1568e540f | |||
| 8a47dab3d1 | |||
| f19359cbee | |||
| ab33cbe005 |
@@ -1,6 +1,10 @@
|
||||
name: Build and Deploy Docker Container
|
||||
run-name: ${{ gitea.actor }} runs docker deployment
|
||||
on: [push]
|
||||
# run this workflow on push to main branch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@@ -26,18 +30,14 @@ jobs:
|
||||
push: true
|
||||
tags: git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||
|
||||
- name: Verify the Docker image is running
|
||||
run: |
|
||||
docker run -d --name test-thc-webhook -e DISCORD_WEBHOOK_URL=${{ secrets.DISCORD_WEBHOOK_URL }} -e TENOR_API_KEY=${{ secrets.TENOR_API_KEY }} -p 8080:8080 git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||
sleep 10
|
||||
if [ $(docker ps -q -f name=test-thc-webhook | wc -l) -eq 1 ]; then
|
||||
echo "Container is running successfully."
|
||||
docker logs test-thc-webhook
|
||||
docker stop test-thc-webhook
|
||||
docker rm test-thc-webhook
|
||||
else
|
||||
echo "Container failed to start."
|
||||
exit 1
|
||||
fi
|
||||
- name: Clean up Docker images
|
||||
run: docker image prune -f
|
||||
- name: Deploy to Portainer
|
||||
uses: appleboy/ssh-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||
script: |
|
||||
docker pull git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||
docker stop thc-webhook || true
|
||||
docker rm thc-webhook || true
|
||||
docker run -d --name thc-webhook -e DISCORD_WEBHOOK_URL=${{ secrets.DISCORD_WEBHOOK_URL }} -p 8080:8080 git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||
|
||||
33
.gitea/workflows/test-application.yaml
Normal file
33
.gitea/workflows/test-application.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Test Application
|
||||
run-name: ${{ gitea.actor }} tests the application
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Create and activate virtual environment
|
||||
run: |
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install pytest
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
pytest --maxfail=1 --disable-warnings -q
|
||||
60
main.py
60
main.py
@@ -18,26 +18,6 @@ load_dotenv()
|
||||
|
||||
WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
|
||||
|
||||
MSG_FOOTER = "THC - Toke Hash Coordinated"
|
||||
|
||||
MSG_REMINDER = "This is your 5 minute reminder to 420!"
|
||||
MSG_HALFTIME_REMINDER = "Half-time in 5 minutes!"
|
||||
MSG_HALFTIME = "Half-time!"
|
||||
MSG_NOTIFICATION = "Blaze it!"
|
||||
|
||||
COL_BLUE = 0x3498db
|
||||
COL_ORANGE = 0xe67e22
|
||||
COL_GREEN = 0x2ecc71
|
||||
COL_UNKNOWN = 0x95a5a6
|
||||
|
||||
messages = {
|
||||
"reminder_halftime": {"text": MSG_HALFTIME_REMINDER, "color": COL_ORANGE},
|
||||
"halftime": {"text": MSG_HALFTIME, "color": COL_GREEN},
|
||||
"reminder": {"text": MSG_REMINDER, "color": COL_ORANGE},
|
||||
"420": {"text": MSG_NOTIFICATION, "color": COL_GREEN},
|
||||
"unknown": {"text": "Unknown notification type", "color": COL_UNKNOWN}
|
||||
}
|
||||
|
||||
|
||||
def load_timezones() -> list[dict]:
|
||||
"""Load timezones from csv file."""
|
||||
@@ -77,42 +57,43 @@ def load_countries() -> list[dict]:
|
||||
return countries
|
||||
|
||||
|
||||
def create_embed(type: str) -> dict:
|
||||
"""
|
||||
Create a Discord embed message.
|
||||
"""
|
||||
color_orange = 0xe67e22
|
||||
color_green = 0x2ecc71
|
||||
messages = {
|
||||
"reminder_halftime": {"text": "Half-time in 5 minutes!", "color": color_orange},
|
||||
"halftime": {"text": "Half-time!", "color": color_green},
|
||||
"reminder": {"text": "This is your 5 minute reminder to 420!", "color": color_orange},
|
||||
"420": {"text": "Blaze it!", "color": color_green},
|
||||
}
|
||||
timezones = load_timezones()
|
||||
countries = load_countries()
|
||||
|
||||
|
||||
def get_message(type: str) -> dict[str, int]:
|
||||
"""
|
||||
Get the notification message based on the type.
|
||||
"""
|
||||
msg = messages["unknown"]
|
||||
if type in messages:
|
||||
msg = messages[type]
|
||||
if type in ["halftime", "420"]:
|
||||
# Add an image for halftime and 420
|
||||
msg["image"] = {
|
||||
"url": "https://www.freepnglogos.com/uploads/weed-leaf-png/cannabis-weed-leaf-png-clipart-images-24.png"}
|
||||
"url": "https://copyparty.allucanget.biz/img/weed.png"}
|
||||
if type == "420":
|
||||
# Check where it's 4:20
|
||||
tz_list = where_is_it_420(timezones, countries)
|
||||
if tz_list:
|
||||
tz_str = "\n".join(tz_list)
|
||||
msg["text"] += f"\nIt's 4:20 in:\n{tz_str}"
|
||||
else:
|
||||
msg["text"] += " It's not 4:20 anywhere right now."
|
||||
return msg
|
||||
|
||||
|
||||
def create_embed(message: str) -> dict:
|
||||
"""
|
||||
Create a Discord embed message.
|
||||
"""
|
||||
msg = get_message(message)
|
||||
else:
|
||||
msg = {"text": "Unknown notification type", "color": 0xFF0000}
|
||||
embed = {
|
||||
"title": message.replace("_", " ").capitalize(),
|
||||
"title": type.replace("_", " ").capitalize(),
|
||||
"description": msg["text"],
|
||||
"image": msg.get("image"),
|
||||
"color": msg["color"],
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"footer": {"text": MSG_FOOTER}
|
||||
"footer": {"text": "THC - Toke Hash Coordinated"}
|
||||
}
|
||||
return embed
|
||||
|
||||
@@ -163,8 +144,9 @@ def where_is_it_420(timezones: list[dict], countries: list[dict]) -> list[str]:
|
||||
tz_info = get_tz_info(tz, timezones)
|
||||
if tz_info:
|
||||
country = get_country_info(tz_info["country_code"], countries)
|
||||
if country and country["country_name"] not in tz_list:
|
||||
if country:
|
||||
country_name = country["country_name"].strip().strip('"')
|
||||
if country_name not in tz_list:
|
||||
tz_list.append(country_name)
|
||||
return tz_list
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pytest
|
||||
python-dotenv
|
||||
pytz
|
||||
requests
|
||||
|
||||
111
test_main.py
Normal file
111
test_main.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import io
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import main
|
||||
|
||||
|
||||
SAMPLE_TIMEZONE_CSV = """Etc/UTC,ZZ,UTC,0,0,0
|
||||
America/New_York,US,EST,0,-18000,0
|
||||
Europe/London,GB,BST,0,0,1
|
||||
"""
|
||||
|
||||
SAMPLE_COUNTRY_CSV = """ZZ,Unknown
|
||||
US,United States
|
||||
GB,United Kingdom
|
||||
"""
|
||||
|
||||
|
||||
def test_load_timezones_and_countries(monkeypatch):
|
||||
tzs = main.load_timezones()
|
||||
countries = main.load_countries()
|
||||
assert any(t['zone_name'] == 'America/New_York' for t in tzs)
|
||||
assert any(c['country_code'] == 'US' for c in countries)
|
||||
|
||||
|
||||
def test_get_tz_and_country_info():
|
||||
timezones = [{'zone_name': 'A/B', 'country_code': 'US'}]
|
||||
countries = [{'country_code': 'US', 'country_name': 'United States'}]
|
||||
assert main.get_tz_info('A/B', timezones)['zone_name'] == 'A/B'
|
||||
assert main.get_country_info('US', countries)[
|
||||
'country_name'] == 'United States'
|
||||
assert main.get_tz_info('X/Y', timezones) is None
|
||||
assert main.get_country_info('XX', countries) is None
|
||||
|
||||
|
||||
def test_create_embed_all_types(monkeypatch):
|
||||
# Prevent create_embed from trying to read actual CSV files by patching loaders
|
||||
monkeypatch.setattr(main, 'load_timezones', lambda: [
|
||||
{'zone_name': 'Etc/UTC', 'country_code': 'ZZ'}])
|
||||
monkeypatch.setattr(main, 'load_countries', lambda: [
|
||||
{'country_code': 'ZZ', 'country_name': 'Nowhere'}])
|
||||
|
||||
# reminder
|
||||
emb = main.create_embed('reminder')
|
||||
assert emb['title'] == 'Reminder'
|
||||
assert '5 minute' in emb['description']
|
||||
assert emb['color'] == 0xe67e22
|
||||
|
||||
# reminder_halftime
|
||||
emb = main.create_embed('reminder_halftime')
|
||||
assert emb['title'] == 'Reminder halftime'
|
||||
assert 'Half-time in 5 minutes' in emb['description']
|
||||
|
||||
# halftime (should include image)
|
||||
monkeypatch.setattr(main, 'where_is_it_420', lambda tzs, cs: [])
|
||||
emb = main.create_embed('halftime')
|
||||
assert emb['title'] == 'Halftime'
|
||||
assert emb['image'] is not None
|
||||
|
||||
# 420 (should include image and appended tz info string when list empty)
|
||||
monkeypatch.setattr(main, 'where_is_it_420', lambda tzs, cs: [])
|
||||
emb = main.create_embed('420')
|
||||
assert emb['title'] == '420'
|
||||
assert emb['image'] is not None
|
||||
assert "not 4:20" in emb['description'] or "It's 4:20" in emb['description']
|
||||
|
||||
# unknown
|
||||
emb = main.create_embed('nope')
|
||||
assert emb['description'] == 'Unknown notification type'
|
||||
|
||||
|
||||
def test_where_is_it_420(monkeypatch):
|
||||
# Limit timezones to a predictable set
|
||||
monkeypatch.setattr(main.pytz, 'all_timezones', ['Etc/UTC'])
|
||||
|
||||
tzs = [{'zone_name': 'Etc/UTC', 'country_code': 'ZZ'}]
|
||||
countries = [{'country_code': 'ZZ', 'country_name': 'Nowhere'}]
|
||||
|
||||
class FakeDatetime:
|
||||
@staticmethod
|
||||
def now(tz):
|
||||
class R:
|
||||
hour = 4
|
||||
return R()
|
||||
|
||||
monkeypatch.setattr(main, 'datetime', FakeDatetime)
|
||||
monkeypatch.setattr(main, 'get_tz_info', lambda name,
|
||||
t: tzs[0] if name == 'Etc/UTC' else None)
|
||||
monkeypatch.setattr(main, 'get_country_info', lambda code,
|
||||
c: countries[0] if code == 'ZZ' else None)
|
||||
|
||||
res = main.where_is_it_420(tzs, countries)
|
||||
assert res == ['Nowhere']
|
||||
|
||||
|
||||
def test_main_exits_quickly(monkeypatch):
|
||||
# Patch send_notification so it doesn't perform network
|
||||
monkeypatch.setattr(main, 'send_notification', lambda x: None)
|
||||
# Make schedule.run_pending raise KeyboardInterrupt to exit loop
|
||||
monkeypatch.setattr(main.schedule, 'run_pending', lambda: (
|
||||
_ for _ in ()).throw(KeyboardInterrupt()))
|
||||
# Patch time.sleep to no-op
|
||||
monkeypatch.setattr(main.time, 'sleep', lambda s: None)
|
||||
# Ensure WEBHOOK_URL present to avoid early return
|
||||
monkeypatch.setenv('DISCORD_WEBHOOK_URL', 'http://example.com/webhook')
|
||||
main.WEBHOOK_URL = 'http://example.com/webhook'
|
||||
|
||||
# Should exit quickly due to KeyboardInterrupt from run_pending
|
||||
main.main()
|
||||
@@ -1,17 +0,0 @@
|
||||
[Unit]
|
||||
Description=Discord Webhook Notification Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/srv/app/thc-time
|
||||
ExecStart=/srv/app/thc-time/venv/bin/python /srv/app/thc-time/main.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user