From 823956395b8e3d9c912f3307eb2ce3bfb1606b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Fri, 17 Feb 2023 18:15:02 +0100 Subject: [PATCH] remove scrub (#1524) --- lnbits/extensions/scrub/README.md | 30 ---- lnbits/extensions/scrub/__init__.py | 34 ---- lnbits/extensions/scrub/config.json | 6 - lnbits/extensions/scrub/crud.py | 80 --------- lnbits/extensions/scrub/migrations.py | 14 -- lnbits/extensions/scrub/models.py | 28 ---- .../extensions/scrub/static/image/scrub.png | Bin 24041 -> 0 bytes lnbits/extensions/scrub/static/js/index.js | 143 ---------------- lnbits/extensions/scrub/tasks.py | 89 ---------- .../scrub/templates/scrub/_api_docs.html | 136 --------------- .../scrub/templates/scrub/_lnurl.html | 31 ---- .../scrub/templates/scrub/index.html | 156 ------------------ lnbits/extensions/scrub/views.py | 17 -- lnbits/extensions/scrub/views_api.py | 107 ------------ 14 files changed, 871 deletions(-) delete mode 100644 lnbits/extensions/scrub/README.md delete mode 100644 lnbits/extensions/scrub/__init__.py delete mode 100644 lnbits/extensions/scrub/config.json delete mode 100644 lnbits/extensions/scrub/crud.py delete mode 100644 lnbits/extensions/scrub/migrations.py delete mode 100644 lnbits/extensions/scrub/models.py delete mode 100644 lnbits/extensions/scrub/static/image/scrub.png delete mode 100644 lnbits/extensions/scrub/static/js/index.js delete mode 100644 lnbits/extensions/scrub/tasks.py delete mode 100644 lnbits/extensions/scrub/templates/scrub/_api_docs.html delete mode 100644 lnbits/extensions/scrub/templates/scrub/_lnurl.html delete mode 100644 lnbits/extensions/scrub/templates/scrub/index.html delete mode 100644 lnbits/extensions/scrub/views.py delete mode 100644 lnbits/extensions/scrub/views_api.py diff --git a/lnbits/extensions/scrub/README.md b/lnbits/extensions/scrub/README.md deleted file mode 100644 index 3b8d0b2d..00000000 --- a/lnbits/extensions/scrub/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Scrub - -## Automatically forward funds (Scrub) that get paid to the wallet to an LNURLpay or Lightning Address - -SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress! - -Only whole values, integers, are Scrubbed, amounts will be rounded down (example: 6.3 will be 6)! The decimals, if existing, will be kept in your wallet! - -[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) - -## Usage - -1. Create an scrub (New Scrub link)\ - ![create scrub](https://i.imgur.com/LUeNkzM.jpg) - - - select the wallet to be _scrubbed_ - - make a small description - - enter either an LNURL pay or a lightning address - - Make sure your LNURL or LNaddress is correct! - -2. A new scrub will show on the _Scrub links_ section\ - ![scrub](https://i.imgur.com/LNoFkeu.jpg) - - - only one scrub can be created for each wallet! - - You can _edit_ or _delete_ the Scrub at any time\ - ![edit scrub](https://i.imgur.com/Qu65lGG.jpg) - -3. On your wallet, you'll see a transaction of a payment received and another right after it as apayment sent, marked with **#scrubed**\ - ![wallet view](https://i.imgur.com/S6EWWCP.jpg) diff --git a/lnbits/extensions/scrub/__init__.py b/lnbits/extensions/scrub/__init__.py deleted file mode 100644 index 29428af9..00000000 --- a/lnbits/extensions/scrub/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -import asyncio - -from fastapi import APIRouter -from fastapi.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_scrub") - -scrub_static_files = [ - { - "path": "/scrub/static", - "app": StaticFiles(directory="lnbits/extensions/scrub/static"), - "name": "scrub_static", - } -] - -scrub_ext: APIRouter = APIRouter(prefix="/scrub", tags=["scrub"]) - - -def scrub_renderer(): - return template_renderer(["lnbits/extensions/scrub/templates"]) - - -from .tasks import wait_for_paid_invoices -from .views import * # noqa: F401,F403 -from .views_api import * # noqa: F401,F403 - - -def scrub_start(): - loop = asyncio.get_event_loop() - loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/scrub/config.json b/lnbits/extensions/scrub/config.json deleted file mode 100644 index 93eb871a..00000000 --- a/lnbits/extensions/scrub/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Scrub", - "short_description": "Pass payments to LNURLp/LNaddress", - "tile": "/scrub/static/image/scrub.png", - "contributors": ["arcbtc", "talvasconcelos"] -} diff --git a/lnbits/extensions/scrub/crud.py b/lnbits/extensions/scrub/crud.py deleted file mode 100644 index 1772a8c5..00000000 --- a/lnbits/extensions/scrub/crud.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import CreateScrubLink, ScrubLink - - -async def create_scrub_link(data: CreateScrubLink) -> ScrubLink: - scrub_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO scrub.scrub_links ( - id, - wallet, - description, - payoraddress - ) - VALUES (?, ?, ?, ?) - """, - ( - scrub_id, - data.wallet, - data.description, - data.payoraddress, - ), - ) - link = await get_scrub_link(scrub_id) - assert link, "Newly created link couldn't be retrieved" - return link - - -async def get_scrub_link(link_id: str) -> Optional[ScrubLink]: - row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,)) - return ScrubLink(**row) if row else None - - -async def get_scrub_links(wallet_ids: Union[str, List[str]]) -> List[ScrubLink]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f""" - SELECT * FROM scrub.scrub_links WHERE wallet IN ({q}) - ORDER BY id - """, - (*wallet_ids,), - ) - return [ScrubLink(**row) for row in rows] - - -async def update_scrub_link(link_id: int, **kwargs) -> Optional[ScrubLink]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE scrub.scrub_links SET {q} WHERE id = ?", - (*kwargs.values(), link_id), - ) - row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,)) - return ScrubLink(**row) if row else None - - -async def delete_scrub_link(link_id: int) -> None: - await db.execute("DELETE FROM scrub.scrub_links WHERE id = ?", (link_id,)) - - -async def get_scrub_by_wallet(wallet_id) -> Optional[ScrubLink]: - row = await db.fetchone( - "SELECT * from scrub.scrub_links WHERE wallet = ?", - (wallet_id,), - ) - return ScrubLink(**row) if row else None - - -async def unique_scrubed_wallet(wallet_id): - (row,) = await db.fetchone( - "SELECT COUNT(wallet) FROM scrub.scrub_links WHERE wallet = ?", - (wallet_id,), - ) - return row diff --git a/lnbits/extensions/scrub/migrations.py b/lnbits/extensions/scrub/migrations.py deleted file mode 100644 index f1f4bade..00000000 --- a/lnbits/extensions/scrub/migrations.py +++ /dev/null @@ -1,14 +0,0 @@ -async def m001_initial(db): - """ - Initial scrub table. - """ - await db.execute( - """ - CREATE TABLE scrub.scrub_links ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - description TEXT NOT NULL, - payoraddress TEXT NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/scrub/models.py b/lnbits/extensions/scrub/models.py deleted file mode 100644 index 8079f358..00000000 --- a/lnbits/extensions/scrub/models.py +++ /dev/null @@ -1,28 +0,0 @@ -from sqlite3 import Row - -from pydantic import BaseModel -from starlette.requests import Request - -from lnbits.lnurl import encode as lnurl_encode - - -class CreateScrubLink(BaseModel): - wallet: str - description: str - payoraddress: str - - -class ScrubLink(BaseModel): - id: str - wallet: str - description: str - payoraddress: str - - @classmethod - def from_row(cls, row: Row) -> "ScrubLink": - data = dict(row) - return cls(**data) - - def lnurl(self, req: Request) -> str: - url = req.url_for("scrub.api_lnurl_response", link_id=self.id) - return lnurl_encode(url) diff --git a/lnbits/extensions/scrub/static/image/scrub.png b/lnbits/extensions/scrub/static/image/scrub.png deleted file mode 100644 index b3d4d24f243b2cc046b663b3db863037fabf5d35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24041 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_Sjn5se|N`ey06$*;-(=u~X z6-p`#QWa7wGSe6sDsEk!?LAFJeRAvbHH)Sn_`Ol)z=JvW9QF6__jXyjWRlvi>MLHM zsbMZltP|3{Cq8)h=X`zq_xL~8KX>R&zxH~?)z9&AdyIZA{C&Ru^IzGTy88Y9>-HYq z|Nqmj-<8Urf*w`>`sehw?%(y-51%v5&!0U1)O`Id`}5cA-)H#g`>(&-xoh*Mm@GQM z|84JuA8{H-&r3h7Zu~iO+WN)Y?fnzyo-hAxI{Dwv$6vy;-ruqp|M~a+&W%-J6^2JE zf;{H!5B|lc@Y261^OJK$y|G4})SsAp_brdU_lTHKAGLh`e>UY^Qd$3w^Z!-VvbS%o zuetDY+;Gd;d{>%R*&+qKFKe~Tkp8B2h9d*&Qe|KL0Ra=}U zW*k0kSDpITY_0d|cH4Kf=G~QbUcK+*@`BE9``F^=uDR!O_RqKO^|EdqEw9^Tbj}nU zuDoA&6Wqh&qo=qlh}HL$9dxA`!zisHIF`I>Hm58`ul&8>n<$a zVUemv5jVc>JfiiGU&TZJLMOl2zWP(DCconvR3BYyJ=o&H(h=pMdgVcMk1mVYCgBAq zFC?ET6l@IeGGUV}_L_R*%ZljF+PZuCj|o-Y{AiWnrsB0~rJ(exo`k0{C+1IM+;8Kp zwR1|4xAxv1qwG~Xr$uG2W;%K^==8d&VbR%pd(Em}?VKK0{d#Y|ox2R@1)arxX1^qo z{oYyb{PbkIy8Mjdqf^7;j;`Hx>s9ueou2dOmftMhe)s#0%ZpT*?bk$Y&O7{!vpvM> z=agW7>%Tq5`YX#O@BTS0I)C@yKI{5L-*{i&-}le;$|3WtkFH+wIrf2%uU@N*RpRjL zN^(1pFwx=2Y$pdN_oHXNPbdy~B4TXcus=kpS*L3zt7{L(GvTD255wQY{Ba4H_}l*I z*{y%3?>PG3q{wgK@97hNSKs-knWZLmOFwD8)|ExST)y6&llG=3{r%eawOwE9YRmWU z|1iJq|Njz2`!em#?UjA{E3a?6-FTq$ut#od%7^nN%)FSlSg|LNQEvqBFV4Bzga zWLi~wT9B{7ecAfJ{nFRs)^Z%=jFe4ZayM!7+gJPEyFGiaP-D3NM311eUW(3jjg|@G z+x6bxKRWf_)4if4%(X_=p3UcvFJS*KdG_DAzF9wiMowNj$Kyn7I7hn40{!d8hpbMi zOiz$na%yhaagQly_v{II_0_HEEu*euuxae6FXt{XcrMLX5*GJxar_>3D(BsuUDc6k zt)Y9hnr409v$<*Ag8r#@f+8JVwwm5=IeYh+q~zmU547;| zoB!Aq&8-@!pDrjSW^_LEv83sZR14#`dz0pdg;}~SbDnUZc5AikFWcCER4X zF8jaAEsULhv~hFw%6;>Vl`INg&sl!-D&zknf7Fxnwtui~VA5{$QqlUJY?^;=;bO&v zF9(Hs0~Dt(zVq<$=B+$`w$1IHJ^SF(;I~Ig#3d7W4HI=N4(*iMJ||=1=9LAf%%&Z3 zDOMDGuebH+tR3r*y;&tVVRP|ju095}WAh`wZ;SBX)u8)xs-(Z^vI8I7e4p)fpS9J{ z_rjIUs~B!qT|Bv`@S;h2!Rdr8Ef{hUyyK5dhvx#6Wts%y4FU|o6ddpa(S0zjnBcb zw+DX(Y`NT7UivIo#ll&6|GPw^;`w%IF$wxN1CQmVv`ut(`T1sAj`nWlz~ra-w_QGL zb20mV=ymGiyFxSFE`=~fOp%t}CUN{n)XmU>Z9b>+z6zgQ=uz^fr_jrMT2_eD-LPuO zu4~?zs~j%OU}?zy`Y7Bx<{qP&(o7SZ{C5`b7c(VUY@c8G)@`H6BkSDA-Iflbm91VG z(qA_SaTu!b#eV5cy4CD;OVp=ni(k|7w8p+2;$KgEs4$gZXPqecQmq zru3@`FM2nd{XA*?IA!(oZ$7QZjFZpy-E^Oj^zD?(v!uWpWwWdNYj3E1YIIuZI8}B6 zBkL083;{voCo@C?`nQ>vM@;d4wQTPX*J(Q(H1)Uhc?WneYH?uuz_aq;Bx8$b0a_n( zChOc@F{yi9QDW@0sk73pW?uTD&K>G`c1pXd!<%QmJU)@fSA3eQ)fIM7dBZHpGN#$v zlx@ouI{rS{=3#q!`N5dw%UXJFX2v|>*{q~|O698g`yN#euKcLFd3z14)~(we;jbaU zwq#58=Gk_ZWi>IAyBwJ(Mm^w6Os;Y$y?>F5k!AO6*0pJ+dle=vZSHvOmE7KSrc3AI z(tV62EJ~aEvUyfdV0>W0X3%g#$lBR_O0VOQcL$VWwxq1P_GJIA`;#VcaxfoTSa6A@ z=VbMlR;Jfq*YU32TDdYVSwJQJ(2uFCJ6NRKzq8-m&!y2FkdfQ4*L-5a>$R+|oaY<@ zdDaWx(KKZ5nIC!h^&{Or-myC-J5UM=q}p8Ib>)(qzE zW1fXJZcN$2&KKSJ3U;uv?zw5V^19ab6W_Ar-y|-zjhbE~|9Qsk-U;$}0>kIhS+K5xFeEcp_w$t3ZhvXv9ebC2>|WNsDwYvS1w!q3Se#(GkbzkEVhs^g;qh3?M%CRFH+a|GBJ(it_x0e z#d|PYMK)G9X8m3udQeBCGdZDwgQcdm*CAp5q4vaMGAkJ_{Xdg;+e+okf(6Oz&e$B7 zH2az3oA+t$N7HV48iq}Hbq!f;<~2F#xHHrS=wC6K*7sqv&XODK z_2yO5yU(7QP`Th~+}+PYoQfg~0^$PvI1W!(zr1#XmVmx{7UQ1tY04LvcJ`(vvR-(X zamb13rsG*Z4yn1jPqsAAe>+8^@nIYLt{0-SP5pA~_UNRV&F|T!(HzjpHDl)%z9rl| zjQ$#vgb(?zbP5wO+m;fUcQAK`Xpr@)yaR?VA4DphzHmqvtXI^-!$pQ&S~!K$=9x1MM$ryU;X;XPM=P9t%O#woV{?{^&3d7HxI2Ag@l6eu7PI-c zHmd{$XbIh!d8EuJ>5tRvLg%1Mo31RC-&>`U9v#vu-@DtIqfW)MaK*CK{P!MQZ(UY1e^Y-c0@?YNX(Ks!zh%?3G=jkRU{XOj* zcV~%Zc0N)1n6+n(UDQrN?}D3>K}@?em$2RFIDh6w-}N^^(oY|SJwFh<;Z=x;R{o(C z%i^TG_(jhi{dGvJVS@QjRzU-|886gquCZ;N!53o^XZ*-4bS# zk7kRR3U~6%njLiH<#tyK1t$)kvlV%93sktS@E$R=XY740X#QzOQ*8GcZNW;94M)GO zQc#h)WN~12N?EIYgyw;ROI-W8ZyaDynJl`2`A<{7@KKq=oHt@B{qBGJ6DYqkVabXr z9Zo66vIc&>`?6mh^F%xydsttTU%Yst_+%^FN*%X`#3ei42u^kUFCggCEaDmW?p{h_ ztcvM_Kt0n17BNZ!ZyxTrywdH`B?aBBuS~YE+8;7~uvh(X!1VMzPbcUk^e%ZRIO)tq z^^<*?^Vu&gH0j{{#;bmWYwPEPFjMD>2|qcc4@`{DP5D#8IytH!bc2cG!|P8sYgb+6 z5p9ywe86pbWnb*HuIn1=K^g_2_m^DIoH(`M3d=boMvDt_i|_cpl+oPx?bi*1tV1CN zTVnEMBHs1gan5Tv7^AwkPk;6EI2DN>uD4>Q2+q$t&rmkuU!y0BkB_bBJNw3}d;gj^ zKMB5Yov^vbPv-oAjc~ zb%PF06W?UOIqgAT%?E+d4FOAC`ge&(q;uU^yt3PhEknrD?gPv7)+ulNmK_r4R?<}d zJyBkDrLN+QL)p$>914!GKiU;%&AM}~tzsSPboS?}e>3wo#JR`io0l$}H*@33Cy{>j zaW@w)s=mpnQ=vR_isTfP)eV(XB8~~na;|FWH%n?`bPn$nN28xg{0eE*gui_fO8?A79|&zV!ZZXVru_MQIIi&yr$Xsc~~lWsb=k@0Cr$`KdFnO0GFwXc79_^Tvy%c8uyWhI#ph5)F>N z3vT}SGp&!iQF8J8<&%1*)`}!#U30!U<;0qq=BwHrC;DipwT5ku;Sea8)M+w-*|)oF z(Upn*%$J>G6Q46_b2AkzywbY!fx%3Nql#5qG8zRY-1~fD9rJOA4{kyU76-mK6>U_B z63O6Qa{A~}NrT6S7Ta=d67Sahe!S|zBST@A2Xg^NMH|<-zq~ghKh1{=?H*|R!Ml(lqh^1|{(2zJWFVbPz z6D!uWbObHP(J}kvm{`R6qKc>Eet8_B-V&g2!*@6-Pgn zG!EOir{=5uLiOOQ>a0Tiof#`w?3UJ9{NcS;lv%K?x$LlDO1I~y1m%BDlCNC#JjEyf zagSZRdczrGkCq<|vVzaGLnA^sdmVRO;YvGPy4h7|MuDP%CJTQ;c1pqfy%UaIcCy%J zzvsjuP{$0{j^W)fzkj!xDO*gpKSlRusn#7)W z>r>tnxq|j^CPOwwyKKXX1-`rv))RK}{t`PQXg;lF+ zsOo7xI=We5f28r9>M30oJsS_(^S-?(o@KO3d$n1w@yyE{=ML+xQEV+cB^fH7(3WuC zsDW{PySaz_klQiG+`{FpA zHWlR}=aQ*^0{y$py=7-xKAb4r_^BZ2(7SHG&>a?REjK4cI=P&PU})-KEHOCZ{`K?E zN`^MB#brF^^Cg4Nnft7hHT4p)_R~sc+${gDwfc{5Ga)k*G%9i#q&`h!w4ST@i3*cqrMF5x`AO>AM;s~tSw zHl*C`^xwWnbml9cFwNJ|LZN{_Q)3SN%3(V8@_G5;bN_3~_wQ{8asJZUm!-pC{B^Bv z;|4c1wp(ltXBRj5#bt3E^6p_OpVkyym+WY4#;_F>i!WGj{*06^O~olw19$I{CW3vI%vzw_4L=2le z8Ll2xPG7o)x8lN+i5$)x-nuSbkFSR7H&wFyKA@An!#LOLD(hFFx4Yk4KEA)LF3>B6 zdC@)<6Q11;B?8H(CiU^PubAy2X!-86X?RC?>F&S`2ZfORo66cgf5}grAImN(>FXM^ zcus`-fy|TrD+-o(3r}pFeekP-Hq#xS*wuT@U&$(TuPhCla5eO|)U7#dPE1{slWHTm zu=C!r4C~V8g)`b>R~~raUSeygb9_0Q`ox3p6&1|p?G3&hn)R(z@qkC(!?nNPPO6&m z%J|bMUHSE!FaFSYqgu?h%%ReJ-t}!VLRqu=Jr5mix-unN)AHf|AcXyI<~rPWIi^PaazdXNqP0h zWL~c7_0z99P}lLbIb-jkVkIy0DaI))++SoGZ%R(@Xxa2HS2y|R%6PA{^A{bK65ii; z?0{tMl+e0Y5IgU=5uOWP;#p?2Wk4!I)gVCT0cwtpV-U9Cw?QIW#<;7>WK; zQB`D%WN8dFAH| z{)(E;I5-Vw^Ch4A)y0vq`J@u{&6)yY>}e@62jTDvxxb3teL0j;R;)qyM4@|`VRcI)aFuLwc&h1;h1$raWv zoTTOH`*|U=%Z_5B;I(Bc+#hUB*|>+wnU{4)>sf4}T16!7<@?A#Ym#F+GZHg4fG5b_qlF(j79IGA;|4rX)y3rt2thy-N0QT$B=cD^%8j z?ZN#-{T{1L#$VoU=l`U&V1-%L1BI_R(Wh)93t(rXoRm-z;+|wBkRI8{d zMI0}Gwzff=tF=2-AWJL1$LZxu<>=|(?(Sz2>zQ-$r7I8HCBx?GXx_toB?~7XWRYmy zd2Zg739L&E?5!(vBj=yuNItMjKF=^9Cxlsm(`MqzQ1+xn=T2`>(GmK8rO{BAClG3(mSR(8dS`){cjyzC7s>-oXl zepcF9U0mRs=M~=AjN&O1S8iCaaD_{~Zc)9#J?8sQ)V22V&E4y|SVUI8aLLEHRw2e) zJ-e!d+{0^^7^HmET<+v!e(Q|XBN5%`C+6p6Cx~vG7NsFz{ElPKLB^m(l@AThE#B}r zz3b_U$J-0+qh-x6PyBqb^j62OK;ec=hPLO+l5gn9mh;;F+o;=qn?bXrC8(Xh-LFlt zH7D-pi;i@)-SLOrbv;TL!`oP8PUe?gDZ6^m=95X|lw%j3@JQS`6Md<-UoK{C0Mqpa zYuRnqmtF`xd9l3Mo$Kn6&oNcXsXJm@o43wxZay%JVbe7M-QE-RA=i98vQ;>pRx)|E zmdN>;cFCQ-rQh_q@szle(TB_FZ~ol2sY}*AANlsTrr?V=eLvd&7awmv^V)RH>#q^p zy`PGgrc7S9I8H!f?Y!@wE|`_4&+U+0*HftQ{EEQ08ObZAw5+f?7Mf7g6Ert4!&hrn z>@YlB7-OT=|W&ww|Z zD_^?LePX#SL|4)0Rra5j?FX4=PB#DSHizfu^3Uqgzg-txZ&@O>YvYvuTeJ3l{dIa{ z@qc@h8O|+#IkvL!uqIA_1IukY;;1KzM)Zo|@BkE`CExwE}?ZfpWyAlII>Qy%T|3>o@8&Wda0w!G{P zo4sp=y`#_8%W>TMZ=6_WP_jZ+^KnVk`@_>K?lT-TF#lpTjc4!bfAL?<8JhD?pUpn( z^qlQ@=BNIw?XK4<*spO|*I(Uzy-cuU!Q(9ovrId#y}l9eQX(UJ+BqWm)x{G_CL9QP zF6FT%T0r7-w(;fiv)W6y+Xo!7)?X}q)!@RUp0`foCqw_~7&^HsKT?ovU$IQoUpxS-(l zGK4*%@x;qlNBU&WgyoC=;8khxYe?ca%Qhp+cu>pFFD;w!C{uUaCfa31T7 zxW0I@%>&NqOkee@;?wW@SvXBz^Z4jfrrgNeJ4L2Vdci#7ZeriROZv*%Un|aAv+Tbh znBmS}=UAz7*>Sh>tD}ZiM?UTne6aR@venh?vo0S|shHgNYEqol*ArPU8z*d@HgCZO zF0bY^hQr2=jT?~rKh0@#8q&bzj^PT~cbu zI&o=9^WuW<*12lO9A1ZfGT*QK;>T&tx2|QrE()iYJ$JtnSbmD*LH2|b#xK@{oOrnE z{-2ws?7C8`8WSboNGe}w7B=x*t@&_kVS?4I6Ri;m)|2!+c)uR<$Sw8$wms8xnSx>c zF8wPeQI7o?YqDZzm$~y^Dv4p=w{em&$DXbJ9tWFS`A=+Jy`AyD-qe7rvo7zGJkWdi zm)-sai&8SS@8ZpNJ2Ayy(AsowNd?FGnTk@fSDl~aUHf6CTP~az<8wmm%I9gnCSBt= zJBN>PZr|p>`%dhir}A0xw@x`ZbJdh6kKfg1?4>+6m`+M>Ta=-f+BR`r=5EoRCmq{9 zS@&y+bsA{wKiU3q-kMLltQzy&XZp{zIibouv+lo4)1&Fa6-SaDJ4*b$Q0&6wyfte< z$f=GNCbhyIj>K23S}SZP1cdGs-14;hD(@>Di^}lKgXaxH)h0H`?cXBL{nKl5cg9t} z6IOwKt--Rc>E{KOuFMnAQjA}&TvPOV{oSi8U#?r%@a?+$)sNpl%ZV^Pz3`4fm{sb2 zbBMbh$KR{ZbFxguw`)ip@_D16yvpR}HJ;_?4!vmMSH0TSSH-}xM1pI^RmL;V?pf_V ze(1E_*+&{T{w!>XI<225^u?mB?_aWBijaBs!po1X1UEW7?qZzs-+4~3Qr5KmO&;Cx6AH-gM`*XAT?F)0nt%ASzS{^jvJCTiPeo|d}7HS zd~Uf6*_^A&5+3Y}`x?RaAa{f1^h<^5vfEGOi$6NGZ~LFm0T-tKN%A_P-eq`2^tvc4^!!&m*LieEaB{*9excn?JyzE`vNoMd5|<5^U}(0_ znAm)}aq&tI+r6r(%eVEPTXkm5KMm%~bz6Dnh6JtU{&V1srL6V!XN(&C>zo&6&hGfM zbH|N;rx4PVETgZ7Zmm0o?2-g?9AIYb$vq1@v~pJ?<#+Y^wVUW$|JAr z67sVqJ@D{^iO+-sjY`&S-E$!O)k^2JK1`uqOv{B|NZ-9@H)+L-lPR)SKc_To>(dT= z)Z}|UXJwz?!J?)(Q=z&$E*CVXY`lDNA7|L#3R~f(&6kxO4S!la%39&fz+2~FGjGWs zqdnqp=3bHh{pLIOTJ|+djK5aRIx}g>k%O@f5gk{joSIz2aW7X(?+e#z#&t@@-BvR= zUxv@ry19sL>YKAHIy=JHzCSqGGp*CNpUL3?qgME~HKJE~E04>+uimHC#=3s$#jhDN zeudrczCBZW(vH^&KeHA|eC0WLd!75@5Bw4bmhYW$f87fyS-y827kLz4s#xDTQc`tU zJZYJ_^Gt@1sf|I7fh@MZeb3D7WSq+*4615DGy&?sC*>AAinQeb(Vl8 zzw?WSi~UuXf0({(`-1LISCpmCy|Z?7o9Fg+Z+5-ZwyQJS7Q5?z`EziE!F-)=xeczj zYu_xtwdm8d!~T)p&r27`erS2PUF!UknX}q?H%zxZ>dg3KLc8CNG}&Hr+lZf5|EFYC zttvRs+gG3V;M9_U)b3eMtCw%wU?2R{NKlJCpXxu0rQ#Kfq?^0lIl$A|8MYdTfuUkf?L=FT!vP|#{=u#j zyPT~%EK*kZiLL3f$P(=eP`tvm)@sI{FZ{DiR5bPanz9cboPTsxbNA+TeCwJNelS0J z^=QcpCDnV=MWT*WJp5>PZuj#K_i7t1tS~tHN{Zd&uG!_2Ei=xp?Xq3Zq0nLQsN?h* zqh~6btoMKITAsIRx5W8R?|*Opb#>3th6WFt(>#~e?kUW>w}toI?<0a?lR_UAKQf%G z=ke!6>CwmL=Uk_=&XY7Wep&g5V`Iddgr&`qC$DrY?^wKQS@6CIUn8FGIU2e8O2q4V ze_9vr*|+ITY1CnlBth>93Qmr^sUZo)Ll=gBNDZnDlQJ^s_KY|b-Iff}}TU$*VP#gJy_xqkcB7lH;G zcw=I=o2K&bxWmXcpXcV3SWORa4UvZL#=n-y2<&71b#2eit>+4VF{EUKt(1Q7KtMt_ z=-aOIyQ{6Af6MpJuCt#%v1sR$BO<3682BD!hD4M^`1)8S=jZArrsOB3>Q&?xFo1xK zeMLcHa&~HoLQ-maW}dCm``!DM6f#q6mBLMZ4SWlnQ!_F>s)|yBtNcQetFn_VQ& zbX_Yl%Z!xlxD;%PQqrt~T-=~W6s4ruDrJnJX9Ei1vVqd26pAXPsowK%`DC^^-&EH$r0 z8QF-GWVrr<(xM!&cT$q|Q*%;tQ}arS^$qn5QLJ?L^bLUP00lvMW^MskS4D0CiprAA zG(=#b_y!~c_71W`Dsl_p=Ax*E`5mkn97a|y`N^dq=Xtu=DuL{`O36>oOtAtp(@f11 z4NQ|$brUVqjC4&>jSX~@j0`PxjS~~i3{w*=O-&8ckc{%oD=taQOHKtDRgqhumzkMj zWoB++kdl~Utecu@k)msomS(1#XqaZ8Yi49=ZfscSYoS` znVXoNs$Y&Bao&iE6ASbaTEx#z&R>>zbue1Uo5t5mk8eEbH3Qa>Z zb0c$eQ%ehDOH%_wLkonWu+*aB%=|o%nT7^>#t?Hs(PHIal$n}Wk_bwtwn~Oz9Tm9+ zR?bDKi6!|(A^G_^wn`u;DH!P)8i13xf(;~6JTi+*@{20%z$qG>Z-P?`Av{PH$jJmt zDJX!GtyN+&#FFB~veXo?MG7#f26vqW7J z;}lC>3$x@TT?2D-6VntUGjoeXi1*;87w4yylqVLYI;N-QmDnn|XXX}weWRcO4oXc_ zx0Gk3g2Ktb$Vk`FNY~gf#K6+Z*wV_#K-<8;%D_NLpY~ofNlQ*OH#4=+O)^eQ)HO*o zNYb@1GELGoF-S>HHZe*yN;5J-bu@)uG&Hs{FtRc~tl(Z@*H z5CuquxgD1RL@da~&5p}PA6%n=Y8Pm-02S{v#Lyf?LmLzn6h3IUQ7 zkEX8C;36pmNK!nSx~LXhT!?OKYF>)1Qn`}7UGLPlzZn=9*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>l>Ty8lV}?|Amq4|D4$r$p9h95@vb*f4eDA_nbbSH`eoT}*3wL|G#wwYfGJ zg^47jlBb~jhk^IMB|$4<#PUlxrph>{8)rOKg?G`ufd_Hs0pY^Tvdxhr5R?975Ua!ux zxF!7SpYK`^e;<7x`lOaYF|BTXq^QGgzMbESpZLigwEb^oHJ#;Y!#VpM6aJlD!g;q^QTf08!r$k=jJv#F=fDQR ziAE`ryqi`rE?&pFJJZyyGWDR%*$-t>+xO3Y((V5s)`}@^-HR;N1D_9^UNl!F?zckR z@7GVxaC~I9@7!B;_{^g7sVC=TDObP9ymFy3{$N^#-X5J>rx#V^*h|8&c-*RP$HvwN9aF(``%&V9fjY20*iSNDtD@5xt9S*4GrFxi%H&Xu11&3f@O zv-@1UwjpgU9qol>4cOw7T5ZAcih{;FWGy30YlW} zy#-xYCjW2n`+D~FevZtfkdU()KFgO+zQW!AxOC6M=&3t@bC=}tOqJ0M@Y??S&!IVs zXCJfv&((R5@08pxmbB8lTly;wGiT^aXN2|aS{8Y?!`!H)%(!vC@vCU=eer+diVIfX z3@mEj-!1iS@tazC=Nn}P93@p&o_}9xa|_uubX-+Pe(GxI`s_%c(EWefd%h`u7Wp@Y zb9MGygWOw%0ejQMSXEwL_CXzAv-+tP2E6&8ANFW$8;fRU}b zJNluep2YV*+E4W6@@-?Vny&iwPkGbX<${M++;6$Ki*@lDAs%5H@u@P0rC%I*ZPj_M z$H!*H#Vs2T^!SR_{pt<(u>XJl!Lsfn>S{{&8g{0MyOlB@;%k1sFetfVulPr+`%dZZ zwc;r^ShcUnN;+A)uFG~6bgQ`ZA^Yk2HA>oo2W-n(YJb;q9M-8|U%%cq;^SF?G}o^> zMHhB3-<_;i^6Go9fLp5X!M%6(^(h(HIsNE!7g%PR%KE&(%J1Yejm?5^jmzRD&)*UvORs`=WA7_sHsg`eMvw#eVx5xAdg$&7E0 zW(MaPbHppWHWS?U)9#Jw>$tatXetk&FcGnJu)SF+%HaxIB3q9>|S#vcEzNY{Ms28 zZk;_?rz@X!qM+r%E8do{wTE~A`?lw%&YwNj7o30HT+hxO@3H3XccI;HYlRh5U&J{{ zNTiCotlv5FLR)UitvJ_XkA%4%ePv&-(<6{pT6@q-Y+0vlWb=-<%ssW6bwBKM*&(Cn z{8?+JgL+f!lAC)JZ+wbXW4J26hQaOgGN#6J&*KgmYz)g+tG>ZiBC90C?y`_+MGV{b zOB)#`iyh303$BXMS8SFc_)sN=IX~m9rnf5SxkyNu+8xsf zkj>9wd7Hw-u%uZ^dY=9yEaQ%Mh6<=TWhPTDLrhl_BT7IkGUX5A6!nd#Q zcMEFImSg2Oea^U=@x0A7&tEhDd*A9&I{nHp$}IWX`a>%+53E_|+;-@4m}g_cuj&t0 z_jQC0;au^Z?G`^Zn(8!N(aN{C^> zZP_>fkIFD8IJ|q)*;IM`CQsh+%sa*F{k9u8CFI@kesU{X?b$*H#-3AJE?cB*Zw1cz zS}e5UAWPN<9lpv_h9PY`-&MWQ-MumTn(uP|t;aI$qeZ?(-~RsN)b$ zIkiNsSo$SP-fzu4-;yt@dq>^4e&4_1_PflmvCSB8Y(mka^v?jHYNPxtyf zsZ1vO`d*j(U8m#MIEHN4a=0abPyd(A|1E5VG~5vf`7Js3 z^uj7X(W^e2SEU6$Sub{iq3cuFo9u6b_iB3Qn`kzz>)uq<7AwVUA8(BIpN0YYsp*J9F+dakoR5n&N}WL)9*R; ze_eF<%b7cSuk8@`JMeC=0*lpTu0D50LCw#_AG|pkyr2EO?P>p4wB(!MqU&#YLzw0t z)IP>wHl2lG?>ePCt)NDosf;-{+>RdpFUJ$Pz(8xNO7%OHnd0)S>V{`spY{Ej@!{NT z^&bMoX?DjCtXcK7_-*uh*Xr+!kG0kA`tBorZf3LV-4-vtgd;lE2F3A0Wbs=9^hx`pa4^tC=Q z@1E-`_jsOVQinf!z7AQF#U9`+TbDX9CZY6}4^P69t`qMxC$?Ryu)crvhH!oVt7>b8 zSpLbjl_JIGZ5ag*L>MHpNQjzVUh)2P`{nCzXTM-DI-PSjK;Y6@>$^HCZn?r7$-98&R*h6T(^JWj!2uI zGAj@3FOp3UvtJ*?c`3JDqWVofci4+L5(hNTr{-#WipgVJ{`t?A${C%yrRU_FQ=1)E zc&U2q`(RsArc&VAzHaB%1$71AN{W}P&^|NIF3#k6(w28R31(*tKP(ZpO^N>}zNvkG z|HU1w*?;xU>`LqTT;42}*YU6l{6DD2Q{>j!d;GpBuD&*94wG5eM;@ny zwYMDJbx3BMoMU|a!u7jR3}<>ad9?^~eL1`8y~@e-{mwsj-8R(976@8olzC@fnNj<^ zW3O|#zsbzbKb#jEZgu;{2DA7RiH_lthn?* z<)fbLvE3y%mRIuLUjIXu-Tc(o$Tdo>vo|f8dg5L5vb93HD@CvM?Jn&5r*NZ1OZ@#E z$4TOG&NC+du|58?>UjM^O(&yS=DuRFS*_;DTstO2DD}j%wH43fIb&+iMf*RVW7S~b{BMpX|J!9X({pcRb8(lPmAq-J-#(9-TlLUXm-n?Si)|JNgh(CvwrBPc z5BqmN!@GV3PKgeB`ud8L!&8fjrA!A-%WDfA&bZrJ{j0TMf@bUG(v=R3EhZfC+xt^~ zOCOyXWd62Tb@SQqr&&qg{{|&IeAB~|uurQ>PjJr8JpPTh@7w3iSX{M@%-$|~GiK8H#F%Nn_}1QY zco8elrfjlbe1h7sS+g~(KbUWnieg&ax`^k2%u}U9Q(M0M(fRnj&Sb->QzqK0JNNxJ zKRGw|ux;@TcFnwLlgbu&a0{+IcyDX}hjX(H9zES$uNvm|XI}fsyc?e1E*Mwr&412q zl)iI*qOz0djM;f=TT;!#F0W!`JiugrVau#7lC0{MwHsqCx=z_WNS>tWCA{}zSuywktL|mY6Z_oTA^7A?lWDyJ;Ke=5?&A4d?%# zcihb2t<{dddw1LwFZ^xw=eH``fs+bvB$T4s)?^)7k$GrE?x9urZf%Dze`vjYx$*XW zSqsgDrnA*11s%0a+27z)o%g`>%~k1ct?lR13U=Kt+@<&6>(<3?RTp0d2Iw8?Q+i~2 zVwKyOU1De2#LnDmJH&NwLADj&&C9tjCUxii$xS)Lr>Jb066w0{Nba|{m5W|XH2mom zZ`=4uo!ch;cD|Xtn)||wmCh6PU$^=8Dt2RF%Dxtf__^WF?P7}4CU1(!oE7wE(GxB4 zLQb`aJ&*4trdI#h|1nYTx6{el?wU25HIM$N|GodqmXt~N1=h%9e&Y*cVDwnHvDL{c zQ%mA#kx;}<#Y1sI4^<|_u_fCitLnxGs;MVeEG)X?p~@N9m+a~x;Hnt#KF9sz<@F3q z1}#dm6NSj3d! z^r2(VM*07jncpgwh&lHzxZ8ciE#CRd%qfzg6{6-7wC+2UR~RcltlDA_7ISoJ5JTfK zr<9dGa~3XoFR*aq^$B_WPSzZ{7oGgyz2?#Q_v!BbZ&w{zrU!HAI#h7HO?tY^kgcx4 zG$Sz0%s8k{`s6g*9@kfr#k^O#)Xw)z3`dUU>h@n){1$ZKTok zwMSikJe|qA<7>I`jS6kqMSSk-ceJU7GBCS#-}ZF9&h5fJ|L})jf3D8`{_wP@ug~7e zZi~){v8{f;c=xxoNe{KYa3#dVunIW%EYFm+^Qk=kSYju4gSjh5%tTG`sOP**4#^MQ zwi({}<+$rq`J^k~x~7OTGkch9&6Dq)>n|u-!?iLfX5N9FL9czgPqGOGSx2liUtxFM zd&Nt|bD?h)bB~3sIp`#!pdxzq<>o7M|BD$_o^XE|ez)aWaa5&v%C5UdZvUUB+qqpb z;(MZ%%VUwI%+x7s7!n^|vMHK#t2_Sqi8*XnLg&r+INz7C?UayQRa=#M=jK<5zZPW| zhBG`$omShH_;^#=3)8IgwUWx!Txw3cR^Ok~wI^F3Dp`$x)2l<*Gd$-Wby(}MSA=!L zhZ_cE9jCbCj}};`AGja>{eqLS>5P@hSK@!DU$c5X;q>1>UJGU0&qTYQ*i(9BeO*|1 zyV9(K^(${ouy*|UbG_o;cjbGgJ-^5p_R4qF`G3YIP4_$b?$4MecP_8@ z5t+B_@YEU7`D%N9Ut1LGv3>@tLCZ3|+Id}<#TWmP_ctq*TK&h(-CUxI` z`BLHI+Wki|tBvmST}!tqP2*Vp*7JXPZEW~-CX<(SQC4#9%l!X^{IcmiBvU8g9RYkzmZVeUOKnu|h(gO$DholDxu z5dW8jOSyVt+qvbP)hAu|bLYL3&N5=(Kii(?5tl2Y;KZe?o--I-4Rcs_M)TRUr!PwR z7qa;!^6Wp8(x(tFd%EIQc+(1(<7Nx9w>fV)roT|mrd!Uk?M$go<&2e|{nsvGch(}d??<-mI zcd$P{a9^pqj^P%g?%(XJ-f4;}_A!^)G+eE+HHw>ifBt$!^S0}yOzYoTU-;-8u-N-O z=N5+vi}pM}9ul6n)IUW3Tq$q!hkN`J>t5x12c$J#-Box@uG@PRL*S;6@B5q1l%9$F z;lzCDNc8tWC-Yhs!Ig(Qt{jM+f5jkr~GoR?HYT2O1+sk$+ z@l5H~6Tfty^sRB;UMuO8V0uD|qo{$lh0)>{Jm#CP*O-uBVa-0?MyxZkjfz0VO-G{ey z*nf!bDEKHG?6Tw3|1&z)t<#Dm{=Yq_JAdL~{YB3Yc6;((@_nQ#x+u-oC~B&|2xm`# z<)%#ObvIkKepYea?F_yJ8Mb5a>7}o`3C9?)TU4 zo_#4R&9H3?$lPKOSo;3puBY2KMwK;f_$20}XmdXPCaY~zn%FY70LxA7^3Bcl9TzO) zHJo%4)tnxL-aUA-R6f1Mb(@XXLfif~FCS+-O}u9IEBajPoFBV6%R}z($@#J&H*!Xs zak*BI`*!8LUo2Hiq-+12me{G+Gf(~FJlVEryT+aGV(XGQ84fSmpuP88dt>Ei`2`$x zYUf?dat@fTHn{88GOZ%wXM~e-f%r|Oj3*h=PhZNpDZQ>;nEBLvVFZW6>3DWcgDk0n z-&QAOsyV-ZS^aN!N*i}bRLkm)`X7@|CrQd&KPpmDY*cwGu6fVL?;Ez=<(=oQ7t~t* zqh5K_azSPNhc@OtMfScKpXVy*Y?-lZzx$8L4=ppMx^j0KEdJ>ypEOIzL%V5$;GGwK zEA}%+Zaw&3%j#PDyIN-3KSo6pU&JhQtUeslGk4M9ywoQ*-#^}0D^UE{cgEStZ=+jG zICAe8{?(YX^3FL2`Mmk_K@I1pt-bdnkNTdq**{ZJsK2)F$N8G9le&%9q>A;Pg>)M{ zx)p!;R+YMhpn=IZQ~4vEArEB#XnV*yzlxD#_nB7N|Nht2IH~R0_Y;bK@EAq)-`T>y z`zxD)vv8~0-4i5^SXMK9J$7Ai@%pEg?1t~J@(Xo3 zr~Kgka#Q8pMtMf5STYTTx<4L4*@Bf`$YHzaTj;xLCFMjY};rF~#(-)p+ zSL~?KoavHsE-?GLfyR+3Ywxw5G74@=zj&tjcCgA4$D6VIdYbMDe;)omZ69paI?ws* zzFv2k&1t-ccz!ocR9m!(^J0~4*=th{?RoDNAKVU``$qZqr*OgCcn7PuKNl(4n`+I8 z+q2|QL^b!Nu3y^TPi{I(I(eKHKV#u;xbME){|_30as4aq_FTW+>@)XNe6-F1(H$>N z$(GD}%abRuvB#OWBw^j|ZA;E+bDv_JB4?(N@=ZqOU$N2pnydDIJp1#Rwr*H;ZIN^8 zPtiB&!l6|&?ySD=Fz1KSA!)`>yIa+KRy1t9JVElK?nD9MKMewp?T<=^H+BZ@mn?pt z|L91Wjzx>GRrk{`$u|_&{U`{o^QzqVOH=$b>zS#};(qd5Y8y+cB&yG^Pnu@_d-can z3ck-LS-9`xRo~YXkbKnHVmF&$mtv`$UYy=X$whAa^_x4d*63y(SN?EoexskB1Ea~d zhXHlR-94VF%xu0tH#jN%;zx%??PpFVyOiB2cq+P}!6Cr&%gg#B2aoJ{T`o}eb4Qg= z$4yzA_TS&i4fZxNvF_@zIG$50ZWJ{0xKit-YcDjd+cob+-P>Sje$VmSlgAQ^*XwqO zZ<)gqdb?~NPO)Wu(oHZRjjPF6lEH^sC;MpS%?HGq7Q@TdMHSeHHhfxLx}ABB?9r z)F-!;zr6dTURBO!C}=nMB)hjYQGuuUo_$!Y@I+a2ft#hmANlJqr8_^D6xDHkI>GYl zj`K%FGpa?8_3rQ9`*2%>--Dk8=eiQ}celqZ;mQ9WuXsF8yF}2a>(0O88N9PUZ95$Q zHi~(r;vDbx4|@BW{CFLnO1;r7PrjDxD*a4FY|bmyC9Nx>SZ8Dy*6-I0X`114dH;zu zi$kxsZ2ej`XPf$SIng;4jL&aHZ+sp- z{*d`~?tjnDUu_IxfeTw5q7#q&IQlGGtdwu+=|#%jx%sLO?{0sl-SYee*Bz0Wi+^u? z_w&P|w$`F`#;Ptn8^1noS@$>L-x`KkkwyLQ6$LLH%h=2J`<;EmwDyZeU0D_O3vKuJ z^jzy(UmIXqBp#>weX_k<+z+Wk(k>bND@`9H^zCc={LB1r+O(u!t2soLxQp+}+4^nT z8mHr{vXyF%pFi8ZC93O}qvi4qo-H4))$+Vv)~|Hv-^!?2d(O{u6l7R3A)<8l#&>^g z_r%UWa_{%+6R)oBo9eK5Yjg2C)jQAsi6nnuc(R*OQ}KdFeduC=!^{5|cbEikaF~}@ zW@J#&w$ggqb7!tbr&mjFn+n`3;#In5uzrIE?ta!DzC9jS}Ic)m;qKDy!LyPk&|&Ms4InJK=n{q{$zbN}|csP*?P?$O*4xAwWdMmt6qJaM!&~+(E((QTFBN?V?-tdloIOX3X=aV^`=kFm zKR@d~DCyi=Hp%*f=$dcG54?3>a_fSA?X)+q|MO3ko0Gtt&LHyQ49CpF{Yvcf(yv)D zt`*KDz1)G@Ozo~ojGoRsR;Vt=vZWs3EFDPPLl($x1l_iAH*M;k`?f>d?&g-(cE=Z+JKpOT$N!W$^fp%HU`3g6 z^VjmpLJw6#85C7F?csXx<`McFi z>D(XYo9TA#m;ak(l+U>_*H5BT;MpqajqiUlmt6j<@TSo1*#DlJ4SODj8%l3`CK~5& z&~&eW{atpvYuY)TSqWQ0UBg-goD%d3?dt9&K2&ojLek&muVE&qbM6jL&%2UQYk8OW$FSWMClwA*~yn z$`^jFndjoNY65TcMM-JX8nL}U#X0W(;XYAtl;yYi&6=R3DOVru47~DybBPzL+2T33 zFMDn3eZSCrQ{K&ALfn}SJD9$oS-7*-aJoy_`lD;I_skMi=)Y?Gc154?ivU}<(ADuT z6pvi-?_p;X&^nxPuPc0`X!Y%C!$)6R6`Zp#2rY5A71*=v{q~Ej3=A9(Kx+YXH5QBA zEf;&qtA9Z)%pl-i+FUqh&JVp{qBtx1ag_B6equw6UpDt<3ua z{}rmgSRZ89=*zz&A|<#^anIMg5>I!Av=U9j6O7@sVOCMIM%&S-W z@a9mbrDjyes*^GmHTNPM*p5Hx;4=GN-CMRz3;%JxPW<@|e$A=}k4@h% z{`9!MXX|I%gV#O2Ot3AjspEPe^)b~vTBx3rWo{rBI;$;Wq{*!$;v$HVaFt-&D~x-Wm}u1L+g8MNxdEt@OA3yKxYnP}LE;nerS!JsvqQ8qhfWzQa_QX=@ zk8JLWrHp%6zQ`W+-Fc$Ibm#T`tv0_+FWmfTBQbLt|7FShZL8O@J#60kdGC?;zb+iP z$FO)++nd?#3g&+gdQY(ZmHXxM|EXs{5oa;kzTWBiYpy7cw|`ShF7It< zJg08+k7d*33_0}FetKM5Xu)Y1~9D6!&Zr|@pW_^H}y zE8a!gh(F*I_~;vP-L9qM+9E-_{ml-$CFhve_tXZ zdHzvM1?wK2xbL%9{H^cxkUf8$`K;|5=3eo8ZsmX9F4_B(v$gi&nWi7%Fd571F ziaVcw`1eWw$-Uo7?}*#?%T!i+-QNDQha-6{zfnqkf3$wDpJ`r@ zak)=&lDpLN8~om7tiok#+D;{Ace|x;nz8nFt8QbGGA?I)|5X1`*`CJBciT4q&lfnU zERweBc=`J*mtVYl4%c_R_|2)a;OlOO2|D{Hzk7A}w9q%-8*`OzpE*=5r?E5q!keou z|MHZUFJD~3dck3SsDJ(I!x@p&w{v@28$Y&-SL1tV_dC7fe2tjp52w2De!WK)2R<0- ztobB9=Y(HXwB3OfyNY;cEn8H#d10wvJZJvh-qN$byLLC#EbhEHWdTE`hVBcE+s8aE zAI=bDJ#xv7HL1&9u2R(UgUl|LgRfYn)XuL^-T6+jEqQH4xa0l0#5Tk1SI@r59#+`* zzL8b?pvj7vhnMXA7Zy}1?~<&`t*@}`$-Qac{G?P*dzk#?NmsHysaIScR=(i;ece3~ zpUuQoH7-rFGq~o_Az{1vzWc6UcPkEk61MP`7e2Gv`{kUT-VWoi|HfO)@3@t9p5DhS zx?p{=WAst+MJzLwe={pHN9wq|3Vm9#wpL>Kr@SdK--4fsUq1Zgu3JyYXZMW!JI7b_ zeYkAm709&wt)|d6%8}Qio%u%)36T|r=C&hFAiU%o|+dWL{c~rIU8wxLKao`L{<4#^(Khe0feBy+_ zx>?8Hyv)Yw8B*n?|xbP%QU9=YH0Qc*N2l1bjP26(RcrO%GcbKlo|Eh zhqE$fE){g0r+lLM&?Ifn%Pki1o7>l|{V%w@NWP$SCrj`2^^Zg~BK>xK6x7T4XX`gB zTYN3^k3&9_4!yP6ZsQejy=80d{WS{&mOi_t?4kH#L$t@*e-geEO^^JpZz{3+wEtrG znh8P7ZDtoFo+Mp2xbZ8|>Xg}UkN;Yhj{>qyFB+Ce@o)LKtY-U_qf8Eye(YoSaJV?D z`D35*vpee11{q<-mwwc(dbB&^P3^phB>PLBxvi%CW+|NSY*6qfw8!L*fBs*&wobil z&+ks{H}~XSobA}>&StF1cxJP=M1#7|uhZ8IWwMv1y%Br$J6>UyO-sq|8J&ye{Vk0z z3clHN-|wY}m2mn5^RkLC;hD3nU#{8P%D9{F=+!E-4|n<|Mne;IW+8is+GcWo7)jQmU)2Mg#-^pk0$UJ*yD{RW%ZI-=m%S*l6 zM;vN&x1Rl;)b}-TuAOhj3f`GJIqoUUl8NHrnaZC1b2H~r7P+qiP75Y@%=Q1A#jKiZ zvzbe2b-}4^Mq&Fp7kY4WN~$lvXSuv_Ht$+HtNDl6x(Y0BF Q63`w%Pgg&ebxsLQ0F0-OtN;K2 diff --git a/lnbits/extensions/scrub/static/js/index.js b/lnbits/extensions/scrub/static/js/index.js deleted file mode 100644 index 43990792..00000000 --- a/lnbits/extensions/scrub/static/js/index.js +++ /dev/null @@ -1,143 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -var locationPath = [ - window.location.protocol, - '//', - window.location.host, - window.location.pathname -].join('') - -var mapScrubLink = obj => { - obj._data = _.clone(obj) - obj.date = Quasar.utils.date.formatDate( - new Date(obj.time * 1000), - 'YYYY-MM-DD HH:mm' - ) - obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount) - obj.print_url = [locationPath, 'print/', obj.id].join('') - obj.pay_url = [locationPath, obj.id].join('') - return obj -} - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return { - checker: null, - payLinks: [], - payLinksTable: { - pagination: { - rowsPerPage: 10 - } - }, - formDialog: { - show: false, - data: {} - }, - qrCodeDialog: { - show: false, - data: null - } - } - }, - methods: { - getScrubLinks() { - LNbits.api - .request( - 'GET', - '/scrub/api/v1/links?all_wallets=true', - this.g.user.wallets[0].inkey - ) - .then(response => { - this.payLinks = response.data.map(mapScrubLink) - }) - .catch(err => { - clearInterval(this.checker) - LNbits.utils.notifyApiError(err) - }) - }, - closeFormDialog() { - this.resetFormData() - }, - openUpdateDialog(linkId) { - const link = _.findWhere(this.payLinks, {id: linkId}) - - this.formDialog.data = _.clone(link._data) - this.formDialog.show = true - }, - sendFormData() { - const wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet - }) - let data = Object.freeze(this.formDialog.data) - console.log(wallet, data) - - if (data.id) { - this.updateScrubLink(wallet, data) - } else { - this.createScrubLink(wallet, data) - } - }, - resetFormData() { - this.formDialog = { - show: false, - data: {} - } - }, - updateScrubLink(wallet, data) { - LNbits.api - .request('PUT', '/scrub/api/v1/links/' + data.id, wallet.adminkey, data) - .then(response => { - this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id) - this.payLinks.push(mapScrubLink(response.data)) - this.formDialog.show = false - this.resetFormData() - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - createScrubLink(wallet, data) { - LNbits.api - .request('POST', '/scrub/api/v1/links', wallet.adminkey, data) - .then(response => { - console.log('RES', response) - this.getScrubLinks() - this.formDialog.show = false - this.resetFormData() - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - deleteScrubLink(linkId) { - var link = _.findWhere(this.payLinks, {id: linkId}) - - LNbits.utils - .confirmDialog('Are you sure you want to delete this pay link?') - .onOk(() => { - LNbits.api - .request( - 'DELETE', - '/scrub/api/v1/links/' + linkId, - _.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey - ) - .then(response => { - this.payLinks = _.reject(this.payLinks, obj => obj.id === linkId) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }) - } - }, - created() { - if (this.g.user.wallets.length) { - var getScrubLinks = this.getScrubLinks - getScrubLinks() - } - } -}) diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py deleted file mode 100644 index 26249bb1..00000000 --- a/lnbits/extensions/scrub/tasks.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -import json -from http import HTTPStatus -from math import floor -from urllib.parse import urlparse - -import httpx -from fastapi import HTTPException - -from lnbits import bolt11 -from lnbits.core.models import Payment -from lnbits.core.services import pay_invoice -from lnbits.helpers import get_current_extension_name -from lnbits.tasks import register_invoice_listener - -from .crud import get_scrub_by_wallet - - -async def wait_for_paid_invoices(): - invoice_queue = asyncio.Queue() - register_invoice_listener(invoice_queue, get_current_extension_name()) - - while True: - payment = await invoice_queue.get() - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment): - # (avoid loops) - if payment.extra.get("tag") == "scrubed": - # already scrubbed - return - - scrub_link = await get_scrub_by_wallet(payment.wallet_id) - - if not scrub_link: - return - - from lnbits.core.views.api import api_lnurlscan - - # DECODE LNURLP OR LNADDRESS - data = await api_lnurlscan(scrub_link.payoraddress) - - # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267 - domain = urlparse(data["callback"]).netloc - rounded_amount = floor(payment.amount / 1000) * 1000 - - async with httpx.AsyncClient() as client: - try: - r = await client.get( - data["callback"], - params={"amount": rounded_amount}, - timeout=40, - ) - if r.is_error: - raise httpx.ConnectError("issue with scrub callback") - except (httpx.ConnectError, httpx.RequestError): - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"Failed to connect to {domain}.", - ) - - params = json.loads(r.text) - if params.get("status") == "ERROR": - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"{domain} said: '{params.get('reason', '')}'", - ) - - invoice = bolt11.decode(params["pr"]) - - if invoice.amount_msat != rounded_amount: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.", - ) - - payment_hash = await pay_invoice( - wallet_id=payment.wallet_id, - payment_request=params["pr"], - description=data["description"], - extra={"tag": "scrubed"}, - ) - - return { - "payment_hash": payment_hash, - # maintain backwards compatibility with API clients: - "checking_id": payment_hash, - } diff --git a/lnbits/extensions/scrub/templates/scrub/_api_docs.html b/lnbits/extensions/scrub/templates/scrub/_api_docs.html deleted file mode 100644 index ae3f44d8..00000000 --- a/lnbits/extensions/scrub/templates/scrub/_api_docs.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - GET /scrub/api/v1/links -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<pay_link_object>, ...] -
Curl example
- curl -X GET {{ request.base_url }}scrub/api/v1/links?all_wallets=true - -H "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - GET - /scrub/api/v1/links/<scrub_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- {"id": <string>, "wallet": <string>, "description": - <string>, "payoraddress": <string>} -
Curl example
- curl -X GET {{ request.base_url }}scrub/api/v1/links/<pay_id> - -H "X-Api-Key: {{ user.wallets[0].inkey }}" - -
-
-
- - - - POST /scrub/api/v1/links -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"wallet": <string>, "description": <string>, - "payoraddress": <string>} -
- Returns 201 CREATED (application/json) -
- {"id": <string>, "wallet": <string>, "description": - <string>, "payoraddress": <string>} -
Curl example
- curl -X POST {{ request.base_url }}scrub/api/v1/links -d '{"wallet": - <string>, "description": <string>, "payoraddress": - <string>}' -H "Content-type: application/json" -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - PUT - /scrub/api/v1/links/<pay_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"wallet": <string>, "description": <string>, - "payoraddress": <string>} -
- Returns 200 OK (application/json) -
- {"id": <string>, "wallet": <string>, "description": - <string>, "payoraddress": <string>} -
Curl example
- curl -X PUT {{ request.base_url }}scrub/api/v1/links/<pay_id> - -d '{"wallet": <string>, "description": <string>, - "payoraddress": <string>}' -H "Content-type: application/json" - -H "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /scrub/api/v1/links/<pay_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.base_url - }}scrub/api/v1/links/<pay_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
-
diff --git a/lnbits/extensions/scrub/templates/scrub/_lnurl.html b/lnbits/extensions/scrub/templates/scrub/_lnurl.html deleted file mode 100644 index f2ba8661..00000000 --- a/lnbits/extensions/scrub/templates/scrub/_lnurl.html +++ /dev/null @@ -1,31 +0,0 @@ - - - -

