From 1b6536d8ae0c23bf6eb6456554007e2568351788 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Wed, 27 Mar 2024 16:59:22 +1100 Subject: [PATCH] feat: Add nostr --- main.py | 139 +++++++++++++++++++++++++++- nostr.py | 29 ++++++ render.py | 2 +- requirements.txt | 3 +- templates/404.html | 8 ++ templates/assets/css/styles.min.css | 2 +- templates/index.html | 16 ++++ templates/nostr.html | 111 ++++++++++++++++++++++ templates/page.html | 4 + templates/page_no_image.html | 4 + templates/publishing.html | 8 ++ templates/site.html | 10 +- 12 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 nostr.py create mode 100644 templates/nostr.html diff --git a/main.py b/main.py index d716fcb..c042913 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import render import secrets import nginx import threading +import nostr as nostr_module app = Flask(__name__) dotenv.load_dotenv() @@ -109,7 +110,8 @@ def site(): "btn_bg": "#2c54cf", "btn_fg": "#ffffff", "socials": [], - "address": [] + "address": [], + "nostrs": [] } @@ -298,6 +300,104 @@ def publish(): response.set_cookie('auth', '', expires=0) return response +@app.route('/nostr') +def nostr(): + if 'auth' not in request.cookies: + return redirect('/') + auth = request.cookies['auth'] + + for i in cookies: + if i['cookie'] == auth: + # Load site content + if os.path.isfile(f'sites/{i["name"]}.json'): + with open(f'sites/{i["name"]}.json') as file: + data = json.load(file) + nostr = [] + if 'nostr' in data: + nostr = data['nostr'] + + return render_template('nostr.html',year=datetime.datetime.now().year, domain=i['name'],nostr=nostr) + + response = make_response(redirect('/')) + response.set_cookie('auth', '', expires=0) + return response + +@app.route('/nostr', methods=['POST']) +def nostr_post(): + if 'auth' not in request.cookies: + return redirect('/') + auth = request.cookies['auth'] + + for i in cookies: + if i['cookie'] == auth: + # Get site content + if os.path.isfile(f'sites/{i["name"]}.json'): + with open(f'sites/{i["name"]}.json') as file: + data = json.load(file) + else: + return redirect('/site') + + nostr = [] + if 'nostr' in data: + nostr = data['nostr'] + + # Check for new nostr links + if 'new-name' in request.form and 'new-pub' in request.form: + name = request.form['new-name'] + pub = request.form['new-pub'] + id = len(nostr) + for link in nostr: + if link['name'] == name: + link['pub'] = pub + data['nostr'] = nostr + with open(f'sites/{i["name"]}.json', 'w') as file: + json.dump(data, file) + return redirect('/nostr') + if link['id'] >= id: + id = link['id'] + 1 + + nostr.append({'name': name, 'pub': pub, 'id': id}) + + + data['nostr'] = nostr + with open(f'sites/{i["name"]}.json', 'w') as file: + json.dump(data, file) + return redirect('/nostr') + + response = make_response(redirect('/')) + response.set_cookie('auth', '', expires=0) + return response + +@app.route('/nostr/delete/') +def nostr_delete(id): + if 'auth' not in request.cookies: + return redirect('/') + auth = request.cookies['auth'] + + for i in cookies: + if i['cookie'] == auth: + # Get site content + if os.path.isfile(f'sites/{i["name"]}.json'): + with open(f'sites/{i["name"]}.json') as file: + data = json.load(file) + else: + return redirect('/site') + + nostr = [] + if 'nostr' in data: + nostr = data['nostr'] + + nostr = [i for i in nostr if i['id'] != id] + data['nostr'] = nostr + with open(f'sites/{i["name"]}.json', 'w') as file: + json.dump(data, file) + return redirect('/nostr') + + response = make_response(redirect('/')) + response.set_cookie('auth', '', expires=0) + return response + + @app.route('/.well-known/wallets/') def wallets(path): # Check if host is in domains @@ -332,6 +432,43 @@ def wallets(path): response.headers['Content-Type'] = 'text/plain' return response return render_template('404.html', year=datetime.datetime.now().year), 404 + +@app.route('/.well-known/nostr.json') +def nostr_account(): + # Check if host is in domains + if request.host in DOMAINS: + # Check if user is logged in + if 'auth' not in request.cookies: + return redirect(f'https://{DOMAINS[0]}') + auth = request.cookies['auth'] + for i in cookies: + if i['cookie'] == auth: + # Load site content + if os.path.isfile(f'sites/{i["name"]}.json'): + with open(f'sites/{i["name"]}.json') as file: + data = json.load(file) + if 'nostr' in data: + nostr = data['nostr'] + # Return as plain text + response = make_response(nostr_module.json(nostr)) + response.headers['Content-Type'] = 'text/plain' + response.headers.add('Access-Control-Allow-Origin', '*') + return response + + # Get wallet from domain + host = request.host.split(':')[0] + + if os.path.isfile(f'sites/{host}.json'): + with open(f'sites/{host}.json') as file: + data = json.load(file) + if 'nostr' in data: + nostr = data['nostr'] + # Return as plain text + response = make_response(nostr_module.json(nostr)) + response.headers['Content-Type'] = 'text/plain' + response.headers.add('Access-Control-Allow-Origin', '*') + return response + return render_template('404.html', year=datetime.datetime.now().year), 404 @app.route('/publishing') diff --git a/nostr.py b/nostr.py new file mode 100644 index 0000000..a3ece67 --- /dev/null +++ b/nostr.py @@ -0,0 +1,29 @@ +from base64 import b32decode +from base64 import b16encode +from bech32 import bech32_decode +from bech32 import bech32_encode + +B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + +def add_padding(base32_len): + bits = base32_len * 5 + padding_size = (8 - (bits % 8)) % 8 + return padding_size + +def npub_to_hex(npub): + hrd, data = bech32_decode(npub) + b32_data = [B32[index] for index in data] + data_str = "".join(b32_data) + data_length = len(data_str) + data_str += "=" * add_padding(data_length) + decoded_data = b32decode(data_str) + b16_encoded_data = b16encode(decoded_data) + hex_str = b16_encoded_data.decode("utf-8").lower() + return hex_str + +def json(links): + names = {} + for link in links: + names[link['name']] = npub_to_hex(link['pub']) + + return {'names':names} \ No newline at end of file diff --git a/render.py b/render.py index cc69e3c..40a7a9d 100644 --- a/render.py +++ b/render.py @@ -63,7 +63,7 @@ def address_links(addresses,foreground): html = '' for address in addresses: token = address['token'].upper() - html += f'{tokenImage(token,foreground)}' + html += f'{tokenImage(token,foreground)}' return html def tokenImage(token,foreground): diff --git a/requirements.txt b/requirements.txt index 5ab19ae..dd2c841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ flask python-dotenv gunicorn requests -apscheduler \ No newline at end of file +apscheduler +bech32 \ No newline at end of file diff --git a/templates/404.html b/templates/404.html index bd316d5..73ddffd 100644 --- a/templates/404.html +++ b/templates/404.html @@ -5,7 +5,15 @@ HNS Links + + + + + + + + diff --git a/templates/assets/css/styles.min.css b/templates/assets/css/styles.min.css index 556e924..ba20759 100644 --- a/templates/assets/css/styles.min.css +++ b/templates/assets/css/styles.min.css @@ -1 +1 @@ -.video-container{position:relative;padding-bottom:56.25%;padding-top:0;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.social-icons{color:#313437;background-color:#fff;padding:70px 0}@media (max-width:767px){.social-icons{padding:50px 0}}.social-div{display:flex;justify-content:center;align-items:center}.social-list{max-width:100%;display:flex;list-style:none;gap:2.5rem}@media (max-width:500px){.social-list{gap:1.5rem}}@media (max-width:420px){.social-list{gap:0}}.custom-button{max-width:500px}.addresses{margin-bottom:20px;display:flex;justify-content:center;align-items:center}.social-icons i{color:#757980;margin:0 10px;width:60px;height:60px;border:1px solid #c8ced7;text-align:center;border-radius:50%;line-height:60px;display:inline-block}.social-link a{text-decoration:none;width:4.8rem;height:4.8rem;background-color:#f0f9fe;border-radius:50%;display:flex;justify-content:center;align-items:center;position:relative;z-index:1;border:3px solid #f0f9fe;overflow:hidden}.social-link a::before{content:"";position:absolute;width:100%;height:100%;background:#000;z-index:0;scale:1 0;transform-origin:bottom;transition:scale .5s}.social-link:hover a::before{scale:1 1}.icon{font-size:2rem;color:#011827;transition:.5s;z-index:2}.social-link a:hover .icon{color:#fff;transform:rotateY(360deg)} \ No newline at end of file +.video-container{position:relative;padding-bottom:56.25%;padding-top:0;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.nostr{margin-top:1em}.social-icons{color:#313437;background-color:#fff;padding:70px 0}@media (max-width:767px){.social-icons{padding:50px 0}}.social-div{display:flex;justify-content:center;align-items:center}.social-list{max-width:100%;display:flex;list-style:none;gap:2.5rem}@media (max-width:500px){.social-list{gap:1.5rem}}@media (max-width:420px){.social-list{gap:0}}.custom-button{max-width:500px}.addresses{margin-bottom:20px;display:flex;justify-content:center;align-items:center}.social-icons i{color:#757980;margin:0 10px;width:60px;height:60px;border:1px solid #c8ced7;text-align:center;border-radius:50%;line-height:60px;display:inline-block}.social-link a{text-decoration:none;width:4.8rem;height:4.8rem;background-color:#f0f9fe;border-radius:50%;display:flex;justify-content:center;align-items:center;position:relative;z-index:1;border:3px solid #f0f9fe;overflow:hidden}.social-link a::before{content:"";position:absolute;width:100%;height:100%;background:#000;z-index:0;scale:1 0;transform-origin:bottom;transition:scale .5s}.social-link:hover a::before{scale:1 1}.icon{font-size:2rem;color:#011827;transition:.5s;z-index:2}.social-link a:hover .icon{color:#fff;transform:rotateY(360deg)} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 510e8d6..9fb45e8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,23 @@ HNS Links + + + + + + + + + diff --git a/templates/nostr.html b/templates/nostr.html new file mode 100644 index 0000000..a0474ee --- /dev/null +++ b/templates/nostr.html @@ -0,0 +1,111 @@ + + + + + + + Nostr - HNS Links + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

