diff --git a/Dockerfile b/Dockerfile
index e568243..9158e76 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,12 @@
FROM python:3.10-alpine3.18
-RUN pip install requests
WORKDIR /app
+COPY requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir --upgrade --requirement requirements.txt
COPY notifier.py template.html /app/
# TODO Dev purporse
COPY conf.ini /app/conf.ini
-ENTRYPOINT ["python3", "/app/notifier.py"]
+EXPOSE 80
+CMD ["uvicorn", "notifier:app", "--host", "0.0.0.0", "--port", "80"]
diff --git a/Justfile b/Justfile
index 609eeaf..633cc68 100644
--- a/Justfile
+++ b/Justfile
@@ -12,6 +12,10 @@ remote_build_image := remote_image_name + ":" + last_commit_sha1
run: _ensure_venv_is_ok
{{ python }} notifier.py
+# Launch the API
+api: _ensure_venv_is_ok
+ {{ venv }}/bin/uvicorn notifier:app --reload
+
# Init python virtual env
init:
python3 -m venv venv
@@ -59,8 +63,9 @@ dpush: dbuild
docker push {{ remote_build_image }}
echo "To push a tagged version, do 'just release '"
-# Release a version: create a tagged images, push it and create a git tag
+# Use just a number! Without 'v'! Release a version - create a tagged images, push it and create git tag.
release version: dbuild
docker tag github-release-notifier {{ remote_image_name }}:{{ version }}
docker push {{ remote_image_name }}:{{ version }}
git tag -a v{{ version }} -m ""
+ git push --tags
diff --git a/README.md b/README.md
index eedd120..b90125c 100644
--- a/README.md
+++ b/README.md
@@ -12,33 +12,71 @@ Why using configuration?...
------------------------
...instead use stared project of your Github account?
Cause I've got about 80 "stared" projects and I don't wan't to be alerted for new releases of each of these project.
-But, perhaps I'll add such a feature later on...
And, Github API limits is at 60 requests by seconds, and I want to write this script really quickly in a first time.
+But, perhaps I'll add such a feature later on...
+
How to use?
-----------
Really simple!
-* Edit conf.ini file to set `[config]` section
+* Install dependances: `pip install -r requirements.txt`
+* Edit conf.ini file to set `[config]` section:
* your SMTP server configuration (host and port)
* sender mail
* receiver mail (:warning: not tested with more than 1 receiver)
-* Add the projects you want to follow in the section `[project]`
+* ...or use environment variable - same name but in upper case e.g. `SMTP_PORT`
+* Add projects you want to follow in the section `[project]`
* Be careful to follow a JSON valid syntax as in the provided file, i.e. coma after each `autor/project` except the last one.
+Execute the script: `python notifier.py`
+
After first execution, the `conf.ini` file will be filled with last release tag found by the script, as you can see in the provided file.
Hope you like, and have fun to read your mail!
+Hey, what is this API?
+--------------------------
+Since it's not really conveniant to go on a VM and edit the config.ini every time I find a cool new project whose new versions I want to keep track of, there is an API to do this.
+
+This is a really simple thing with no verification: you can add everything that you want!
+
+Just one endpoint: `subscription`, and two method: GET and PUT.
+PUT take three parameters:
+* `project`: full project name e.g. `MaxenceG2M/github-release-notifier` or "short" name (without author) e.g. `github-release-notifier`
+* (optionnal) `author`: the author of the repository
+* (optionnal) `credentials`: just the TOTP check code.
+
+Indeed, to have a minimal better-than-nothing protection, I opted for a simple TOTP code. No user/password or wathever.
+You can disable-it in the `config.ini`, or specify a custom key.
+If you don't specify a key, information will be displayed at startup (URL and the actual code. Be quick to check if it's ok!).
+
+To start the API:
+```sh
+$ uvicorn notifier:app
+TOTP enabled.
+ Information: otpauth://totp/Secret?secret=mysuperkey
+ TOTP check: 377826
+INFO: Started server process [28264]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+```
+
+Cron-it with [ofelia](https://github.com/mcuadros/ofelia)
+---------------------------------------------------------
+Ofelia is very conveniant for croning the verification part while letting the API run continuously.
+So the `docker-compose.yml` use it to do this :)
+
+<3
+
Problems I have to solve really quickly
---------------------------------------
-I wrote this script really quickly, certainly faster than this README. So I already have two big proglems:
-* The script sends mail even if no new projets or release has been detected
-* The biggest, you have to edit script to specify absolute path...
-* A lot of other little problems, like the code that's disgusting and so on.
+One script to do all things.
-For who's asking: yes, it's normal that I have put all code in one main function [like a blind gunner.](https://media.giphy.com/media/1yMexL5rkwYhuiVEmZ/giphy.gif). Really quickly I said!
+For who's asking: yes, it's normal that I have put all code in ~~one main function~~ one main script [like a blind gunner.](https://media.giphy.com/media/1yMexL5rkwYhuiVEmZ/giphy.gif).
+I want to keep this script as simple as possible.
But overall, the script works and sends mail!
diff --git a/conf.ini b/conf.ini
index c0867e8..354a048 100644
--- a/conf.ini
+++ b/conf.ini
@@ -5,6 +5,10 @@ smtp_server = mailpit
sender_email = sender@host.eu
receiver_email = receiver@anotherhost.eu
+[api.totp]
+enabled = true
+key = mysuperkey
+
[projects]
projects = [
"borgbackup/borg",
diff --git a/docker-compose.yml b/docker-compose.yml
index 4eb8239..ac4399c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,13 +6,15 @@ services:
image: github-release-notifier
container_name: github-release-notifier
volumes:
- - ./conf.ini:/app/conf.ini
+ - ./conf.ini:/app/conf.ini
+ ports:
+ - 8000:80
mailpit:
image: axllent/mailpit
ports:
- - "8025:8025"
- - "1025:1025"
+ - 8025:8025
+ - 1025:1025
ofelia:
image: mcuadros/ofelia:latest
diff --git a/notifier.py b/notifier.py
index d576fb0..bd7453d 100644
--- a/notifier.py
+++ b/notifier.py
@@ -7,7 +7,17 @@ import sys
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
+import pyotp
import requests
+from fastapi import FastAPI, status
+from fastapi.responses import JSONResponse
+
+#
+# CONF PART
+#
+SCRIPT_DIR = os.path.dirname(__file__)
+CONF_FILE = os.path.join(SCRIPT_DIR, "conf.ini")
+TEMPLATE_FILE = os.path.join(SCRIPT_DIR, "template.html")
class EnvInjection(configparser.Interpolation):
@@ -26,15 +36,79 @@ class EnvInjection(configparser.Interpolation):
return env_value if env_value else file_value
-def main():
- script_dir = os.path.dirname(__file__)
- conf_file = os.path.join(script_dir, "conf.ini")
- template_file = os.path.join(script_dir, "template.html")
-
+def load_conf(conf_file=CONF_FILE) -> configparser.ConfigParser:
parser = configparser.ConfigParser(
default_section="config", interpolation=EnvInjection()
)
parser.read(conf_file)
+ return parser
+
+
+def load_totp():
+ parser = load_conf()
+
+ if parser.getboolean("api.totp", "enabled", fallback=True):
+ totp = pyotp.TOTP(parser.get("api.totp", "key", fallback=pyotp.random_base32()))
+ # TODO Catch totp.now() error
+ print(
+ f"""TOTP enabled.
+ Information: {totp.provisioning_uri()}
+ TOTP check: {totp.now()}"""
+ )
+ return totp
+ else:
+ print("WARNING: Api is open without security")
+ return type("totp", (object,), {"verify": lambda str: True})
+
+
+#
+# API PARTS
+#
+app = FastAPI()
+TOTP = load_totp()
+
+
+@app.get("/subscriptions")
+def get_projects():
+ parser = load_conf()
+ projects = json.loads(parser["projects"].get("projects"))
+ return projects
+
+
+@app.put("/subscriptions")
+def put_projects(
+ project: str, author: str | None = None, credentials: str | None = None
+):
+ if not TOTP.verify(credentials):
+ return JSONResponse(
+ status_code=status.HTTP_401_UNAUTHORIZED, content="Unauthorized"
+ )
+ # TODO Check if project really exist?
+ parser = load_conf()
+ projects = json.loads(parser.get("projects", "projects"))
+
+ if author:
+ project = f"{author}/{project}"
+
+ if project in projects:
+ return project
+
+ projects.append(project)
+
+ # TODO Watch a converter for list: https://stackoverflow.com/a/53274707/1346391
+ parser.set("projects", "projects", json.dumps(projects, indent=0))
+
+ with open("conf.ini", "w", encoding="utf-8") as configfile:
+ parser.write(configfile)
+
+ return JSONResponse(status_code=status.HTTP_201_CREATED, content=project)
+
+
+#
+# SCRIPT PART
+#
+def main():
+ parser = load_conf()
default_config = parser["config"]
try:
@@ -67,27 +141,31 @@ def main():
return
content = ""
+ news = []
for new_r in new_releases:
+ news.append(new_r["project_name"])
content += f"""
- {new_r["project_name"]}
+ {get_html_project_link(new_r)}
: new release
- {new_r["release_tag"]}
+ {get_html_release_link(new_r)}
available (old: {new_r["previous_tag"]}).
(published {convert_date(new_r["published_date"])})"""
for new_p in new_projects:
+ news.append(new_p["project_name"])
content += f"""
- {new_p["project_name"]}
+ {get_html_project_link(new_p)}
was added to your configuration.
Last release:
- {new_p["release_tag"]}
+ {get_html_release_link(new_p)}
(published {convert_date(new_p["published_date"])})"""
- with open(template_file, "r", encoding="utf-8") as f_template:
+ with open(TEMPLATE_FILE, "r", encoding="utf-8") as f_template:
template = f_template.read()
send_mail(template.replace("{{content}}", content), default_config)
+ print(f"Send a mail for new releases and projects on: {', '.join(news)}")
with open("conf.ini", "w", encoding="utf-8") as configfile:
parser.write(configfile)
@@ -97,8 +175,7 @@ def get_last_release(project):
url = f"https://api.github.com/repos/{project}/releases/latest"
result = requests.get(url, timeout=10)
- print(project)
- print(url)
+ print(f"Check {project} - {url}")
release = result.json()
release_tag = release["tag_name"]
published_date = release["published_at"]
@@ -114,6 +191,15 @@ def get_last_release(project):
}
+def get_html_project_link(el):
+ project_url = f'https://github.com/{el["project_name"]}'
+ return f'{el["project_name"]}'
+
+
+def get_html_release_link(el):
+ return f'{el["release_tag"]}'
+
+
def send_mail(content, config):
smtp_port = config.get("smtp_port")
smtp_server = config.get("smtp_server")
diff --git a/ofelia.ini b/ofelia.ini
index 808e4a7..77f096a 100644
--- a/ofelia.ini
+++ b/ofelia.ini
@@ -4,6 +4,7 @@ smtp-port = 1025
email-to = max@ence.fr
email-from = ofelia@container.sh
-[job-run "notifier"]
+[job-exec "notifier"]
schedule = @every 30s
container = github-release-notifier
+command = python3 /app/notifier.py
diff --git a/requirements.txt b/requirements.txt
index 49977e2..227cc3e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,6 @@
-requests
-pre-commit
black
+fastapi
+pre-commit
+pyotp
+requests
+uvicorn[standard]
diff --git a/template.html b/template.html
index 60851ce..71a5ec5 100644
--- a/template.html
+++ b/template.html
@@ -5,62 +5,13 @@
-
-
-
- Some new release on Github project available!
-
-
-
+
+
+
+ Some new release on Github project available!
+
+