- WARNING: LNURL must be used over https or TOR
- LNURL is a range of lightning-network standards that allow us to use - lightning-network differently. An LNURL-pay is a link that wallets use - to fetch an invoice from a server on-demand. The link or QR code is - fixed, but each time it is read by a compatible wallet a new QR code is - issued by the service. It can be used to activate machines without them - having to maintain an electronic screen to generate and show invoices - locally, or to sell any predefined good or service automatically. -

-

- Exploring LNURL and finding use cases, is really helping inform - lightning protocol development, rather than the protocol dictating how - lightning-network should be engaged with. -

- Check -
Awesome LNURL - for further information. - - - diff --git a/lnbits/extensions/scrub/templates/scrub/index.html b/lnbits/extensions/scrub/templates/scrub/index.html deleted file mode 100644 index a3756df3..00000000 --- a/lnbits/extensions/scrub/templates/scrub/index.html +++ /dev/null @@ -1,156 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New scrub link - - - - - -
-
-
Scrub links
-
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
{{SITE_TITLE}} Scrub extension
-

- Automatically forward funds (Scrub) that get paid to the LNbits - wallet, to an LNURLpay or Lightning Address. -
- More info in Scrub's - readme. -

-

- Important: wallet will need a float to account for - any fees, before being able to push a payment -

