forked from Mirroring/github-release-notifier
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a43d462bf1 | |||
| 2d48a37ccb |
@@ -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"]
|
||||
|
||||
4
Justfile
4
Justfile
@@ -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
|
||||
|
||||
54
README.md
54
README.md
@@ -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!
|
||||
|
||||
|
||||
4
conf.ini
4
conf.ini
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
93
notifier.py
93
notifier.py
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
black
|
||||
fastapi
|
||||
pre-commit
|
||||
pyotp
|
||||
requests
|
||||
uvicorn[standard]
|
||||
pre-commit
|
||||
black
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user