2 Commits
api ... v3

Author SHA1 Message Date
a43d462bf1 Justfile: improve release process & doc 2024-01-23 01:43:25 +01:00
2d48a37ccb Improve HTML template + improve links
Reduce size (from ~15kB to ~4kB)
Reduce incompatibility for emails

Link to project on project name
2024-01-23 01:43:25 +01:00
9 changed files with 56 additions and 167 deletions

View File

@@ -1,12 +1,10 @@
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
EXPOSE 80
CMD ["uvicorn", "notifier:app", "--host", "0.0.0.0", "--port", "80"]
ENTRYPOINT ["python3", "/app/notifier.py"]

View File

@@ -12,10 +12,6 @@ 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

View File

@@ -12,71 +12,33 @@ 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!
* Install dependances: `pip install -r requirements.txt`
* Edit conf.ini file to set `[config]` section:
* 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)
* ...or use environment variable - same name but in upper case e.g. `SMTP_PORT`
* Add projects you want to follow in the section `[project]`
* Add the 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
---------------------------------------
One script to do all things.
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.
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.
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!
But overall, the script works and sends mail!

View File

@@ -5,10 +5,6 @@ smtp_server = mailpit
sender_email = sender@host.eu
receiver_email = receiver@anotherhost.eu
[api.totp]
enabled = true
key = mysuperkey
[projects]
projects = [
"borgbackup/borg",

View File

@@ -6,15 +6,13 @@ services:
image: github-release-notifier
container_name: github-release-notifier
volumes:
- ./conf.ini:/app/conf.ini
ports:
- 8000:80
- ./conf.ini:/app/conf.ini
mailpit:
image: axllent/mailpit
ports:
- 8025:8025
- 1025:1025
- "8025:8025"
- "1025:1025"
ofelia:
image: mcuadros/ofelia:latest

View File

@@ -7,17 +7,7 @@ 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):
@@ -36,79 +26,15 @@ class EnvInjection(configparser.Interpolation):
return env_value if env_value else file_value
def load_conf(conf_file=CONF_FILE) -> configparser.ConfigParser:
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")
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:
@@ -141,10 +67,8 @@ def main():
return
content = ""
news = []
for new_r in new_releases:
news.append(new_r["project_name"])
content += f"""
<li>{get_html_project_link(new_r)}
: new release
@@ -153,7 +77,6 @@ def main():
(published {convert_date(new_r["published_date"])})</li>"""
for new_p in new_projects:
news.append(new_p["project_name"])
content += f"""
<li>{get_html_project_link(new_p)}
was added to your configuration.
@@ -161,11 +84,10 @@ def main():
{get_html_release_link(new_p)}
(published {convert_date(new_p["published_date"])})</li>"""
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)
@@ -175,7 +97,8 @@ def get_last_release(project):
url = f"https://api.github.com/repos/{project}/releases/latest"
result = requests.get(url, timeout=10)
print(f"Check {project} - {url}")
print(project)
print(url)
release = result.json()
release_tag = release["tag_name"]
published_date = release["published_at"]

View File

@@ -4,7 +4,6 @@ smtp-port = 1025
email-to = max@ence.fr
email-from = ofelia@container.sh
[job-exec "notifier"]
[job-run "notifier"]
schedule = @every 30s
container = github-release-notifier
command = python3 /app/notifier.py

View File

@@ -1,6 +1,3 @@
black
fastapi
pre-commit
pyotp
requests
uvicorn[standard]
pre-commit
black

View File

@@ -1,17 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<head>
<meta charset="utf-8">
<title></title>
<style>
.markdown-body a,.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}body{margin:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;max-width:980px;padding:45px}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.6;word-wrap:break-word}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects;color:#4078c0}.markdown-body a:hover{outline-width:0;text-decoration:underline}.markdown-body strong{font-weight:bolder}.markdown-body h1{margin:.67em 0}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:1em;margin-bottom:16px;font-weight:700;line-height:1.4}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2.25em;line-height:1.2}.markdown-body h1 .anchor,.markdown-body h2 .anchor{line-height:1}.markdown-body h2{font-size:1.75em;line-height:1.225}.markdown-body h3{font-size:1.5em;line-height:1.43}.markdown-body h3 .anchor,.markdown-body h4 .anchor{line-height:1.2}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body hr{border-bottom-color:#eee}
</style>
</head>
<div class="markdown-body wysiwyg-content">
<h1 id="-some-new-release-on-github-project-available-">
<style>
.markdown-body a,.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor {text-decoration: none}
body {margin: auto;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;max-width: 980px;padding: 45px}
.markdown-body {-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #333;font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size: 16px;line-height: 1.6;word-wrap: break-word}
.markdown-body a {background-color: transparent;-webkit-text-decoration-skip: objects;color: #4078c0}
.markdown-body a:hover {outline-width: 0;text-decoration: underline}
.markdown-body strong {font-weight: bolder}
.markdown-body h1 {margin: .67em 0}
.markdown-body h1,.markdown-body h2 {padding-bottom: .3em;border-bottom: 1px solid #eee}
.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6 {margin-top: 1em;margin-bottom: 16px;font-weight: 700;line-height: 1.4}
.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link {color: #000;vertical-align: middle;visibility: hidden}
.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link {visibility: visible}
.markdown-body h1 {font-size: 2.25em;line-height: 1.2}
.markdown-body h1 .anchor,.markdown-body h2 .anchor {line-height: 1}
.markdown-body h2 {font-size: 1.75em;line-height: 1.225}
.markdown-body h3 {font-size: 1.5em;line-height: 1.43}
.markdown-body h3 .anchor,.markdown-body h4 .anchor {line-height: 1.2}
.markdown-body .octicon {display: inline-block;vertical-align: text-top;fill: currentColor}
.markdown-body hr {border-bottom-color: #eee}
</style>
</head>
<div class="markdown-body wysiwyg-content">
<h1 id="-some-new-release-on-github-project-available-">
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" height="32">
Some new release on Github project available!</h1>
<p><ul>{{content}}</ul></p>
</div>
Some new release on Github project available!
</h1>
<p>
<ul>{{content}}</ul>
</p>
</div>
</html>