-
- - - - {% include "scrub/_api_docs.html" %} - - {% include "scrub/_lnurl.html" %} - - -
-
- - - - - - - - - -
- Update pay link - Create pay link - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/scrub/views.py b/lnbits/extensions/scrub/views.py deleted file mode 100644 index 48958013..00000000 --- a/lnbits/extensions/scrub/views.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import Depends, Request -from fastapi.templating import Jinja2Templates -from starlette.responses import HTMLResponse - -from lnbits.core.models import User -from lnbits.decorators import check_user_exists - -from . import scrub_ext, scrub_renderer - -templates = Jinja2Templates(directory="templates") - - -@scrub_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return scrub_renderer().TemplateResponse( - "scrub/index.html", {"request": request, "user": user.dict()} - ) diff --git a/lnbits/extensions/scrub/views_api.py b/lnbits/extensions/scrub/views_api.py deleted file mode 100644 index eae0098d..00000000 --- a/lnbits/extensions/scrub/views_api.py +++ /dev/null @@ -1,107 +0,0 @@ -from http import HTTPStatus - -from fastapi import Depends, Query -from starlette.exceptions import HTTPException - -from lnbits.core.crud import get_user -from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key - -from . import scrub_ext -from .crud import ( - create_scrub_link, - delete_scrub_link, - get_scrub_link, - get_scrub_links, - unique_scrubed_wallet, - update_scrub_link, -) -from .models import CreateScrubLink - - -@scrub_ext.get("/api/v1/links", status_code=HTTPStatus.OK) -async def api_links( - wallet: WalletTypeInfo = Depends(get_key_type), - all_wallets: bool = Query(False), -): - wallet_ids = [wallet.wallet.id] - - if all_wallets: - user = await get_user(wallet.wallet.user) - wallet_ids = user.wallet_ids if user else [] - - try: - return [link.dict() for link in await get_scrub_links(wallet_ids)] - - except: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, - detail="No SCRUB links made yet", - ) - - -@scrub_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_link_retrieve(link_id, wallet: WalletTypeInfo = Depends(get_key_type)): - link = await get_scrub_link(link_id) - - if not link: - raise HTTPException( - detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN - ) - - return link - - -@scrub_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) -@scrub_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_scrub_create_or_update( - data: CreateScrubLink, - link_id=None, - wallet: WalletTypeInfo = Depends(require_admin_key), -): - if link_id: - link = await get_scrub_link(link_id) - - if not link: - raise HTTPException( - detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN - ) - - link = await update_scrub_link(**data.dict(), link_id=link_id) - else: - wallet_has_scrub = await unique_scrubed_wallet(wallet_id=data.wallet) - if wallet_has_scrub > 0: - raise HTTPException( - detail="Wallet is already being Scrubbed", - status_code=HTTPStatus.FORBIDDEN, - ) - link = await create_scrub_link(data=data) - - return link - - -@scrub_ext.delete("/api/v1/links/{link_id}") -async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)): - link = await get_scrub_link(link_id) - - if not link: - raise HTTPException( - detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if link.wallet != wallet.wallet.id: - raise HTTPException( - detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN - ) - - await delete_scrub_link(link_id) - return "", HTTPStatus.NO_CONTENT