Compare commits
23 Commits
b8cb3aa71e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 983c7cde9d | |||
| 08069d660d | |||
| c1568e540f | |||
| 8a47dab3d1 | |||
| f19359cbee | |||
| ab33cbe005 | |||
| 44e60fd535 | |||
| 36a7b8bcf1 | |||
| 5b81106015 | |||
| 043dd05304 | |||
| 145a8be442 | |||
| 3f854f2080 | |||
| 8904da50dc | |||
| 3c5dcc6ac2 | |||
| c9603be9bd | |||
| a31744704f | |||
| 4b6ecf1726 | |||
| 118269935d | |||
| 054ac79984 | |||
| ff18be85ad | |||
| 3c5fafadaa | |||
| b652dc748c | |||
| f1b831b2ca |
43
.gitea/workflows/build-container.yaml
Normal file
43
.gitea/workflows/build-container.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Build and Deploy Docker Container
|
||||||
|
run-name: ${{ gitea.actor }} runs docker deployment
|
||||||
|
# run this workflow on push to main branch
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.allucanget.biz
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: git.allucanget.biz/${{ secrets.REGISTRY_USERNAME }}/thc-webhook:latest
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
# Use official Python image
|
# Use official Python image
|
||||||
FROM python:3.11-slim
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies if needed (none for this app)
|
|
||||||
# RUN apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy requirements and install Python dependencies
|
# Copy requirements and install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
@@ -15,8 +12,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Create a non-root user
|
# Create a non-root user
|
||||||
RUN useradd --create-home --shell /bin/bash app
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
USER app
|
USER appuser
|
||||||
|
|
||||||
# Command to run the application
|
# Command to run the application
|
||||||
CMD ["python", "main.py"]
|
CMD ["python", "main.py"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
thc-webhook:
|
thc-webhook:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
155
main.py
155
main.py
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import pytz
|
||||||
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
@@ -16,53 +18,94 @@ load_dotenv()
|
|||||||
|
|
||||||
WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
|
WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
|
||||||
|
|
||||||
MSG_TEST = "Discord 420 timer activated. This is a test notification."
|
|
||||||
MSG_REMINDER = "5 minute reminder!"
|
|
||||||
MSG_NOTIFICATION = "420!"
|
|
||||||
COL_BLUE = 0x3498db
|
|
||||||
COL_ORANGE = 0xe67e22
|
|
||||||
COL_GREEN = 0x2ecc71
|
|
||||||
COL_UNKNOWN = 0x95a5a6
|
|
||||||
|
|
||||||
messages = {
|
def load_timezones() -> list[dict]:
|
||||||
"test": {"text": MSG_TEST, "color": COL_BLUE},
|
"""Load timezones from csv file."""
|
||||||
"reminder": {"text": MSG_REMINDER, "color": COL_ORANGE},
|
# Read the CSV file and return a list of timezones
|
||||||
"notification": {"text": MSG_NOTIFICATION, "color": COL_GREEN},
|
with open("tzdb/TimeZoneDB.csv/time_zone.csv", "r", encoding="utf-8") as f:
|
||||||
"unknown": {"text": "Unknown notification type", "color": COL_UNKNOWN}
|
lines = f.readlines()
|
||||||
}
|
# Fields: zone_name,country_code,abbreviation,time_start,gmt_offset,dst
|
||||||
|
timezones = []
|
||||||
|
for line in lines:
|
||||||
|
fields = line.strip().split(",")
|
||||||
|
if len(fields) >= 5:
|
||||||
|
timezones.append({
|
||||||
|
"zone_name": fields[0],
|
||||||
|
"country_code": fields[1],
|
||||||
|
"abbreviation": fields[2],
|
||||||
|
"time_start": fields[3],
|
||||||
|
"gmt_offset": int(fields[4]),
|
||||||
|
"dst": fields[5] == '1'
|
||||||
|
})
|
||||||
|
return timezones
|
||||||
|
|
||||||
|
|
||||||
def get_message(type: str) -> dict[str, int]:
|
def load_countries() -> list[dict]:
|
||||||
|
"""Load countries from csv file."""
|
||||||
|
# Read the CSV file and return a list of countries
|
||||||
|
with open("tzdb/TimeZoneDB.csv/country.csv", "r", encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
# Fields: country_code,country_name
|
||||||
|
countries = []
|
||||||
|
for line in lines:
|
||||||
|
fields = line.strip().split(",")
|
||||||
|
if len(fields) >= 2:
|
||||||
|
countries.append({
|
||||||
|
"country_code": fields[0],
|
||||||
|
"country_name": fields[1]
|
||||||
|
})
|
||||||
|
return countries
|
||||||
|
|
||||||
|
|
||||||
|
def create_embed(type: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Get the notification message based on the type.
|
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()
|
||||||
if type in messages:
|
if type in messages:
|
||||||
return messages[type]
|
msg = messages[type]
|
||||||
|
if type in ["halftime", "420"]:
|
||||||
|
# Add an image for halftime and 420
|
||||||
|
msg["image"] = {
|
||||||
|
"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."
|
||||||
else:
|
else:
|
||||||
return messages["unknown"]
|
msg = {"text": "Unknown notification type", "color": 0xFF0000}
|
||||||
|
embed = {
|
||||||
|
"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": "THC - Toke Hash Coordinated"}
|
||||||
|
}
|
||||||
|
return embed
|
||||||
|
|
||||||
|
|
||||||
def send_notification(message: dict[str, int]) -> None:
|
def send_notification(message: str) -> None:
|
||||||
"""
|
"""
|
||||||
Send a notification to the Discord webhook as an embed.
|
Send a notification to the Discord webhook.
|
||||||
|
|
||||||
Args:
|
|
||||||
message (dict[str, int]): The message to send.
|
|
||||||
"""
|
"""
|
||||||
if not WEBHOOK_URL:
|
if not WEBHOOK_URL:
|
||||||
logging.error("WEBHOOK_URL not set")
|
logging.error("WEBHOOK_URL not set")
|
||||||
return
|
return
|
||||||
|
embed = create_embed(message)
|
||||||
embed = {
|
|
||||||
"title": "Notification",
|
|
||||||
"description": message["text"],
|
|
||||||
"color": message["color"],
|
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
||||||
"footer": {
|
|
||||||
"text": "Discord Webhook App"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {"embeds": [embed]}
|
data = {"embeds": [embed]}
|
||||||
try:
|
try:
|
||||||
response = requests.post(WEBHOOK_URL, json=data, timeout=10)
|
response = requests.post(WEBHOOK_URL, json=data, timeout=10)
|
||||||
@@ -77,11 +120,35 @@ def send_notification(message: dict[str, int]) -> None:
|
|||||||
logging.error(f"Error sending notification: {e}")
|
logging.error(f"Error sending notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
def test_notification() -> None:
|
def get_tz_info(tz_name: str, timezones: list[dict]) -> dict | None:
|
||||||
|
"""Get timezone info by name."""
|
||||||
|
return next((tz for tz in timezones if tz["zone_name"] == tz_name), None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_country_info(country_code: str, countries: list[dict]) -> dict | None:
|
||||||
|
"""Get country info by country code."""
|
||||||
|
return next((c for c in countries if c["country_code"] == country_code), None)
|
||||||
|
|
||||||
|
|
||||||
|
def where_is_it_420(timezones: list[dict], countries: list[dict]) -> list[str]:
|
||||||
|
"""Get timezones where the current hour is 4 or 16, indicating it's 4:20 there.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: A list of timezones where it's currently 4:20 PM or AM.
|
||||||
"""
|
"""
|
||||||
Send a test notification to verify the webhook.
|
tz_list = []
|
||||||
"""
|
for tz in pytz.all_timezones:
|
||||||
send_notification(get_message("test"))
|
now = datetime.now(pytz.timezone(tz))
|
||||||
|
if now.hour == 4 or now.hour == 16:
|
||||||
|
# Find the timezone in the loaded timezones
|
||||||
|
tz_info = get_tz_info(tz, timezones)
|
||||||
|
if tz_info:
|
||||||
|
country = get_country_info(tz_info["country_code"], countries)
|
||||||
|
if country:
|
||||||
|
country_name = country["country_name"].strip().strip('"')
|
||||||
|
if country_name not in tz_list:
|
||||||
|
tz_list.append(country_name)
|
||||||
|
return tz_list
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -89,19 +156,19 @@ def main() -> None:
|
|||||||
Main function to run the scheduler.
|
Main function to run the scheduler.
|
||||||
"""
|
"""
|
||||||
# Schedule notifications
|
# Schedule notifications
|
||||||
schedule.every().hour.at(":15").do(send_notification, get_message("reminder"))
|
schedule.every().hour.at(":15").do(send_notification, "reminder")
|
||||||
schedule.every().hour.at(":20").do(send_notification, get_message("notification"))
|
schedule.every().hour.at(":20").do(send_notification, "420")
|
||||||
|
schedule.every().hour.at(":45").do(send_notification, "reminder_halftime")
|
||||||
logging.info(
|
schedule.every().hour.at(":50").do(send_notification, "halftime")
|
||||||
"Scheduler started. Notifications will be sent every hour at :15 and :20")
|
logging.info("Scheduler started.")
|
||||||
|
|
||||||
# Test the notification on startup
|
# Test the notification on startup
|
||||||
test_notification()
|
send_notification("420")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
schedule.run_pending()
|
schedule.run_pending()
|
||||||
time.sleep(60) # Check every minute
|
time.sleep(1) # Check every second
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.info("Scheduler stopped by user")
|
logging.info("Scheduler stopped by user")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
requests
|
pytest
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
pytz
|
||||||
|
requests
|
||||||
schedule
|
schedule
|
||||||
|
|||||||
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
|
|
||||||
247
tzdb/TimeZoneDB.csv/country.csv
Normal file
247
tzdb/TimeZoneDB.csv/country.csv
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
AF,Afghanistan
|
||||||
|
AL,Albania
|
||||||
|
DZ,Algeria
|
||||||
|
AS,American Samoa
|
||||||
|
AD,Andorra
|
||||||
|
AO,Angola
|
||||||
|
AI,Anguilla
|
||||||
|
AQ,Antarctica
|
||||||
|
AG,Antigua and Barbuda
|
||||||
|
AR,Argentina
|
||||||
|
AM,Armenia
|
||||||
|
AW,Aruba
|
||||||
|
AU,Australia
|
||||||
|
AT,Austria
|
||||||
|
AZ,Azerbaijan
|
||||||
|
BS,Bahamas
|
||||||
|
BH,Bahrain
|
||||||
|
BD,Bangladesh
|
||||||
|
BB,Barbados
|
||||||
|
BY,Belarus
|
||||||
|
BE,Belgium
|
||||||
|
BZ,Belize
|
||||||
|
BJ,Benin
|
||||||
|
BM,Bermuda
|
||||||
|
BT,Bhutan
|
||||||
|
BO,"Bolivia, Plurinational State of"
|
||||||
|
BQ,"Bonaire, Sint Eustatius and Saba"
|
||||||
|
BA,Bosnia and Herzegovina
|
||||||
|
BW,Botswana
|
||||||
|
BR,Brazil
|
||||||
|
IO,British Indian Ocean Territory
|
||||||
|
BN,Brunei Darussalam
|
||||||
|
BG,Bulgaria
|
||||||
|
BF,Burkina Faso
|
||||||
|
BI,Burundi
|
||||||
|
KH,Cambodia
|
||||||
|
CM,Cameroon
|
||||||
|
CA,Canada
|
||||||
|
CV,Cape Verde
|
||||||
|
KY,Cayman Islands
|
||||||
|
CF,Central African Republic
|
||||||
|
TD,Chad
|
||||||
|
CL,Chile
|
||||||
|
CN,China
|
||||||
|
CX,Christmas Island
|
||||||
|
CC,Cocos (Keeling) Islands
|
||||||
|
CO,Colombia
|
||||||
|
KM,Comoros
|
||||||
|
CG,Congo
|
||||||
|
CD,"Congo, the Democratic Republic of the"
|
||||||
|
CK,Cook Islands
|
||||||
|
CR,Costa Rica
|
||||||
|
HR,Croatia
|
||||||
|
CU,Cuba
|
||||||
|
CW,Curaçao
|
||||||
|
CY,Cyprus
|
||||||
|
CZ,Czech Republic
|
||||||
|
CI,Côte d'Ivoire
|
||||||
|
DK,Denmark
|
||||||
|
DJ,Djibouti
|
||||||
|
DM,Dominica
|
||||||
|
DO,Dominican Republic
|
||||||
|
EC,Ecuador
|
||||||
|
EG,Egypt
|
||||||
|
SV,El Salvador
|
||||||
|
GQ,Equatorial Guinea
|
||||||
|
ER,Eritrea
|
||||||
|
EE,Estonia
|
||||||
|
ET,Ethiopia
|
||||||
|
FK,Falkland Islands (Malvinas)
|
||||||
|
FO,Faroe Islands
|
||||||
|
FJ,Fiji
|
||||||
|
FI,Finland
|
||||||
|
FR,France
|
||||||
|
GF,French Guiana
|
||||||
|
PF,French Polynesia
|
||||||
|
TF,French Southern Territories
|
||||||
|
GA,Gabon
|
||||||
|
GM,Gambia
|
||||||
|
GE,Georgia
|
||||||
|
DE,Germany
|
||||||
|
GH,Ghana
|
||||||
|
GI,Gibraltar
|
||||||
|
GR,Greece
|
||||||
|
GL,Greenland
|
||||||
|
GD,Grenada
|
||||||
|
GP,Guadeloupe
|
||||||
|
GU,Guam
|
||||||
|
GT,Guatemala
|
||||||
|
GG,Guernsey
|
||||||
|
GN,Guinea
|
||||||
|
GW,Guinea-Bissau
|
||||||
|
GY,Guyana
|
||||||
|
HT,Haiti
|
||||||
|
VA,Holy See (Vatican City State)
|
||||||
|
HN,Honduras
|
||||||
|
HK,Hong Kong
|
||||||
|
HU,Hungary
|
||||||
|
IS,Iceland
|
||||||
|
IN,India
|
||||||
|
ID,Indonesia
|
||||||
|
IR,"Iran, Islamic Republic of"
|
||||||
|
IQ,Iraq
|
||||||
|
IE,Ireland
|
||||||
|
IM,Isle of Man
|
||||||
|
IL,Israel
|
||||||
|
IT,Italy
|
||||||
|
JM,Jamaica
|
||||||
|
JP,Japan
|
||||||
|
JE,Jersey
|
||||||
|
JO,Jordan
|
||||||
|
KZ,Kazakhstan
|
||||||
|
KE,Kenya
|
||||||
|
KI,Kiribati
|
||||||
|
KP,"Korea, Democratic People's Republic of"
|
||||||
|
KR,"Korea, Republic of"
|
||||||
|
KW,Kuwait
|
||||||
|
KG,Kyrgyzstan
|
||||||
|
LA,Lao People's Democratic Republic
|
||||||
|
LV,Latvia
|
||||||
|
LB,Lebanon
|
||||||
|
LS,Lesotho
|
||||||
|
LR,Liberia
|
||||||
|
LY,Libya
|
||||||
|
LI,Liechtenstein
|
||||||
|
LT,Lithuania
|
||||||
|
LU,Luxembourg
|
||||||
|
MO,Macao
|
||||||
|
MK,"Macedonia, the Former Yugoslav Republic of"
|
||||||
|
MG,Madagascar
|
||||||
|
MW,Malawi
|
||||||
|
MY,Malaysia
|
||||||
|
MV,Maldives
|
||||||
|
ML,Mali
|
||||||
|
MT,Malta
|
||||||
|
MH,Marshall Islands
|
||||||
|
MQ,Martinique
|
||||||
|
MR,Mauritania
|
||||||
|
MU,Mauritius
|
||||||
|
YT,Mayotte
|
||||||
|
MX,Mexico
|
||||||
|
FM,"Micronesia, Federated States of"
|
||||||
|
MD,"Moldova, Republic of"
|
||||||
|
MC,Monaco
|
||||||
|
MN,Mongolia
|
||||||
|
ME,Montenegro
|
||||||
|
MS,Montserrat
|
||||||
|
MA,Morocco
|
||||||
|
MZ,Mozambique
|
||||||
|
MM,Myanmar
|
||||||
|
NA,Namibia
|
||||||
|
NR,Nauru
|
||||||
|
NP,Nepal
|
||||||
|
NL,Netherlands
|
||||||
|
NC,New Caledonia
|
||||||
|
NZ,New Zealand
|
||||||
|
NI,Nicaragua
|
||||||
|
NE,Niger
|
||||||
|
NG,Nigeria
|
||||||
|
NU,Niue
|
||||||
|
NF,Norfolk Island
|
||||||
|
MP,Northern Mariana Islands
|
||||||
|
NO,Norway
|
||||||
|
OM,Oman
|
||||||
|
PK,Pakistan
|
||||||
|
PW,Palau
|
||||||
|
PS,"Palestine, State of"
|
||||||
|
PA,Panama
|
||||||
|
PG,Papua New Guinea
|
||||||
|
PY,Paraguay
|
||||||
|
PE,Peru
|
||||||
|
PH,Philippines
|
||||||
|
PN,Pitcairn
|
||||||
|
PL,Poland
|
||||||
|
PT,Portugal
|
||||||
|
PR,Puerto Rico
|
||||||
|
QA,Qatar
|
||||||
|
RO,Romania
|
||||||
|
RU,Russian Federation
|
||||||
|
RW,Rwanda
|
||||||
|
RE,Réunion
|
||||||
|
BL,Saint Barthélemy
|
||||||
|
SH,"Saint Helena, Ascension and Tristan da Cunha"
|
||||||
|
KN,Saint Kitts and Nevis
|
||||||
|
LC,Saint Lucia
|
||||||
|
MF,Saint Martin (French part)
|
||||||
|
PM,Saint Pierre and Miquelon
|
||||||
|
VC,Saint Vincent and the Grenadines
|
||||||
|
WS,Samoa
|
||||||
|
SM,San Marino
|
||||||
|
ST,Sao Tome and Principe
|
||||||
|
SA,Saudi Arabia
|
||||||
|
SN,Senegal
|
||||||
|
RS,Serbia
|
||||||
|
SC,Seychelles
|
||||||
|
SL,Sierra Leone
|
||||||
|
SG,Singapore
|
||||||
|
SX,Sint Maarten (Dutch part)
|
||||||
|
SK,Slovakia
|
||||||
|
SI,Slovenia
|
||||||
|
SB,Solomon Islands
|
||||||
|
SO,Somalia
|
||||||
|
ZA,South Africa
|
||||||
|
GS,South Georgia and the South Sandwich Islands
|
||||||
|
SS,South Sudan
|
||||||
|
ES,Spain
|
||||||
|
LK,Sri Lanka
|
||||||
|
SD,Sudan
|
||||||
|
SR,Suriname
|
||||||
|
SJ,Svalbard and Jan Mayen
|
||||||
|
SZ,Swaziland
|
||||||
|
SE,Sweden
|
||||||
|
CH,Switzerland
|
||||||
|
SY,Syrian Arab Republic
|
||||||
|
TW,"Taiwan, Province of China"
|
||||||
|
TJ,Tajikistan
|
||||||
|
TZ,"Tanzania, United Republic of"
|
||||||
|
TH,Thailand
|
||||||
|
TL,Timor-Leste
|
||||||
|
TG,Togo
|
||||||
|
TK,Tokelau
|
||||||
|
TO,Tonga
|
||||||
|
TT,Trinidad and Tobago
|
||||||
|
TN,Tunisia
|
||||||
|
TR,Turkey
|
||||||
|
TM,Turkmenistan
|
||||||
|
TC,Turks and Caicos Islands
|
||||||
|
TV,Tuvalu
|
||||||
|
UG,Uganda
|
||||||
|
UA,Ukraine
|
||||||
|
AE,United Arab Emirates
|
||||||
|
GB,United Kingdom
|
||||||
|
US,United States
|
||||||
|
UM,United States Minor Outlying Islands
|
||||||
|
UY,Uruguay
|
||||||
|
UZ,Uzbekistan
|
||||||
|
VU,Vanuatu
|
||||||
|
VE,"Venezuela, Bolivarian Republic of"
|
||||||
|
VN,Viet Nam
|
||||||
|
VG,"Virgin Islands, British"
|
||||||
|
VI,"Virgin Islands, U.S."
|
||||||
|
WF,Wallis and Futuna
|
||||||
|
EH,Western Sahara
|
||||||
|
YE,Yemen
|
||||||
|
ZM,Zambia
|
||||||
|
ZW,Zimbabwe
|
||||||
|
AX,Åland Islands
|
||||||
|
145637
tzdb/TimeZoneDB.csv/time_zone.csv
Normal file
145637
tzdb/TimeZoneDB.csv/time_zone.csv
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user