From 69bf22c9ecb6dce938ff92fe1fe2e5057b75da9e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 10 Feb 2023 14:55:30 +0100 Subject: [PATCH] init --- README.md | 3 + __init__.py | 36 ++++ __pycache__/__init__.cpython-39.pyc | Bin 0 -> 1206 bytes __pycache__/crud.cpython-39.pyc | Bin 0 -> 1137 bytes __pycache__/migrations.cpython-39.pyc | Bin 0 -> 504 bytes __pycache__/models.cpython-39.pyc | Bin 0 -> 2071 bytes __pycache__/tasks.cpython-39.pyc | Bin 0 -> 1719 bytes __pycache__/views.cpython-39.pyc | Bin 0 -> 1153 bytes __pycache__/views_api.cpython-39.pyc | Bin 0 -> 3525 bytes cbc.py | 26 +++ config.json | 6 + crud.py | 29 +++ manifest.json | 9 + migrations.py | 13 ++ models.py | 92 +++++++++ nostr | 1 + static/images/nostr-bitcoin.png | Bin 0 -> 10786 bytes tasks.py | 88 +++++++++ templates/nostr-client/index.html | 257 ++++++++++++++++++++++++++ views.py | 100 ++++++++++ views_api.py | 118 ++++++++++++ 21 files changed, 778 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-39.pyc create mode 100644 __pycache__/crud.cpython-39.pyc create mode 100644 __pycache__/migrations.cpython-39.pyc create mode 100644 __pycache__/models.cpython-39.pyc create mode 100644 __pycache__/tasks.cpython-39.pyc create mode 100644 __pycache__/views.cpython-39.pyc create mode 100644 __pycache__/views_api.cpython-39.pyc create mode 100644 cbc.py create mode 100644 config.json create mode 100644 crud.py create mode 100644 manifest.json create mode 100644 migrations.py create mode 100644 models.py create mode 160000 nostr create mode 100644 static/images/nostr-bitcoin.png create mode 100644 tasks.py create mode 100644 templates/nostr-client/index.html create mode 100644 views.py create mode 100644 views_api.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..596cce9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Nostr + +Opens a Nostr daemon diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1555eb9 --- /dev/null +++ b/__init__.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter +from starlette.staticfiles import StaticFiles + +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_nostrclient") + +nostrclient_static_files = [ + { + "path": "/nostrclient/static", + "app": StaticFiles(directory="lnbits/extensions/nostrclient/static"), + "name": "nostrclient_static", + } +] + +nostrclient_ext: APIRouter = APIRouter(prefix="/nostrclient", tags=["nostrclient"]) + + +def nostr_renderer(): + return template_renderer(["lnbits/extensions/nostrclient/templates"]) + + +from .views import * # noqa +from .views_api import * # noqa + +from .tasks import init_relays, subscribe_events + + +def nostrclient_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(init_relays)) + # loop.create_task(catch_everything_and_restart(send_data)) + # loop.create_task(catch_everything_and_restart(receive_data)) + loop.create_task(catch_everything_and_restart(subscribe_events)) diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5481de08e7e1ea430665bf61b9ae72b4e6b8ea79 GIT binary patch literal 1206 zcmYe~<>g{vU|{g{dX}ug!octt#6iX^3=9ko3=9m#OBfg!LK#vRQW$d>av7r-89{8O z9Hw06C}uF5Ifo^eHHsC?X31g8WshP9vsrUEayg?o85vSpQ`njrqqtJoQ<+mZQaF1V zQ@B#N=P;);MscSyrtqZjrZcAS^)f~Aq%x-RrtsS^qzI%4wlGBTr3j@6=kSLxfN23R zEs`UcD-Vs2^` z53)f~+(?=~Zcb`? zYF>)I9+a<-BVgj=GxIV_;^XxSDsS##<~PHHc6u5@29pxFrboAtW7vqP(a?9^^?jP#iIGF!C`MNii@m z_-TsXVoysfE=erNECL0Y>rntMtzlj9*q^MQ=!kB?8x%gYC)@cg{u_;_$M z++r=uOf4_I#R;b36ALnLu|kZ6dmrozB)@~x3OE2bY;yBcN^?@}K#@|cz`(%3$H>FP g!^p$P!^FYD#K^(O^q1v73x^mp2MY%y2a^CZ0ROF1mH+?% literal 0 HcmV?d00001 diff --git a/__pycache__/crud.cpython-39.pyc b/__pycache__/crud.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9122a6fd3cb7f11425e47438f9d9bef1fa6cfd63 GIT binary patch literal 1137 zcmYe~<>g{vU|^W)_AGfj69dCz5C<7EGcYhXFfcF_moYFfq%fo~<}l*k#F%nea#^ET!EELnwkWm~mK4?;_FRrA4n~F)mK3%e&Rni2u3YXYZbpVw?iBVp zj42!`oGmO-JgHnM%qd)Rm{Pb?cv@JZc;S5B6uuUgD83ZtU7)qMwtOlvz@&o0FQJnwO%l2j%OhR+OaX6=&w>73+ho)J@LGOwB9Ng^TMa7nP>y z6;$5hN>43`hqwh~TQLjBql^NKMFJqZnQpOy<%+l%7#Na4eg{c|Fo?~@z`y_w2PJqo z)G%Z*)i7i+*Dz$Uq%kjKf`kev=7SkDnf$6cxD=qk(=XUHC`7^2FT@`+SQIqi>Ojgf zQ*==HrA0ZY0*T2bnPsVPHJWf%n4?dqYp{Zby^aD1AtW?eA%V@FT9KMuT9SH;DKq63 zb7@h|EjEZ@D;bLf85kg;4i5Sveo*9aCZ?o7!V44~#T?+UU}0q9;QL-AjxCTt0SXQz zL}CF2QW_&>;ApwH`nZO;;z%kA;U2C*t_qnc3bqRN(6mw{1ahJg{vU|{&<{VZ9Dk%8ech=Yuo7#J8F7#J9ec^DWNQW#PgTNt7kQka4nG?|kb zkraa1%nS?+he2vZ7#J987#1+3FfL^DWJqE1U|@vmsZ!xmP*70t%*!mvOw3Wp%P%e| zO3ukl%_~tTNleN~)dNdctp-ydmCiw~jv=lJA&yQyuE@&uic)hDD~lC0;3`3?GgA~o zTq8mh{QN@{{6c+v6as=geI0`$6}(*|bx>567UiHSMU_rWF3BuQRdDk6_i=UfQ*d#0 za}4zfQ79=YO@$kyX$@xjX)@koPpwEzE-gvbWW2?clC+Yc2ow~*JoQ71Q;UlAlauw+ zGfVVy@{%%3igj~R(^K8{nUz*)V$)%{Jdg)q~Orc%}g&!ECC7Y6;$5h$u%%A zjL(Dw8z`)c*+7BM!otY(zlfEAfx%Cc5ln+^Mj}`k7#MDG*yQG?l;)(`fpmTbX#@aj CZ*|oG literal 0 HcmV?d00001 diff --git a/__pycache__/models.cpython-39.pyc b/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f27b51ea71c464aa33995aaf995fe62cfda696e2 GIT binary patch literal 2071 zcmYe~<>g{vU|?9`{w(<>I|IXG5C<8vFfcGUFfcF_Z(v|xNMT4}%wfo7jAG1Xieh2} z@tJZMa+#x;!EELnmR!~-Rxq0-hb@;qianPjiUZ7N&Ed@DisAyZ*>bpZd7^m0Z1x=9 zT)rqiMh16=6pj?m7KRkgRQ6`(D1LW_6s{ER7KRjVFkir(A%!P}w}l~v7t9xQXGq~o z;csC`;Ro}D+!;~?QUqHVQUt+#;S`}@22J6YAirob-D2^{EH1gl;*yzMqRDuR!@r;; zGe0jeN0adudr)d&X=-taCetlWr^Mn^-~5!+oLj7JnW;G`nvAzt14~njDm58zai%1e zBqrx178h$W-V!P;$|+7vON}qi$S*31&qyrJNCw%2jG18}*T%rWkjfCnn8Fan6v~jo z7{#2*lFBN{kjj?AWCP-}!}-i$K1V7`DkoT;1*{+;Bb{n9l=O&kpAErn02+ zf%S9PFr;v%aJ4W*@ux5bGiY+(Vhu{oNv!m{#gv(Hi@CHY=N4yjeqLT`a!G2+Ede+) zzPO|)GcWxXTX9KZNonydw#4L;%(B#5ECrc)>B)>p-UP9k7#J9wL1{#Yfq|ihA&a4y zuY|Fgp@t!zA%!8BL6gx>lc|V@fq|h2l-4vEZ?P1o=A^A;C=y{{VE7fFA6lGRRIHzz zte>7)qMwtOlvz@&o0FQJnwO%l2j%OhR+OaX6=&w>73=5a7nc<2Cg)_P=9TEe#r1PR z5m&5NP z4v5VYA75CSm;;f4xEf@{E#~5qqFem&@rikP`6Y><KHg80twlEQ4mkjj?I zlEPxckjk3Mp30ueEXe?ZsVph1HVk0(pd`*7%%I6}i`BI(HLv6rdvbnW35dZ~P@0sT zT6v2rxhOTUBsC>IvE&v@c4l76EtZnR^x|90#hK}TnjGk!1^EKvPmptqWI;X#c?uFI z2)~14M}dKX0p!(UP!w@7axii*@-T8RiZB*|WH15%p691w1OO<1Qm0uekZp)E78C|(Ie{JG=i(xH1_lOPSpZ}QBx!(M zg&@H0Gh|?3Fai}RApdbO7J>MFnj*K@N-9CcVi8EV2$TYfK%sDpJuR`gB(Wf~h!s>k za1>OgB<7W5CKs`R{0|C_TjEd^dIgC^iMjDg{vU|^W)_AI%OlY!weh=Yuo85kHG7#J9edl(oPLK#vRQW$d>q98P56eF0= z6vdRnl){|DoXZl$!pM-ql){q3n#&f&24=J7u;+3_ae&!uIh?s%QCy4+sVph%DIC3w zQQWBv3wTl(7BWWhrZQylq_Rmer1GV3+JH>q%Hhuyh!S9ANa0T5X<>*GOyN!8Yhj2I zN?{CU(Byv!a;M)*Mg|53O~zZ?e)+{EMb0^ysd*)ujJH@_%ODKSfYPL#%w+G>N=?RF zJVB{BiIu*Id5P(%MQ)mmx7d^O^YT)YOKyo2r6#9lmZhe|r-C%b7nY`$rfM?Y;z~~~ zi7x`_DozI32g4v+nHU%t4uir*i-CcmgrS+ilOct{gQ1oog)xPxg`tKai_wLlnX!Z^ zp1Fi+0n0)LMurrIU zL>L$tzz&BH{2)!-NQNtef`J2MCnFD|5F-a;kthQL14`h7LW~s__(|ZvhXi>EV-`~u zGbpG*0pHA6Y*NCS!raWr$WX$zfIWp}AtNM;7#Ru~N;pzj=P;(QrLecK)H0NCW^t7; zW^varq%o#&q;U2!gTj9S4?NPiNRBkH|1=qIaYEBrYDy6grr(PM7#JA5LE?f83=CB& z#idEb$wiq-sgQ(Nj4iCI#PjkMAfc)NH(a4IwWJ6XGAkKxv4RsZN-6`TI8Y3LQ<)1i z%s@#rg&~Ehl}VCeHbV+Cn9q{J0OhmRFk~^!W|+$c4ngJxEG4W9*lHLSGA?9aYp@6J4%CBCMW86UB?ys$kl=ue?sKLPo3IT99u?B()OGq5@$Hyn;<>i+og2K2sKK>Rg{vU|^W${VbV_kAdMah=Yt-85kHG7#J9eA22X5q%fo~<}lkvad7^m0Y>ph> zT)rqiMurs56s{cpT!AQoT)`+oupF|v+&MzI!coFtah@EJT+t{|Fq=0=ELS{A9L(m+ zk;s*dlFXHgl44{?%Kl1ULv%$mZla} zg2Y@>3sUn^iZvN;@q1m7L;V>=V>zD;_(Ra^$AKX zF38U-PJPM9z`&r%bc;VPzqq6*IVUqUuOvRTqU07ISRlSAH7_N#D78qF@s?<5K}upt zYJ5RrWiCinG1yU>jJH@qi@^%G6ALn7vYJe{1d}sTle6PXi&Kl@Q!6ryONwuCLxd7j zax?R8ah4#tk~1Z2&+C|H^Os>Hw^gZNZGGcP5z zLNB8vH>XGx6cEfs+zbp1w*+7T2n(iLEGe1EC15ox8H%JB7#M!J=!3jnte>2$pPpHw zpOcrASyHT>m%D+mYG^!tXEKZixunwF_6PSK44&C zW90i^Bm;7b+%1-jl9GZVkne7>Cl*)cC1>W}VoysfE=erNEMf*ZR2<6FD@ZI#%#BYg z%}WNwa&Zw0NR|&pwzvqCg>DJK1fjv7nU`J!vig=taYs$MEAxfB<%gA5Wz zNWtQzxQGKJ!%)PLnMIrWEmjln5aU=jNxR<`ft4f+R$dB_JUJ z@)#dTP7o@WlA4@flvt8qR9wUlvPb|#2!d2Fr>B;H0}31+NQ5*t7g{vU|`7idX`+x%fRp$#6iX^3=9ko3=9m#vltl|QW#Pga~N_NqZk=MY^EHh zD5eyK6s8>JC}uFt62+3jkiwk9n#&f&23Et8!=B3##Q|os=5Xe6MR9@IY&qPyJW)Jg zHhT_lE?*QMn9Y&HpDPe0z{rronZlJLm@5<|1ZH#R21ZE55 zDCf#WsesvnIm)^6QL13JP>x!zdXzfYO~N@ExtdX$xmrSE@VuVNs*evm?E7b)56ls z7-gI)w7>*OTsB1xEN%)`FTap6RW?Oo4r7X9iV|4d3@om^kTF#*MP&|SifW1)Slm2? zDVRZ1{Us>YG#PJkd4z-n1eYY1lotDGGTvejN-Zo+EiTbyyu}(=np#u|5_3r{NX<(r z)?~cJ;}z`h7nE9Dke^qas>yhZ7o^*@B0067Br`uxlkt|YYguYuNpOB?QF1Cw-AhIW z1_n*WTl{(X#U(|_Ihm<>CGn{hC7MjP*dV-HB1NgmshMS|Deto3s20+Ni6~KJoD1>Z}EVfnVni0Us73+dP}ekVX0xtVxP@0m#r2g!cpXs!kWjF!dA;t!r9DF%UZ*b#kGLD zgr|nJhOvgF2Bd~Jg*}Bmg`<~!AtNJ0p;!$=FoPy1D7|pR#~0=2m&C`HvNJF+JX+KC zen-DTDF*`sgTkXV?MR$i3&0#u`S4->{6(b@#*{4~n(VijGE;6bmloyR5=hR^%S%lz zNll3_E-A{)OTWcd3{IN2SPDQqPPqInw#4L;%(7HXz9Inz28JST1_p*(yx_2k&rQrr zOiwMk#RldU-(t-yNzE<3#h+K28($1gd!RH{d`lQ4fST}%Z}FxU73CMjC+C;um82FG zDT54`1`)C#8`u&HKv|(k5G0}mN+E1TY9O{eNS3ka7B|Gb@g=DhCAawC&V}$fzz&Mf zOj*fLWWd0{@XJF#v^ce>SU)*gKRvTVKPN9Kv!qxzCpA4aFGXJu%GXb=C`rvL&dkp% z)Mu_`EKsDRQ76BnZhlhl7EW$<1R8{U|^`?MM-cg8E>(IjYk8#1FC!o0S3}E0K#E7m%%>Kww}hG6w}5KP=!1N|SOji!(q$2QI}> zTmsSoaw|B`f`Sg@k}Sp=hAgHUhAie9hAfsEhAh?ssT5G4vZXNgvehz{uxD{B;H+T; z15X=jWBA=9S#y0^67jE@8Nmi&7Io87Hyi7E5+!Udk<&lEn1lTb#w2 z>3NAIrA4WlT(`LVKqWA^Xe_b<1t?oVX;OA-Wf3SSi)=w*U7)QaEce6P3RUc->Zk;2%^ zQOj7uS;Lsl*v#0;P{P&BP|H-poyD_&w}z>MVIdPELkV9Ne+^SQQ!R51^8$emh6REP z89*UkBD6rbhIt`lEelw^NDT`}w4EW1F@-6GxrL*awM4XrwV5G>C51JGt(Q5NL6hB2 zlj9aAIQHW+Q;Kh~f?}fh7JFi8Nk)EA@h#Tk%)I2(TdbvdC7C(5xF8CPQqziWaYJGv z9?W6Q$;{0xK`HY=S&x~4fkBvwfuUFf?DqwX3!xdSmZ5`T0aFdbLdGJ78iobT3mL)~ z7#Ru~KxwAP3{*_96oD#?Tg>UHCAU~}6HAgaZgG|5$LB(7y&?++28NZ4x7Zt};$xH;-oSI^{xWOqDQd{3*&P*vT@&KhuPy#CQ z0CRq@IaYJ-5_99zO7oII&A8$sQ007!4@I`Ph!tdjAWX0b+6XKz z0;PvrBE=<%MLD2EqL&J54i*=I+61=*i;Gj^5z@uQsYRe5yv0xi%FDM{OA?E-iy@^A zIE}I-7nPFBG6L4D%+)}$G2#yIo2nkMSMWB92 zktoR3LJ(mH35l$taF7MjAXy2BID~{o6gWLXQi~v@TGmTRP0lY$EXgk_E)oanl>iZv zAVLac3LjKiMruw$Y7sOkf@?B=kQz`Ody55BB!X+HTWpXz1zd%I%Rq1hLNXpWeSisY z`rxq1%}*)KNwou&0mYyKnun1C40#yAkckJB6qp2Og2Y^bONI}aldGY2D=7zd*O PI|rix3kRbB4+kRvk;u8p literal 0 HcmV?d00001 diff --git a/cbc.py b/cbc.py new file mode 100644 index 0000000..0d9e04f --- /dev/null +++ b/cbc.py @@ -0,0 +1,26 @@ +from Cryptodome.Cipher import AES + +BLOCK_SIZE = 16 + + +class AESCipher(object): + """This class is compatible with crypto.createCipheriv('aes-256-cbc')""" + + def __init__(self, key=None): + self.key = key + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] + + def encrypt(self, plain_text): + cipher = AES.new(self.key, AES.MODE_CBC) + b = plain_text.encode("UTF-8") + return cipher.iv, cipher.encrypt(self.pad(b)) + + def decrypt(self, iv, enc_text): + cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) + return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) diff --git a/config.json b/config.json new file mode 100644 index 0000000..0bfa1bd --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr Client", + "short_description": "Nostr client for extensions", + "tile": "/nostr-client/static/images/nostr-bitcoin.png", + "contributors": ["calle"] +} diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..61cd865 --- /dev/null +++ b/crud.py @@ -0,0 +1,29 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash +import shortuuid +from . import db +from .models import Relay, RelayList + + +async def get_relays() -> RelayList: + row = await db.fetchall("SELECT * FROM nostradmin.relays") + return RelayList(__root__=row) + + +async def add_relay(relay: Relay) -> None: + await db.execute( + f""" + INSERT INTO nostradmin.relays ( + id, + url, + active + ) + VALUES (?, ?, ?) + """, + (relay.id, relay.url, relay.active), + ) + + +async def delete_relay(relay: Relay) -> None: + await db.execute("DELETE FROM nostradmin.relays WHERE id = ?", (relay.id,)) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ed54c1a --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "repos": [ + { + "id": "nostr-client", + "organisation": "lnbits", + "repository": "nostr-client-extension" + } + ] +} diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..5a30e45 --- /dev/null +++ b/migrations.py @@ -0,0 +1,13 @@ +async def m001_initial(db): + """ + Initial nostrclient table. + """ + await db.execute( + f""" + CREATE TABLE nostrclient.relays ( + id TEXT NOT NULL PRIMARY KEY, + url TEXT NOT NULL, + active BOOLEAN DEFAULT true + ); + """ + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..bfbc424 --- /dev/null +++ b/models.py @@ -0,0 +1,92 @@ +from typing import List, Dict +from typing import Optional + +from fastapi import Request +from pydantic import BaseModel, Field + +from fastapi.param_functions import Query +from dataclasses import dataclass +from lnbits.helpers import urlsafe_short_hash + + +class Relay(BaseModel): + id: Optional[str] = None + url: Optional[str] = None + connected: Optional[bool] = None + connected_string: Optional[str] = None + status: Optional[str] = None + active: Optional[bool] = None + ping: Optional[int] = None + + def _init__(self): + if not self.id: + self.id = urlsafe_short_hash() + + +class RelayList(BaseModel): + __root__: List[Relay] + + +class Event(BaseModel): + content: str + pubkey: str + created_at: Optional[int] + kind: int + tags: Optional[List[List[str]]] + sig: str + + +class Filter(BaseModel): + ids: Optional[List[str]] + kinds: Optional[List[int]] + authors: Optional[List[str]] + since: Optional[int] + until: Optional[int] + e: Optional[List[str]] = Field(alias="#e") + p: Optional[List[str]] = Field(alias="#p") + limit: Optional[int] + + +class Filters(BaseModel): + __root__: List[Filter] + + +# class nostrKeys(BaseModel): +# pubkey: str +# privkey: str + +# class nostrNotes(BaseModel): +# id: str +# pubkey: str +# created_at: str +# kind: int +# tags: str +# content: str +# sig: str + +# class nostrCreateRelays(BaseModel): +# relay: str = Query(None) + +# class nostrCreateConnections(BaseModel): +# pubkey: str = Query(None) +# relayid: str = Query(None) + +# class nostrRelays(BaseModel): +# id: Optional[str] +# relay: Optional[str] +# status: Optional[bool] = False + + +# class nostrRelaySetList(BaseModel): +# allowlist: Optional[str] +# denylist: Optional[str] + +# class nostrConnections(BaseModel): +# id: str +# pubkey: Optional[str] +# relayid: Optional[str] + +# class nostrSubscriptions(BaseModel): +# id: str +# userPubkey: Optional[str] +# subscribedPubkey: Optional[str] diff --git a/nostr b/nostr new file mode 160000 index 0000000..f598039 --- /dev/null +++ b/nostr @@ -0,0 +1 @@ +Subproject commit f598039e440f1d57c3b5d993ff44473649ffac3d diff --git a/static/images/nostr-bitcoin.png b/static/images/nostr-bitcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..719feaad2e7eae522b2ede24e9c9c8282f895b0f GIT binary patch literal 10786 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

ljVR`1L<=Z;9Sqso8b~x*s^Tw>_Nk zckPY_r8>r#ZI5^tuo{FtxV!Nsv*H6c?Oki_f8XB7z+lfhFZa`}-)pU`nttzESd&q_ zyF9Jz9owI?i)_!@+z68>tus4+=G@VSr?c$uPw`XjFQ3bhJL%Y@1>MW`#D3ZG;P9k` z4O8wHGerpTO#AfPiNU++U|`*Sjs#BwiBtLJyjvuMo<=kA2>ePF)MaGi^Jeg7;Pz&i z%^1NH!Q-OvL*2U};j#BL2C+tk1DnIX98^27_CWuJr>R?igshjpasBguG|{ zZQPZ9WX=&TK1QvM54sQh9^BHie>MAM$ft>n-of`hmQSj+ZnQPXXP7y2t~BdD?mLWc z-XAlLx>@j&k@MV=;FGI=>3(mlW<7J}%^i^svJbxb2`!v^cbneC1O*ZM4Vej6Q*EaE zsn+M*IcR&}wd{_!waf-vu3uicglEzx_Df7_jD_4MWGr}wetrIo#7C@?M%{$TlP z|AVNd!Jc=SnPQIAAOE!YWyq(QYgLXftmJ=i=FOeN2L=TkJNSN7sGnYwv#=O%F7BJ!)Y^Kq;K6^XIY#2m;l75TX-rR7KbLh*2~7Y;axIzw literal 0 HcmV?d00001 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..350418a --- /dev/null +++ b/tasks.py @@ -0,0 +1,88 @@ +import asyncio +import ssl +import threading + +from .nostr.nostr.client.client import NostrClient +from .nostr.nostr.event import Event +from .nostr.nostr.key import PublicKey +from .nostr.nostr.relay_manager import RelayManager + +# relays = [ +# "wss://nostr.mom", +# "wss://nostr-pub.wellorder.net", +# "wss://nostr.zebedee.cloud", +# "wss://relay.damus.io", +# "wss://relay.nostr.info", +# "wss://nostr.onsats.org", +# "wss://nostr-relay.untethr.me", +# "wss://relay.snort.social", +# "wss://lnbits.link/nostrrelay/client", +# ] +client = NostrClient( + connect=False, +) + +# client = NostrClient( +# connect=False, +# privatekey_hex="211aac75a687ad96cca402406f8147a2726e31c5fc838e22ce30640ca1f3a6fe", +# ) + +received_event_queue: asyncio.Queue[Event] = asyncio.Queue(0) + +from .crud import get_relays + + +async def init_relays(): + relays = await get_relays() + client.relays = [r.url for r in relays.__root__] + client.connect() + return + + +# async def send_data(): +# while not any([r.connected for r in client.relay_manager.relays.values()]): +# print("no relays connected yet") +# await asyncio.sleep(0.5) +# while True: +# client.dm("test", PublicKey(bytes.fromhex(client.public_key.hex()))) +# print("sent DM") +# await asyncio.sleep(5) +# return + + +# async def receive_data(): +# while not any([r.connected for r in client.relay_manager.relays.values()]): +# print("no relays connected yet") +# await asyncio.sleep(0.5) + +# def callback(event: Event, decrypted_content=None): +# print( +# f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content or event.content}" +# ) + +# t = threading.Thread( +# target=client.get_dm, +# args=( +# client.public_key, +# callback, +# ), +# name="Nostr DM", +# ) +# t.start() + + +async def subscribe_events(): + while not any([r.connected for r in client.relay_manager.relays.values()]): + print("no relays connected yet") + await asyncio.sleep(1) + + def callback(event: Event): + print(f"From {event.public_key[:3]}..{event.public_key[-3:]}: {event.content}") + asyncio.run(received_event_queue.put(event)) + + t = threading.Thread( + target=client.subscribe, + args=(callback,), + name="Nostr-event-subscription", + ) + t.start() diff --git a/templates/nostr-client/index.html b/templates/nostr-client/index.html new file mode 100644 index 0000000..7b63c07 --- /dev/null +++ b/templates/nostr-client/index.html @@ -0,0 +1,257 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +

+
+ + +
+
+
nostr relays
+
+ +
+ + + +
+
+ + {% raw %} + + + + {% endraw %} + +
+
+ + + + +
+
+ +
+
+ Add relay +
+
+
+
+
+ +
+ + +
{{SITE_TITLE}} Nostr Extension
+

Only Admin users can manage this extension

+ + +
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/views.py b/views.py new file mode 100644 index 0000000..90dd31c --- /dev/null +++ b/views.py @@ -0,0 +1,100 @@ +from http import HTTPStatus +import asyncio +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from . import nostrclient_ext, nostr_renderer + +# FastAPI good for incoming +from fastapi import Request +from lnbits.core.crud import update_payment_status +from lnbits.core.models import User +from lnbits.core.views.api import api_payment +from lnbits.decorators import check_user_exists, check_admin + + +templates = Jinja2Templates(directory="templates") + + +@nostrclient_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_admin)): + return nostr_renderer().TemplateResponse( + "nostrclient/index.html", {"request": request, "user": user.dict()} + ) + + +# ##################################################################### +# #################### NOSTR WEBSOCKET THREAD ######################### +# ##### THE QUEUE LOOP THREAD THING THAT LISTENS TO BUNCH OF ########## +# ### WEBSOCKET CONNECTIONS, STORING DATA IN DB/PUSHING TO FRONTEND ### +# ################### VIA updater() FUNCTION ########################## +# ##################################################################### + +# websocket_queue = asyncio.Queue(1000) + +# # while True: +# async def nostr_subscribe(): +# return +# # for the relays: +# # async with websockets.connect("ws://localhost:8765") as websocket: +# # for the public keys: +# # await websocket.send("subscribe to events") +# # await websocket.recv() + +# ##################################################################### +# ################### LNBITS WEBSOCKET ROUTES ######################### +# #### HERE IS WHERE LNBITS FRONTEND CAN RECEIVE AND SEND MESSAGES #### +# ##################################################################### + +# class ConnectionManager: +# def __init__(self): +# self.active_connections: List[WebSocket] = [] + +# async def connect(self, websocket: WebSocket, nostr_id: str): +# await websocket.accept() +# websocket.id = nostr_id +# self.active_connections.append(websocket) + +# def disconnect(self, websocket: WebSocket): +# self.active_connections.remove(websocket) + +# async def send_personal_message(self, message: str, nostr_id: str): +# for connection in self.active_connections: +# if connection.id == nostr_id: +# await connection.send_text(message) + +# async def broadcast(self, message: str): +# for connection in self.active_connections: +# await connection.send_text(message) + + +# manager = ConnectionManager() + + +# @nostrclient_ext.websocket("/nostrclient/ws/relayevents/{nostr_id}", name="nostr_id.websocket_by_id") +# async def websocket_endpoint(websocket: WebSocket, nostr_id: str): +# await manager.connect(websocket, nostr_id) +# try: +# while True: +# data = await websocket.receive_text() +# except WebSocketDisconnect: +# manager.disconnect(websocket) + + +# async def updater(nostr_id, message): +# copilot = await get_copilot(nostr_id) +# if not copilot: +# return +# await manager.send_personal_message(f"{message}", nostr_id) + + +# async def relay_check(relay: str): +# async with websockets.connect(relay) as websocket: +# if str(websocket.state) == "State.OPEN": +# print(str(websocket.state)) +# return True +# else: +# return False diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..8ee004f --- /dev/null +++ b/views_api.py @@ -0,0 +1,118 @@ +from http import HTTPStatus +import asyncio +import ssl +import json +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends +from fastapi.responses import JSONResponse + +from starlette.exceptions import HTTPException +from sse_starlette.sse import EventSourceResponse + +from . import nostrclient_ext + +from .tasks import client, received_event_queue + +from .crud import get_relays, add_relay, delete_relay +from .models import RelayList, Relay, Event, Filter, Filters + +from .nostr.nostr.event import Event as NostrEvent +from .nostr.nostr.event import EncryptedDirectMessage +from .nostr.nostr.filter import Filter as NostrFilter +from .nostr.nostr.filter import Filters as NostrFilters +from .nostr.nostr.message_type import ClientMessageType + +from lnbits.decorators import ( + WalletTypeInfo, + get_key_type, + require_admin_key, + check_admin, +) + +from lnbits.helpers import urlsafe_short_hash +from .tasks import init_relays + + +@nostrclient_ext.get("/api/v1/relays") +async def api_get_relays(): # type: ignore + relays = RelayList(__root__=[]) + for url, r in client.relay_manager.relays.items(): + status_text = ( + f"⬆️ {r.num_sent_events} ⬇️ {r.num_received_events} ⚠️ {r.error_counter}" + ) + connected_text = "🟢" if r.connected else "🔴" + relay_id = urlsafe_short_hash() + relays.__root__.append( + Relay( + id=relay_id, + url=url, + connected_string=connected_text, + status=status_text, + ping=r.ping, + connected=True, + active=True, + ) + ) + return relays + + +@nostrclient_ext.post("/api/v1/relay") +async def api_add_relay(relay: Relay): # type: ignore + assert relay.url, "no URL" + relay.id = urlsafe_short_hash() + await add_relay(relay) + await init_relays() + + +@nostrclient_ext.delete("/api/v1/relay") +async def api_delete_relay(relay: Relay): # type: ignore + await delete_relay(relay) + + +@nostrclient_ext.post("/api/v1/publish") +async def api_post_event(event: Event): + nostr_event = NostrEvent( + content=event.content, + public_key=event.pubkey, + created_at=event.created_at, # type: ignore + kind=event.kind, + tags=event.tags or None, # type: ignore + signature=event.sig, + ) + client.relay_manager.publish_event(nostr_event) + + +@nostrclient_ext.post("/api/v1/filter") +async def api_subscribe(filter: Filter): + nostr_filter = NostrFilter( + event_ids=filter.ids, + kinds=filter.kinds, # type: ignore + authors=filter.authors, + since=filter.since, + until=filter.until, + event_refs=filter.e, + pubkey_refs=filter.p, + limit=filter.limit, + ) + + filters = NostrFilters([nostr_filter]) + subscription_id = urlsafe_short_hash() + client.relay_manager.add_subscription(subscription_id, filters) + + request = [ClientMessageType.REQUEST, subscription_id] + request.extend(filters.to_json_array()) + message = json.dumps(request) + client.relay_manager.publish_message(message) + + async def event_getter(): + while True: + event = await received_event_queue.get() + if filters.match(event): + yield event.to_message() + + return EventSourceResponse( + event_getter(), + ping=20, + media_type="text/event-stream", + )