Compare commits

...

17 Commits

Author SHA1 Message Date
a270978728 Add remove repos button and change background color 2024-06-27 14:22:15 +02:00
2a7305a4cf Emergency PUSH 2024-03-05 13:00:23 +01:00
f5fc6e38da Add date and time of the release (use full when adding a new repo) 2024-03-05 12:51:15 +01:00
4a57e9e2e1 Comment everything from french to english 2024-03-04 13:18:13 +01:00
1ef3dfa49d Miss name variable that break all program 2024-03-04 13:08:00 +01:00
0e72fd80cc Add web interface to manage repo 2024-03-04 12:57:45 +01:00
3b6f55e703 Add web interface to manage repo 2024-03-04 12:34:36 +01:00
5b113d725b Merge remote-tracking branch 'origin/main' 2024-03-03 09:47:54 +01:00
9b2cf22892 Update to take markdown formating 2024-03-03 09:47:43 +01:00
Félix MARQUET
34b314d40f Update README.md 2024-03-02 16:57:11 +01:00
Félix MARQUET
7a1808569e Update README.md 2024-03-01 23:07:31 +01:00
Félix MARQUET
063c2db3a9 Update README.md 2024-03-01 21:29:22 +01:00
Félix MARQUET
f68bc12902 Update README.md 2024-03-01 21:29:08 +01:00
c728183bf4 Change db path 2024-03-01 20:10:20 +01:00
6da0b5dac0 Merge remote-tracking branch 'origin/main' 2024-03-01 19:30:19 +01:00
6c714abedd Add a button to go to the github page 2024-03-01 19:30:02 +01:00
Félix MARQUET
ecf162f6ed Update README.md 2024-03-01 18:59:35 +01:00
9 changed files with 337 additions and 38 deletions

View File

@@ -3,9 +3,12 @@ FROM python:3.11.8-alpine3.19
LABEL maintainer="BreizhHardware"
ADD ntfy.py /
ADD ntfy_api.py /
ADD requirements.txt /
ADD entrypoint.sh /
RUN apk add --no-cache sqlite-dev sqlite-libs gcc musl-dev
ADD index.html /var/www/html/index.html
ADD script.js /var/www/html/script.js
RUN apk add --no-cache sqlite-dev sqlite-libs gcc musl-dev nginx
RUN pip install -r requirements.txt
# Définir les variables d'environnement pour username et password
@@ -13,6 +16,12 @@ ENV USERNAME="" \
PASSWORD="" \
NTFY_URL="" \
GHNTFY_TIMEOUT="3600" \
GHREPO=""
GHNTFY_TOKEN="" \
FLASK_ENV=production
# Exposer le port 5000 pour l'API et le port 80 pour le serveur web
EXPOSE 5000 80
COPY nginx.conf /etc/nginx/nginx.conf
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,6 +1,7 @@
# ntfy_alerts
Personal ntfy alerts system
feel free to contribute and to fork
# Python ntfy.py
## Description:
@@ -28,15 +29,21 @@ services:
- PASSWORD=password # Required
- NTFY_URL=ntfy_url # Required
- GHNTFY_TIMEOUT=timeout # Default is 3600 (1 hour)
- GHREPO=["username/repo1", "username/repo2"] # Default is empty
- GHNTFY_TOKEN= # Default is empty (Github token)
volumes:
- /path/to/github-ntfy:/github-ntfy/
ports:
- 80:80
restart: unless-stopped
````
GHNTFY_TOKEN, need to have repo, read:org and read:user
Docker Hub repo: https://hub.docker.com/r/breizhhardware/github-ntfy
## TODO:
- [x] Dockerize the ntfy.py
- [x] Add the watched repos list as a parameter
- [x] Add the application version as a database
- [ ] Add the watched repos list as a web interface
- [x] Add the watched repos list as a web interface
# Bash setup-notify.sh
## Description:
This script is used to setup the ntfy notification system on ssh login for a new server.

View File

@@ -3,5 +3,8 @@
# Génère le contenu du fichier auth.txt à partir des variables d'environnement
echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt
# Démarrer nginx en arrière-plan
nginx -g 'daemon off;' &
# Exécute le script Python
exec python ./ntfy.py

