Compare commits

...

21 Commits

Author SHA1 Message Date
983c7cde9d refactor: remove Docker image verification and cleanup steps from build-container workflow
Some checks failed
Test Application / test (push) Successful in 41s
Build and Deploy Docker Container / build-and-deploy (push) Failing after 26s
2025-10-05 16:08:43 +02:00
08069d660d fix: no teardown/cleanup needed
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Successful in 47s
Test Application / test (push) Successful in 23s
Build and Deploy Docker Container / portainer-deploy (push) Failing after 1s
2025-10-05 16:07:57 +02:00
c1568e540f feat: add test workflow and implement unit tests for main functionality
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Successful in 1m15s
Test Application / test (push) Failing after 1m24s
Build and Deploy Docker Container / portainer-deploy (push) Failing after 2s
2025-10-05 16:03:41 +02:00
8a47dab3d1 feat: add portainer deployment step to build-container workflow
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Successful in 49s
Build and Deploy Docker Container / portainer-deploy (push) Failing after 6s
changing action to run deployment only on push to main branch
2025-10-05 15:58:57 +02:00
f19359cbee refactor: streamline message handling and embed creation
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 49s
2025-10-05 11:47:39 +02:00
ab33cbe005 remove service
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 47s
image on own server
2025-10-04 19:23:36 +02:00
44e60fd535 Merge pull request 'adding country list' (#1) from feat/country-list into main
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 44s
Reviewed-on: #1
2025-10-04 18:20:35 +02:00
36a7b8bcf1 adding country list
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 44s
2025-10-04 18:18:26 +02:00
5b81106015 replace image
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 2m14s
2025-10-03 19:43:15 +02:00
043dd05304 increase GIF limit in search_tenor and optimize image retrieval in get_message
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 3m12s
2025-09-23 15:26:51 +02:00
145a8be442 cleanup build action
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 45s
2025-09-21 16:15:59 +02:00
3f854f2080 docker-compose update
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Failing after 5s
2025-09-21 16:14:07 +02:00
8904da50dc fixing main and build updates
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Failing after 4s
2025-09-21 16:12:03 +02:00
3c5dcc6ac2 better messages
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 50s
2025-09-21 15:50:27 +02:00
c9603be9bd full image name
All checks were successful
Build and Deploy Docker Container / build-and-deploy (push) Successful in 49s
according to [naming convention](https://docs.gitea.com/usage/packages/container#image-naming-convention)
2025-09-19 21:30:40 +02:00
a31744704f fixing deploy workflow
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Failing after 23s
2025-09-19 21:23:52 +02:00
4b6ecf1726 switching to alpine and using git.allucanget.biz docker registry
Some checks failed
Build and Deploy Docker Container / build-and-deploy (push) Failing after 2m1s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2025-09-19 21:18:02 +02:00
118269935d fix demo action runner
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m16s
2025-09-19 19:03:11 +02:00
054ac79984 demo workflow
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2025-09-19 18:59:50 +02:00
ff18be85ad stricter timing 2025-09-17 21:24:10 +02:00
3c5fafadaa main function tweaks 2025-09-17 21:19:28 +02:00
10 changed files with 146187 additions and 71 deletions

View 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

View 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

View File

@@ -1,12 +1,9 @@
# Use official Python image
FROM python:3.11-slim
FROM python:3.12-alpine
# Set working directory
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.txt .
RUN pip install --no-cache-dir -r requirements.txt
@@ -15,8 +12,8 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Create a non-root user
RUN useradd --create-home --shell /bin/bash app
USER app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Command to run the application
CMD ["python", "main.py"]

View File

@@ -1,5 +1,3 @@
version: "3.8"
services:
thc-webhook:
build: .

159
main.py
View File

@@ -1,4 +1,6 @@
import os
import pytz
from datetime import datetime
import time
import logging
import requests
@@ -16,55 +18,94 @@ load_dotenv()
WEBHOOK_URL = os.getenv('DISCORD_WEBHOOK_URL')
MSG_TEST = "Discord 420 timer activated. This is a test notification."
MSG_REMINDER = "5 minute reminder!"
MSG_HALFTIME = "Half-time!"
MSG_NOTIFICATION = "420! Blaze it!"
COL_BLUE = 0x3498db
COL_ORANGE = 0xe67e22
COL_GREEN = 0x2ecc71
COL_UNKNOWN = 0x95a5a6
def load_timezones() -> list[dict]:
"""Load timezones from csv file."""
# Read the CSV file and return a list of timezones
with open("tzdb/TimeZoneDB.csv/time_zone.csv", "r", encoding="utf-8") as f:
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 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:
"""
Create a Discord embed message.
"""
color_orange = 0xe67e22
color_green = 0x2ecc71
messages = {
"test": {"text": MSG_TEST, "color": COL_BLUE},
"reminder": {"text": MSG_REMINDER, "color": COL_ORANGE},
"halftime": {"text": MSG_HALFTIME, "color": COL_GREEN},
"notification": {"text": MSG_NOTIFICATION, "color": COL_GREEN},
"unknown": {"text": "Unknown notification type", "color": COL_UNKNOWN}
"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},
}
def get_message(type: str) -> dict[str, int]:
"""
Get the notification message based on the type.
"""
timezones = load_timezones()
countries = load_countries()
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:
return messages["unknown"]
msg["text"] += " It's not 4:20 anywhere right now."
else:
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.
Args:
message (dict[str, int]): The message to send.
Send a notification to the Discord webhook.
"""
if not WEBHOOK_URL:
logging.error("WEBHOOK_URL not set")
return
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"
}
}
embed = create_embed(message)
data = {"embeds": [embed]}
try:
response = requests.post(WEBHOOK_URL, json=data, timeout=10)
@@ -79,11 +120,35 @@ def send_notification(message: dict[str, int]) -> None:
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.
"""
send_notification(get_message("test"))
tz_list = []
for tz in pytz.all_timezones:
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:
@@ -91,19 +156,19 @@ def main() -> None:
Main function to run the scheduler.
"""
# Schedule notifications
schedule.every().hour.at(":15").do(send_notification, get_message("reminder"))
schedule.every().hour.at(":20").do(send_notification, get_message("notification"))
schedule.every().hour.at(":45").do(send_notification, get_message("reminder"))
schedule.every().hour.at(":50").do(send_notification, get_message("halftime"))
schedule.every().hour.at(":15").do(send_notification, "reminder")
schedule.every().hour.at(":20").do(send_notification, "420")
schedule.every().hour.at(":45").do(send_notification, "reminder_halftime")
schedule.every().hour.at(":50").do(send_notification, "halftime")
logging.info("Scheduler started.")
# Test the notification on startup
test_notification()
send_notification("420")
try:
while True:
schedule.run_pending()
time.sleep(30) # Check every 30 seconds
time.sleep(1) # Check every second
except KeyboardInterrupt:
logging.info("Scheduler stopped by user")
except Exception as e:

View File

@@ -1,3 +1,5 @@
requests
pytest
python-dotenv
pytz
requests
schedule

111
test_main.py Normal file
View 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()

View File

@@ -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

View 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
1 AF Afghanistan
2 AL Albania
3 DZ Algeria
4 AS American Samoa
5 AD Andorra
6 AO Angola
7 AI Anguilla
8 AQ Antarctica
9 AG Antigua and Barbuda
10 AR Argentina
11 AM Armenia
12 AW Aruba
13 AU Australia
14 AT Austria
15 AZ Azerbaijan
16 BS Bahamas
17 BH Bahrain
18 BD Bangladesh
19 BB Barbados
20 BY Belarus
21 BE Belgium
22 BZ Belize
23 BJ Benin
24 BM Bermuda
25 BT Bhutan
26 BO Bolivia, Plurinational State of
27 BQ Bonaire, Sint Eustatius and Saba
28 BA Bosnia and Herzegovina
29 BW Botswana
30 BR Brazil
31 IO British Indian Ocean Territory
32 BN Brunei Darussalam
33 BG Bulgaria
34 BF Burkina Faso
35 BI Burundi
36 KH Cambodia
37 CM Cameroon
38 CA Canada
39 CV Cape Verde
40 KY Cayman Islands
41 CF Central African Republic
42 TD Chad
43 CL Chile
44 CN China
45 CX Christmas Island
46 CC Cocos (Keeling) Islands
47 CO Colombia
48 KM Comoros
49 CG Congo
50 CD Congo, the Democratic Republic of the
51 CK Cook Islands
52 CR Costa Rica
53 HR Croatia
54 CU Cuba
55 CW Curaçao
56 CY Cyprus
57 CZ Czech Republic
58 CI Côte d'Ivoire
59 DK Denmark
60 DJ Djibouti
61 DM Dominica
62 DO Dominican Republic
63 EC Ecuador
64 EG Egypt
65 SV El Salvador
66 GQ Equatorial Guinea
67 ER Eritrea
68 EE Estonia
69 ET Ethiopia
70 FK Falkland Islands (Malvinas)
71 FO Faroe Islands
72 FJ Fiji
73 FI Finland
74 FR France
75 GF French Guiana
76 PF French Polynesia
77 TF French Southern Territories
78 GA Gabon
79 GM Gambia
80 GE Georgia
81 DE Germany
82 GH Ghana
83 GI Gibraltar
84 GR Greece
85 GL Greenland
86 GD Grenada
87 GP Guadeloupe
88 GU Guam
89 GT Guatemala
90 GG Guernsey
91 GN Guinea
92 GW Guinea-Bissau
93 GY Guyana
94 HT Haiti
95 VA Holy See (Vatican City State)
96 HN Honduras
97 HK Hong Kong
98 HU Hungary
99 IS Iceland
100 IN India
101 ID Indonesia
102 IR Iran, Islamic Republic of
103 IQ Iraq
104 IE Ireland
105 IM Isle of Man
106 IL Israel
107 IT Italy
108 JM Jamaica
109 JP Japan
110 JE Jersey
111 JO Jordan
112 KZ Kazakhstan
113 KE Kenya
114 KI Kiribati
115 KP Korea, Democratic People's Republic of
116 KR Korea, Republic of
117 KW Kuwait
118 KG Kyrgyzstan
119 LA Lao People's Democratic Republic
120 LV Latvia
121 LB Lebanon
122 LS Lesotho
123 LR Liberia
124 LY Libya
125 LI Liechtenstein
126 LT Lithuania
127 LU Luxembourg
128 MO Macao
129 MK Macedonia, the Former Yugoslav Republic of
130 MG Madagascar
131 MW Malawi
132 MY Malaysia
133 MV Maldives
134 ML Mali
135 MT Malta
136 MH Marshall Islands
137 MQ Martinique
138 MR Mauritania
139 MU Mauritius
140 YT Mayotte
141 MX Mexico
142 FM Micronesia, Federated States of
143 MD Moldova, Republic of
144 MC Monaco
145 MN Mongolia
146 ME Montenegro
147 MS Montserrat
148 MA Morocco
149 MZ Mozambique
150 MM Myanmar
151 NA Namibia
152 NR Nauru
153 NP Nepal
154 NL Netherlands
155 NC New Caledonia
156 NZ New Zealand
157 NI Nicaragua
158 NE Niger
159 NG Nigeria
160 NU Niue
161 NF Norfolk Island
162 MP Northern Mariana Islands
163 NO Norway
164 OM Oman
165 PK Pakistan
166 PW Palau
167 PS Palestine, State of
168 PA Panama
169 PG Papua New Guinea
170 PY Paraguay
171 PE Peru
172 PH Philippines
173 PN Pitcairn
174 PL Poland
175 PT Portugal
176 PR Puerto Rico
177 QA Qatar
178 RO Romania
179 RU Russian Federation
180 RW Rwanda
181 RE Réunion
182 BL Saint Barthélemy
183 SH Saint Helena, Ascension and Tristan da Cunha
184 KN Saint Kitts and Nevis
185 LC Saint Lucia
186 MF Saint Martin (French part)
187 PM Saint Pierre and Miquelon
188 VC Saint Vincent and the Grenadines
189 WS Samoa
190 SM San Marino
191 ST Sao Tome and Principe
192 SA Saudi Arabia
193 SN Senegal
194 RS Serbia
195 SC Seychelles
196 SL Sierra Leone
197 SG Singapore
198 SX Sint Maarten (Dutch part)
199 SK Slovakia
200 SI Slovenia
201 SB Solomon Islands
202 SO Somalia
203 ZA South Africa
204 GS South Georgia and the South Sandwich Islands
205 SS South Sudan
206 ES Spain
207 LK Sri Lanka
208 SD Sudan
209 SR Suriname
210 SJ Svalbard and Jan Mayen
211 SZ Swaziland
212 SE Sweden
213 CH Switzerland
214 SY Syrian Arab Republic
215 TW Taiwan, Province of China
216 TJ Tajikistan
217 TZ Tanzania, United Republic of
218 TH Thailand
219 TL Timor-Leste
220 TG Togo
221 TK Tokelau
222 TO Tonga
223 TT Trinidad and Tobago
224 TN Tunisia
225 TR Turkey
226 TM Turkmenistan
227 TC Turks and Caicos Islands
228 TV Tuvalu
229 UG Uganda
230 UA Ukraine
231 AE United Arab Emirates
232 GB United Kingdom
233 US United States
234 UM United States Minor Outlying Islands
235 UY Uruguay
236 UZ Uzbekistan
237 VU Vanuatu
238 VE Venezuela, Bolivarian Republic of
239 VN Viet Nam
240 VG Virgin Islands, British
241 VI Virgin Islands, U.S.
242 WF Wallis and Futuna
243 EH Western Sahara
244 YE Yemen
245 ZM Zambia
246 ZW Zimbabwe
247 AX Åland Islands

File diff suppressed because it is too large Load Diff