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
|
name: Build and Deploy Docker Container
|
||||||
run-name: ${{ gitea.actor }} runs docker deployment
|
run-name: ${{ gitea.actor }} runs docker deployment
|
||||||
on: [push]
|
# run this workflow on push to main branch
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -26,18 +30,14 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
tags: git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||||
|
|
||||||
- name: Verify the Docker image is running
|
- name: Deploy to Portainer
|
||||||
run: |
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
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
|
with:
|
||||||
sleep 10
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
if [ $(docker ps -q -f name=test-thc-webhook | wc -l) -eq 1 ]; then
|
username: ${{ secrets.SERVER_USER }}
|
||||||
echo "Container is running successfully."
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
docker logs test-thc-webhook
|
script: |
|
||||||
docker stop test-thc-webhook
|
docker pull git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||||
docker rm test-thc-webhook
|
docker stop thc-webhook || true
|
||||||
else
|
docker rm thc-webhook || true
|
||||||
echo "Container failed to start."
|
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
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Clean up Docker images
|
|
||||||
run: docker image prune -f
|
|
||||||
|
|||||||
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
|
||||||
62
main.py
62
main.py
@@ -18,26 +18,6 @@ load_dotenv()
|
|||||||
|
|
||||||
WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
|
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]:
|
def load_timezones() -> list[dict]:
|
||||||
"""Load timezones from csv file."""
|
"""Load timezones from csv file."""
|
||||||
@@ -77,42 +57,43 @@ def load_countries() -> list[dict]:
|
|||||||
return countries
|
return countries
|
||||||
|
|
||||||
|
|
||||||
timezones = load_timezones()
|
def create_embed(type: str) -> dict:
|
||||||
countries = load_countries()
|
|
||||||
|
|
||||||
|
|
||||||
def get_message(type: str) -> dict[str, int]:
|
|
||||||
"""
|
"""
|
||||||
Get the notification message based on the type.
|
Create a Discord embed message.
|
||||||
"""
|
"""
|
||||||
msg = messages["unknown"]
|
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()
|
||||||
if type in messages:
|
if type in messages:
|
||||||
msg = messages[type]
|
msg = messages[type]
|
||||||
if type in ["halftime", "420"]:
|
if type in ["halftime", "420"]:
|
||||||
|
# Add an image for halftime and 420
|
||||||
msg["image"] = {
|
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":
|
if type == "420":
|
||||||
|
# Check where it's 4:20
|
||||||
tz_list = where_is_it_420(timezones, countries)
|
tz_list = where_is_it_420(timezones, countries)
|
||||||
if tz_list:
|
if tz_list:
|
||||||
tz_str = "\n".join(tz_list)
|
tz_str = "\n".join(tz_list)
|
||||||
msg["text"] += f"\nIt's 4:20 in:\n{tz_str}"
|
msg["text"] += f"\nIt's 4:20 in:\n{tz_str}"
|
||||||
else:
|
else:
|
||||||
msg["text"] += " It's not 4:20 anywhere right now."
|
msg["text"] += " It's not 4:20 anywhere right now."
|
||||||
return msg
|
else:
|
||||||
|
msg = {"text": "Unknown notification type", "color": 0xFF0000}
|
||||||
|
|
||||||
def create_embed(message: str) -> dict:
|
|
||||||
"""
|
|
||||||
Create a Discord embed message.
|
|
||||||
"""
|
|
||||||
msg = get_message(message)
|
|
||||||
embed = {
|
embed = {
|
||||||
"title": message.replace("_", " ").capitalize(),
|
"title": type.replace("_", " ").capitalize(),
|
||||||
"description": msg["text"],
|
"description": msg["text"],
|
||||||
"image": msg.get("image"),
|
"image": msg.get("image"),
|
||||||
"color": msg["color"],
|
"color": msg["color"],
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
"footer": {"text": MSG_FOOTER}
|
"footer": {"text": "THC - Toke Hash Coordinated"}
|
||||||
}
|
}
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@@ -163,9 +144,10 @@ def where_is_it_420(timezones: list[dict], countries: list[dict]) -> list[str]:
|
|||||||
tz_info = get_tz_info(tz, timezones)
|
tz_info = get_tz_info(tz, timezones)
|
||||||
if tz_info:
|
if tz_info:
|
||||||
country = get_country_info(tz_info["country_code"], countries)
|
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('"')
|
country_name = country["country_name"].strip().strip('"')
|
||||||
tz_list.append(country_name)
|
if country_name not in tz_list:
|
||||||
|
tz_list.append(country_name)
|
||||||
return tz_list
|
return tz_list
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pytest
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pytz
|
pytz
|
||||||
requests
|
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