{{domain}}/

+

Manage your site

+
+
+
+
+
+
+
+
+

Nostr

+
+

Nostr Links

{% for link in nostr %} +
+ + + + + + +
+{% endfor %} + +
+ Link a new nostr profile
+ + +

+ +
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/templates/page.html b/templates/page.html index 817ecb9..dc8755a 100644 --- a/templates/page.html +++ b/templates/page.html @@ -5,6 +5,10 @@ {{title}} + + + + diff --git a/templates/page_no_image.html b/templates/page_no_image.html index db81ab5..5ffbc6a 100644 --- a/templates/page_no_image.html +++ b/templates/page_no_image.html @@ -5,6 +5,10 @@ {{title}} + + + + diff --git a/templates/publishing.html b/templates/publishing.html index fb0fb72..12a23d4 100644 --- a/templates/publishing.html +++ b/templates/publishing.html @@ -5,7 +5,15 @@ HNS Links + + + + + + + + diff --git a/templates/site.html b/templates/site.html index 76ad950..9651b7e 100644 --- a/templates/site.html +++ b/templates/site.html @@ -5,7 +5,15 @@ Home - HNS Links + + + + + + + + @@ -88,7 +96,7 @@ Publish -
{{preview|safe}}
+
{{preview|safe}}Link Nostr