43
index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Github-Ntfy Add a repo</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="./script.js" defer></script>
</head>
<body class="bg-gradient-to-b from-stone-500 to-green-700">
<div class="flex flex-col gap-2 justify-center items-center my-2 h-screen">
<h1 class="text-4xl font-semibold leading-10 text-gray-900">Github-Ntfy</h1>
<h1>Add a repo</h1>
<form id="addRepoForm">
<div class="space-y-12">
<div class="border-b border-gray-900/10 pb-12">
<h2 class="text-base font-semibold leading-7 text-gray-900">Name of the github repo</h2>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-4">
<div class="mt-2">
<div class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600 sm:max-w-md">
<span class="flex select-none items-center pl-3 sm:text-sm">github.com/</span>
<input type="text" name="repo" id="repo" autocomplete="repo" class="block flex-1 border-0 bg-transparent py-1.5 pl-1 placeholder:text-gray-600 focus:ring-0 sm:text-sm sm:leading-6" placeholder="BreizhHardware/ntfy_alerts">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button>
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Save</button>
</div>
</form>
<div class="mt-8">
<h2 class="text-base font-semibold leading-7 text-gray-900">Watched Repositories</h2>
<ul id="watchedReposList" class="mt-4">
<!-- Dynamically populated with JavaScript -->
</ul>
</div>
</div>
</body>
</html>

39
nginx.conf Normal file
View File

