diff --git a/nostr_dvm/dvm.py b/nostr_dvm/dvm.py index ff42a09..a1c97d1 100644 --- a/nostr_dvm/dvm.py +++ b/nostr_dvm/dvm.py @@ -126,7 +126,7 @@ class DVM: print("[" + self.dvm_config.NIP89.NAME + "] Checking Subscription status") subscription_status = nip88_has_active_subscription(PublicKey.parse(user.npub), self.dvm_config.NIP88.DTAG, self.client, - self.dvm_config) + self.dvm_config.PUBLIC_KEY) if subscription_status["isActive"]: print("User subscribed until: " + str( @@ -317,7 +317,7 @@ class DVM: config=self.dvm_config) # a regular note - elif not anon: + elif not anon and dvm_config.NIP88 is None: print("[" + self.dvm_config.NIP89.NAME + "] Profile Zap received for DVM balance: " + str(invoice_amount) + " Sats from " + str(user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, diff --git a/nostr_dvm/subscription.py b/nostr_dvm/subscription.py index 2a63c25..c7ba831 100644 --- a/nostr_dvm/subscription.py +++ b/nostr_dvm/subscription.py @@ -11,10 +11,12 @@ from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNot from nostr_dvm.utils.database_utils import fetch_user_metadata from nostr_dvm.utils.definitions import EventDefinitions from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nip88_utils import nip88_has_active_subscription from nostr_dvm.utils.nip89_utils import NIP89Config from nostr_dvm.utils.nwc_tools import nwc_zap from nostr_dvm.utils.subscription_utils import create_subscription_sql_table, add_to_subscription_sql_table, \ - get_from_subscription__sql_table, update_subscription_sql_table + get_from_subscription_sql_table, update_subscription_sql_table, get_all_subscriptions_from_sql_table, \ + delete_from_subscription_sql_table from nostr_dvm.utils.zap_utils import create_bolt11_lud16, zaprequest @@ -95,13 +97,85 @@ class Subscription: kind7001eventid = tag.as_vec()[1] if kind7001eventid != "": - subscription = get_from_subscription__sql_table("db/subscriptions", kind7001eventid) + subscription = get_from_subscription_sql_table("db/subscriptions", kind7001eventid) if subscription is not None: update_subscription_sql_table("db/subscriptions", kind7001eventid, recipient, subscription.subscriber, subscription.nwc, subscription.cadence, subscription.amount, subscription.begin, subscription.end, - subscription.tier_dtag, subscription.zaps, subscription.recipe, False) + subscription.tier_dtag, subscription.zaps, subscription.recipe, + False, Timestamp.now().as_secs()) + + def infer_subscription_end_time(start, cadence): + end = start + if cadence == "daily": + end = start + 60 * 60 * 24 + elif cadence == "weekly": + end = start + 60 * 60 * 24 * 7 + elif cadence == "monthly": + # TODO check days of month -.- + end = start + 60 * 60 * 24 * 31 + elif cadence == "yearly": + # TODO check extra day every 4 years + end = start + 60 * 60 * 24 * 356 + return end + + def pay_zap_split(nwc, overall_amount, zaps): + overallsplit = 0 + + for zap in zaps: + overallsplit += int(zap['split']) + + zapped_amount = 0 + for zap in zaps: + name, nip05, lud16 = fetch_user_metadata(zap['key'], self.client) + splitted_amount = math.floor( + (int(zap['split']) / overallsplit) * int(overall_amount) / 1000) + # invoice = create_bolt11_lud16(lud16, splitted_amount) + # TODO add details about DVM in message + invoice = zaprequest(lud16, splitted_amount, "DVM subscription", None, + PublicKey.parse(zap['key']), self.keys, DVMConfig.RELAY_LIST) + print(invoice) + if invoice is not None: + nwc_event_id = nwc_zap(nwc, invoice, self.keys, zap['relay']) + if nwc_event_id is None: + print("error zapping " + lud16) + else: + zapped_amount = zapped_amount + (splitted_amount * 1000) + print(str(zapped_amount) + "/" + str(overall_amount)) + + if zapped_amount < overall_amount * 0.8: # TODO how do we handle failed zaps for some addresses? we are ok with 80% for now + success = False + else: + print("Zapped successfully") + success = True + # if no active subscription exists OR the subscription ended, pay + + return success + + def make_subscription_zap_recipe(event7001, recipient, subscriber, start, end, tier_dtag): + message = "payed by subscription service" + pTag = Tag.parse(["p", recipient]) + PTag = Tag.parse(["P", subscriber]) + eTag = Tag.parse(["e", event7001]) + validTag = Tag.parse(["valid", str(start), str(end)]) + tierTag = Tag.parse(["tier", tier_dtag]) + alttag = Tag.parse(["alt", "This is a NIP90 DVM Subscription Payment Recipe"]) + + tags = [pTag, PTag, eTag, validTag, tierTag, alttag] + + event = EventBuilder(EventDefinitions.KIND_NIP88_PAYMENT_RECIPE, + message, tags).to_event(self.keys) + + dvmconfig = DVMConfig() + signer = NostrSigner.keys(self.keys) + client = Client(signer) + for relay in dvmconfig.RELAY_LIST: + client.add_relay(relay) + client.connect() + recipeid = client.send_event(event) + recipe = recipeid.to_hex() + return recipe def handle_dm(nostr_event): @@ -123,105 +197,59 @@ class Subscription: tier_dtag = jsonevent['tier_dtag'] start = Timestamp.now().as_secs() - end = Timestamp.now().as_secs() isactivesubscription = False recipe = "" - subscription = get_from_subscription__sql_table("db/subscriptions", event7001) - if subscription is not None and subscription.end > start: - start = subscription.end - isactivesubscription = True + subscription = get_from_subscription_sql_table("db/subscriptions", event7001) + #if subscription is not None and subscription.end > start: + # start = subscription.end + # isactivesubscription = True - if cadence == "daily": - end = start + 60 * 60 * 24 - elif cadence == "weekly": - end = start + 60 * 60 * 24 * 7 - elif cadence == "monthly": - # TODO check days of month -.- - end = start + 60 * 60 * 24 * 31 - elif cadence == "yearly": - # TODO check extra day every 4 years - end = start + 60 * 60 * 24 * 356 zapsstr = json.dumps(jsonevent['zaps']) print(zapsstr) success = True if subscription is None or subscription.end <= Timestamp.now().as_secs(): + #rather check nostr if our db is right + subscription_status = nip88_has_active_subscription( + PublicKey.parse(subscriber), + tier_dtag, self.client, recipient) - overallsplit = 0 + if not subscription_status["isActive"]: + success = pay_zap_split(nwc, overall_amount, jsonevent['zaps']) + start = Timestamp.now().as_secs() + end = infer_subscription_end_time(start, cadence) + else: + start = Timestamp.now().as_secs() + end = subscription_status["validUntil"] + else: + start = subscription.begin + end = subscription.end - for zap in jsonevent['zaps']: - overallsplit += int(zap['split']) - - zapped_amount = 0 - for zap in jsonevent['zaps']: - name, nip05, lud16 = fetch_user_metadata(zap['key'], self.client) - splitted_amount = math.floor( - (int(zap['split']) / overallsplit) * int(jsonevent['overall_amount']) / 1000) - # invoice = create_bolt11_lud16(lud16, splitted_amount) - # TODO add details about DVM in message - invoice = zaprequest(lud16, splitted_amount, "DVM subscription", None, - PublicKey.parse(zap['key']), self.keys, DVMConfig.RELAY_LIST) - print(invoice) - if invoice is not None: - nwc_event_id = nwc_zap(nwc, invoice, self.keys, zap['relay']) - if nwc_event_id is None: - print("error zapping " + lud16) - else: - zapped_amount = zapped_amount + (splitted_amount * 1000) - print(str(zapped_amount) + "/" + str(overall_amount)) - - if zapped_amount < overall_amount * 0.8: # TODO how do we handle failed zaps for some addresses? we are ok with 80% for now - success = False - else: - print("Zapped successfully") - # if no active subscription exists OR the subscription ended, pay if success: - message = "payed by subscription service" - pTag = Tag.parse(["p", recipient]) - PTag = Tag.parse(["P", subscriber]) - eTag = Tag.parse(["e", event7001]) - validTag = Tag.parse(["valid", str(start), str(end)]) - tierTag = Tag.parse(["tier", tier_dtag]) - alttag = Tag.parse(["alt", "This is a NIP90 DVM Subscription Payment Recipe"]) - - tags = [pTag, PTag, eTag, validTag, tierTag, alttag] - - event = EventBuilder(EventDefinitions.KIND_NIP88_PAYMENT_RECIPE, - message, tags).to_event(self.keys) - - dvmconfig = DVMConfig() - signer = NostrSigner.keys(self.keys) - client = Client(signer) - for relay in dvmconfig.RELAY_LIST: - client.add_relay(relay) - client.connect() - recipeid = client.send_event(event) - recipe = recipeid.to_hex() + recipe = make_subscription_zap_recipe(event7001, recipient, subscriber, start, end, tier_dtag) print("RECIPE " + recipe) isactivesubscription = True if subscription is None: add_to_subscription_sql_table("db/subscriptions", event7001, recipient, subscriber, nwc, cadence, overall_amount, start, end, tier_dtag, - zapsstr, recipe, isactivesubscription) + zapsstr, recipe, isactivesubscription, Timestamp.now().as_secs()) print("new subscription entry") else: update_subscription_sql_table("db/subscriptions", event7001, recipient, subscriber, nwc, cadence, overall_amount, start, end, - tier_dtag, zapsstr, recipe, isactivesubscription) + tier_dtag, zapsstr, recipe, isactivesubscription, + Timestamp.now().as_secs()) print("updated subscription entry") except Exception as e: print(e) - - - except Exception as e: print("Error in Subscriber " + str(e)) @@ -230,9 +258,65 @@ class Subscription: try: while True: time.sleep(60.0) + subscriptions = get_all_subscriptions_from_sql_table("db/subscriptions") + print("Checking " + str(len(subscriptions)) + " entries..") + for subscription in subscriptions: + if subscription.active: + if subscription.end < Timestamp.now().as_secs(): + # We could directly zap, but let's make another check if our subscription expired + subscription_status = nip88_has_active_subscription( + PublicKey.parse(subscription.subscriber), + subscription.tier_dtag, self.client, subscription.recipent) + + if not subscription_status["isActive"] or subscription_status["expires"]: + update_subscription_sql_table("db/subscriptions", subscription.id, + subscription.recipent, + subscription.subscriber, subscription.nwc, + subscription.cadence, subscription.amount, + subscription.begin, subscription.end, + subscription.tier_dtag, subscription.zaps, + subscription.recipe, + False, + Timestamp.now().as_secs()) + else: + zaps = json.loads(subscription.zaps) + success = pay_zap_split(subscription.nwc, subscription.amount, zaps) + if success: + end = infer_subscription_end_time(Timestamp.now().as_secs(), subscription.cadence) + recipe = make_subscription_zap_recipe(subscription.id, subscription.recipent, + subscription.subscriber, subscription.begin, + end, subscription.tier_dtag) + else: + end = Timestamp.now().as_secs() + recipe = subscription.recipe + + update_subscription_sql_table("db/subscriptions", subscription.id, + subscription.recipent, + subscription.subscriber, subscription.nwc, + subscription.cadence, subscription.amount, + subscription.begin, end, + subscription.tier_dtag, subscription.zaps, recipe, + success, + Timestamp.now().as_secs()) + print("updated subscription entry") - print("Checking Subscription") + else: + delete_threshold = 60 * 60 * 24 * 365 + if subscription.cadence == "daily": + delete_threshold = 60 * 60 * 24 * 7 # After 7 days, delete the subscription, user can make a new one + elif subscription.cadence == "weekly": + delete_threshold = 60 * 60 * 24 * 21 # After 21 days, delete the subscription, user can make a new one + elif subscription.cadence == "monthly": + delete_threshold = 60 * 60 * 24 * 60 # After 60 days, delete the subscription, user can make a new one + elif subscription.cadence == "yearoy": + delete_threshold = 60 * 60 * 24 * 500 # After 500 days, delete the subscription, user can make a new one + + if subscription.end < (Timestamp.now().as_secs() - delete_threshold): + delete_from_subscription_sql_table("db/subscriptions", subscription.id) + print("Delete expired subscription") + + print(str(Timestamp.now().as_secs()) + " Checking Subscription") except KeyboardInterrupt: print('Stay weird!') os.kill(os.getpid(), signal.SIGTERM) diff --git a/nostr_dvm/tasks/content_discovery_currently_popular.py b/nostr_dvm/tasks/content_discovery_currently_popular.py index 11b934c..cdc0b48 100644 --- a/nostr_dvm/tasks/content_discovery_currently_popular.py +++ b/nostr_dvm/tasks/content_discovery_currently_popular.py @@ -219,7 +219,7 @@ def build_example_subscription(name, identifier, admin_config): "lud16": dvm_config.LN_ADDRESS, "encryptionSupported": True, "cashuAccepted": True, - "amount": "subscription", + "subscription": True, "nip90Params": { "max_results": { "required": False, diff --git a/nostr_dvm/utils/nip88_utils.py b/nostr_dvm/utils/nip88_utils.py index ff65e0e..e86f967 100644 --- a/nostr_dvm/utils/nip88_utils.py +++ b/nostr_dvm/utils/nip88_utils.py @@ -88,15 +88,16 @@ def nip88_delete_announcement(eid: str, keys: Keys, dtag: str, client: Client, c send_event(event, client, config) -def nip88_has_active_subscription(user: PublicKey, tiereventdtag, client: Client, dvm_config): +def nip88_has_active_subscription(user: PublicKey, tiereventdtag, client: Client, receiver_public_key_hex): subscription_status = { "isActive": False, "validUntil": 0, "subscriptionId": "", + "expires": False, } subscriptionfilter = Filter().kind(definitions.EventDefinitions.KIND_NIP88_PAYMENT_RECIPE).pubkey( - PublicKey.parse(dvm_config.PUBLIC_KEY)).custom_tag(SingleLetterTag.uppercase(Alphabet.P), + PublicKey.parse(receiver_public_key_hex)).custom_tag(SingleLetterTag.uppercase(Alphabet.P), [user.to_hex()]).limit(1) evts = client.get_events_of([subscriptionfilter], timedelta(seconds=5)) if len(evts) > 0: @@ -114,6 +115,17 @@ def nip88_has_active_subscription(user: PublicKey, tiereventdtag, client: Client if subscription_status["validUntil"] > Timestamp.now().as_secs() & matchesdtag: subscription_status["isActive"] = True + if subscription_status["isActive"]: + cancel_filter = Filter().kind(EventDefinitions.KIND_NIP88_STOP_SUBSCRIPTION_EVENT).author( + user).pubkey(PublicKey.parse(receiver_public_key_hex)).event(EventId.parse(subscription_status["subscriptionId"])).limit(1) + cancel_events = client.get_events_of([cancel_filter], timedelta(seconds=5)) + if len(cancel_events) > 0: + if cancel_events[0].created_at().as_secs() > evts[0].created_at().as_secs(): + subscription_status["expires"] = True + + + + return subscription_status diff --git a/nostr_dvm/utils/subscription_utils.py b/nostr_dvm/utils/subscription_utils.py index 5c34688..b69d71a 100644 --- a/nostr_dvm/utils/subscription_utils.py +++ b/nostr_dvm/utils/subscription_utils.py @@ -17,6 +17,7 @@ class Subscription: zaps: str recipe: str active: bool + lastupdate: int def create_subscription_sql_table(db): @@ -40,7 +41,8 @@ def create_subscription_sql_table(db): tier_dtag text, zaps text, recipe text, - active boolean + active boolean, + lastupdate int ); """) @@ -52,23 +54,23 @@ def create_subscription_sql_table(db): def add_to_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, - recipe, active): + recipe, active, lastupdate): try: con = sqlite3.connect(db) cur = con.cursor() - data = (id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active) + data = (id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, lastupdate) print(id) print(recipient) print(subscriber) print(nwc) - cur.execute("INSERT or IGNORE INTO subscriptions VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", data) + cur.execute("INSERT or IGNORE INTO subscriptions VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", data) con.commit() con.close() except Error as e: print("Error when Adding to DB: " + str(e)) -def get_from_subscription__sql_table(db, id): +def get_from_subscription_sql_table(db, id): try: con = sqlite3.connect(db) cur = con.cursor() @@ -91,6 +93,7 @@ def get_from_subscription__sql_table(db, id): subscription.zaps = row[9] subscription.recipe = row[10] subscription.active = row[11] + subscription.lastupdate = row[12] return subscription @@ -99,12 +102,59 @@ def get_from_subscription__sql_table(db, id): return None -def update_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, - recipe, active): +def get_all_subscriptions_from_sql_table(db): + try: + con = sqlite3.connect(db) + cursor = con.cursor() + + sqlite_select_query = """SELECT * from subscriptions""" + cursor.execute(sqlite_select_query) + records = cursor.fetchall() + subscriptions = [] + for row in records: + subscription = Subscription + subscription.id = row[0] + subscription.recipent = row[1] + subscription.subscriber = row[2] + subscription.nwc = row[3] + subscription.cadence = row[4] + subscription.amount = row[5] + subscription.begin = row[6] + subscription.end = row[7] + subscription.tier_dtag = row[8] + subscription.zaps = row[9] + subscription.recipe = row[10] + subscription.active = row[11] + subscription.lastupdate = row[12] + subscriptions.append(subscription) + + + cursor.close() + return subscriptions + + except sqlite3.Error as error: + print("Failed to read data from sqlite table", error) + finally: + if con: + con.close() + #print("The SQLite connection is closed") + +def delete_from_subscription_sql_table(db, id): try: con = sqlite3.connect(db) cur = con.cursor() - data = (recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, id) + cur.execute("DELETE FROM subscriptions WHERE id=?", (id,)) + con.commit() + con.close() + except Error as e: + print(e) + +def update_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, + recipe, active, lastupdate): + try: + con = sqlite3.connect(db) + cur = con.cursor() + data = (recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, lastupdate, id) cur.execute(""" UPDATE subscriptions SET recipient = ? , @@ -117,7 +167,8 @@ def update_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, a tier_dtag = ?, zaps = ?, recipe = ?, - active = ? + active = ?, + lastupdate = ? WHERE id = ?""", data) con.commit() diff --git a/ui/noogle/src/components/Login.vue b/ui/noogle/src/components/Login.vue index 575e2ce..d398a6c 100644 --- a/ui/noogle/src/components/Login.vue +++ b/ui/noogle/src/components/Login.vue @@ -757,7 +757,7 @@ export default { if(!jsonentry.amount){ jsonentry.amount = "" } - if(jsonentry.amount === "subscription"){ + if(jsonentry.subscription === true){ // if(susbcrition_tier) { const filter = new Filter().kind(37001).author(entry.author) let tiers = await client.getEventsOf([filter], Duration.fromSecs(5)) diff --git a/ui/noogle/src/components/Nip89view.vue b/ui/noogle/src/components/Nip89view.vue index 6c0733b..209295d 100644 --- a/ui/noogle/src/components/Nip89view.vue +++ b/ui/noogle/src/components/Nip89view.vue @@ -90,8 +90,8 @@
Free
Flexible
+Subscription
-Subscription
Free
-Flexible
+Free
+Flexible
+Subscription
Connected to Alby Wallet.