@@ -0,0 +1,39 @@
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 80;
location / {
root /var/www/html;
index index.html;
}
location /app_repo {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /watched_repos {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /delete_repo {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

92
ntfy.py
View File

@@ -2,10 +2,10 @@ import requests
import time
import os
import logging
import json
import sqlite3
import subprocess
# Configurer le logger
# Configuring the logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@@ -14,24 +14,36 @@ github_headers = {}
if github_token:
github_headers['Authorization'] = f"token {github_token}"
repo_list_env = os.environ.get('GHREPO')
watched_repos_list = json.loads(repo_list_env) if repo_list_env else []
if not watched_repos_list:
logger.error("Aucun dépôt n'a été spécifié. Veuillez spécifier les dépôts à surveiller dans l'environnement GHREPO")
exit(1)
# Connexion à la base de données pour stocker les versions précédentes
db_path = 'ghntfy_versions.db'
conn = sqlite3.connect(db_path)
# Connecting to the database to store previous versions
conn = sqlite3.connect('/github-ntfy/ghntfy_versions.db', check_same_thread=False)
cursor = conn.cursor()
# Création de la table si elle n'existe pas
# Creating the table if it does not exist
cursor.execute('''CREATE TABLE IF NOT EXISTS versions
(repo TEXT PRIMARY KEY, version TEXT, changelog TEXT)''')
conn.commit()
logger.info("Démarrage de la surveillance des versions...")
logger.info("Starting version monitoring...")
conn2 = sqlite3.connect('/github-ntfy/watched_repos.db', check_same_thread=False)
cursor2 = conn2.cursor()
cursor2.execute('''CREATE TABLE IF NOT EXISTS watched_repos
(id INTEGER PRIMARY KEY, repo TEXT)''')
conn2.commit()
def get_watched_repos():
cursor2.execute("SELECT * FROM watched_repos")
watched_repos_rows = cursor2.fetchall()
watched_repos = []
for repo in watched_repos_rows:
watched_repos.append(repo[1])
return watched_repos
def start_api():
subprocess.Popen(["python", "ntfy_api.py"])
def get_latest_releases(watched_repos):
@@ -42,12 +54,14 @@ def get_latest_releases(watched_repos):
if response.status_code == 200:
release_info = response.json()
changelog = get_changelog(repo)
release_date = release_info.get('published_at', 'Release date not available')
releases.append({
"repo": repo,
"name": release_info["name"],
"tag_name": release_info["tag_name"],
"html_url": release_info["html_url"],
"changelog": changelog
"changelog": changelog,
"published_at": release_date
})
else:
logger.error(f"Failed to fetch release info for {repo}")
@@ -60,42 +74,50 @@ def get_changelog(repo):
if response.status_code == 200:
releases = response.json()
if releases:
latest_release = releases[0]
if 'body' in latest_release:
return latest_release['body']
return "Changelog non disponible"
latest_release_list = releases[0]
if 'body' in latest_release_list:
return latest_release_list['body']
return "Changelog not available"
def send_to_ntfy(releases, auth, url):
for release in releases:
app_name = release['repo'].split('/')[-1] # Obtenir le nom de l'application à partir du repo
version_number = release['tag_name'] # Obtenir le numéro de version
app_url = release['html_url'] # Obtenir l'URL de l'application
changelog = release['changelog'] # Obtenir le changelog
app_name = release['repo'].split('/')[-1] # Getting the application name from the repo
version_number = release['tag_name'] # Getting the version number
app_url = release['html_url'] # Getting the application URL
changelog = release['changelog'] # Getting the changelog
release_date = release['published_at'] # Getting the release date
release_date = release_date.replace("T", " ").replace("Z", "") # Formatting the release date
# Vérifier si la version a changé depuis la dernière fois
# Checking if the version has changed since the last time
cursor.execute("SELECT version FROM versions WHERE repo=?", (app_name,))
previous_version = cursor.fetchone()
if previous_version and previous_version[0] == version_number:
logger.info(f"La version de {app_name} n'a pas changé. Pas de notification envoyée.")
continue # Passer à l'application suivante
logger.info(f"The version of {app_name} has not changed. No notification sent.")
continue # Move on to the next application
message = f"Nouvelle version: {version_number}\nPour: {app_name}\nChangelog:\n{changelog}\n{app_url}"
# Mettre à jour la version précédente pour cette application
message = f"New version: {version_number}\nFor: {app_name}\nPublished on: {release_date}\nChangelog:\n{changelog}\n{app_url}"
# Updating the previous version for this application
cursor.execute("INSERT OR REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)",
(app_name, version_number, changelog))
conn.commit()
headers = {"Authorization": f"Basic {auth}", "Content-Type": "text/plain"}
headers = {
"Authorization": f"Basic {auth}",
"Title": f"New version for {app_name}",
"Priority": "urgent",
"Markdown": "yes",
"Actions": f"view, Update {app_name}, {app_url}, clear=true"}
response = requests.post(f"{url}", headers=headers, data=message)
if response.status_code == 200:
logger.info(f"Message envoyé à Ntfy pour {app_name}")
logger.info(f"Message sent to Ntfy for {app_name}")
continue
else:
logger.error(f"Échec de l'envoi du message à Ntfy. Code d'état : {response.status_code}")
logger.error(f"Failed to send message to Ntfy. Status code: {response.status_code}")
if __name__ == "__main__":
start_api()
with open('/auth.txt', 'r') as f:
auth = f.read().strip()
ntfy_url = os.environ.get('NTFY_URL')
@@ -103,11 +125,15 @@ if __name__ == "__main__":
if auth and ntfy_url:
while True:
watched_repos_list = get_watched_repos()
latest_release = get_latest_releases(watched_repos_list)
if latest_release:
send_to_ntfy(latest_release, auth, ntfy_url)
time.sleep(timeout) # Attendre une heure avant de vérifier à nouveau
time.sleep(timeout) # Wait an hour before checking again
else:
logger.error("Usage: python ntfy.py")
logger.error("auth: can be generataed by the folowing command: echo -n 'username:password' | base64 and need to be stored in a file named auth.txt")
logger.error(
"auth: can be generataed by the folowing command: echo -n 'username:password' | base64 and need to be "
"stored in a file named auth.txt")
logger.error("NTFY_URL: the url of the ntfy server need to be stored in an environment variable named NTFY_URL")

89
ntfy_api.py Normal file
View File

@@ -0,0 +1,89 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import sqlite3
app = Flask(__name__)
CORS(app)
app.logger.setLevel("WARNING")
def get_db_connection():
conn = sqlite3.connect('/github-ntfy/watched_repos.db')
conn.row_factory = sqlite3.Row
return conn
def close_db_connection(conn):
conn.close()
@app.route('/app_repo', methods=['POST'])
def app_repo():
data = request.json
repo = data.get('repo')
# Vérifier si le champ 'repo' est présent dans les données JSON
if not repo:
return jsonify({"error": "The repo field is required."}), 400
# Établir une connexion à la base de données
conn = get_db_connection()
cursor = conn.cursor()
try:
# Vérifier si le dépôt existe déjà dans la base de données
cursor.execute("SELECT * FROM watched_repos WHERE repo=?", (repo,))
existing_repo = cursor.fetchone()
if existing_repo:
return jsonify({"error": f"The repo {repo} is already in the database."}), 409
# Ajouter le dépôt à la base de données
cursor.execute("INSERT INTO watched_repos (repo) VALUES (?)", (repo,))
conn.commit()
return jsonify({"message": f"The repo {repo} as been added to the watched repos."})
finally:
# Fermer la connexion à la base de données
close_db_connection(conn)
@app.route('/watched_repos', methods=['GET'])
def get_watched_repos():
db = get_db_connection()
cursor = db.cursor()
cursor.execute("SELECT repo FROM watched_repos")
watched_repos = [repo[0] for repo in cursor.fetchall()]
cursor.close()
db.close()
return jsonify(watched_repos)
@app.route('/delete_repo', methods=['POST'])
def delete_repo():
data = request.json
repo = data.get('repo')
# Vérifier si le champ 'repo' est présent dans les données JSON
if not repo:
return jsonify({"error": "The repo field is required."}), 400
# Établir une connexion à la base de données
conn = get_db_connection()
cursor = conn.cursor()
try:
# Vérifier si le dépôt existe dans la base de données
cursor.execute("SELECT * FROM watched_repos WHERE repo=?", (repo,))
existing_repo = cursor.fetchone()
if not existing_repo:
return jsonify({"error": f"The repo {repo} is not in the database."}), 404
# Supprimer le dépôt de la base de données
cursor.execute("DELETE FROM watched_repos WHERE repo=?", (repo,))
conn.commit()
return jsonify({"message": f"The repo {repo} as been deleted from the watched repos."})
finally:
# Fermer la connexion à la base de données
close_db_connection(conn)
if __name__ == "__main__":
app.run(debug=False)

View File

@@ -1,2 +1,4 @@
requests==2.31.0
pysqlite3==0.5.2
pysqlite3==0.5.2
flask==3.0.2
flask-cors==4.0.0

81
script.js Normal file
View File

@@ -0,0 +1,81 @@
document.getElementById('addRepoForm').addEventListener('submit', function(event) {
event.preventDefault();
let repoName = document.getElementById('repo').value;
fetch('/app_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repoName})
})
.then(response => {
if (response.ok) {
// Si la requête s'est bien déroulée, actualiser la liste des dépôts surveillés
refreshWatchedRepos();
} else {
throw new Error('Erreur lors de l\'ajout du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
});
function refreshWatchedRepos() {
fetch('/watched_repos')
.then(response => response.json())
.then(data => {
const watchedReposList = document.getElementById('watchedReposList');
// Vider la liste actuelle
watchedReposList.innerHTML = '';
// Ajouter chaque dépôt surveillé à la liste
data.forEach(repo => {
const listItem = document.createElement('li');
const repoName = document.createElement('span');
repoName.textContent = repo;
repoName.className = 'repo-name';
listItem.appendChild(repoName);
const deleteButton = document.createElement('button');
deleteButton.textContent = ' X';
deleteButton.className = 'delete-btn text-red-500 ml-2';
deleteButton.addEventListener('click', () => {
// Remove the repo from the watched repos
// This is a placeholder. Replace it with your actual code to remove the repo from the watched repos.
removeRepoFromWatchedRepos(repo);
// Remove the repo from the DOM
listItem.remove();
});
listItem.appendChild(deleteButton);
watchedReposList.appendChild(listItem);
});
})
.catch(error => {
console.error('Error:', error);
});
}
function removeRepoFromWatchedRepos(repo) {
fetch('/delete_repo', {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({repo: repo})
})
.then(response => {
if (!response.ok) {
throw new Error('Erreur lors de la suppression du dépôt');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Appeler la fonction pour charger les dépôts surveillés au chargement de la page
refreshWatchedRepos();