From 43f9ae8acfb47c4e69a6550db71015841ce448e3 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 29 Aug 2025 21:07:47 +0200 Subject: [PATCH] Show notification when new release of BSX --- basicswap/basicswap.py | 69 ++++++++ basicswap/basicswap_util.py | 1 + basicswap/js_server.py | 81 ++++++++- .../static/js/modules/notification-manager.js | 59 +++++-- basicswap/templates/settings.html | 155 +++++++++++++++++- basicswap/templates/wallet.html | 2 +- basicswap/ui/page_settings.py | 4 + basicswap/ui/page_wallet.py | 1 + 8 files changed, 357 insertions(+), 15 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 90956b9..ec813c4 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -382,6 +382,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self._expiring_offers = [] # List of offers expiring soon self._updating_wallets_info = {} self._last_updated_wallets_info = 0 + + self.check_updates_seconds = self.get_int_setting( + "check_updates_seconds", 24 * 60 * 60, 60 * 60, 7 * 24 * 60 * 60 + ) + self._last_checked_updates = 0 + self._latest_version = None + self._update_available = False self._notifications_enabled = self.settings.get("notifications_enabled", True) self._disabled_notification_types = self.settings.get( "disabled_notification_types", [] @@ -1325,6 +1332,65 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if ci.isWalletLocked(): raise LockedCoinError(Coins.PART) + def checkForUpdates(self) -> None: + if not self.settings.get("check_updates", True): + return + + now = time.time() + if now - self._last_checked_updates < self.check_updates_seconds: + return + + self._last_checked_updates = now + self.log.info("Checking for BasicSwap updates...") + + try: + url = "https://api.github.com/repos/basicswap/basicswap/tags" + response_data = self.readURL(url, timeout=30) + tags_data = json.loads(response_data.decode("utf-8")) + + if not tags_data or not isinstance(tags_data, list) or len(tags_data) == 0: + self.log.warning("Could not determine latest version from GitHub tags") + return + + latest_tag = tags_data[0].get("name", "").lstrip("v") + if not latest_tag: + self.log.warning("Could not determine latest version from GitHub tags") + return + + self._latest_version = latest_tag + current_version = __version__ + + def version_tuple(v): + return tuple(map(int, v.split("."))) + + try: + if version_tuple(latest_tag) > version_tuple(current_version): + if not self._update_available: + self._update_available = True + self.log.info( + f"Update available: v{latest_tag} (current: v{current_version})" + ) + + self.notify( + NT.UPDATE_AVAILABLE, + { + "current_version": current_version, + "latest_version": latest_tag, + "release_url": f"https://github.com/basicswap/basicswap/releases/tag/v{latest_tag}", + "release_notes": f"New version v{latest_tag} is available. Click to view details on GitHub.", + }, + ) + else: + self.log.info(f"Update v{latest_tag} already notified") + else: + self._update_available = False + self.log.info(f"BasicSwap is up to date (v{current_version})") + except ValueError as e: + self.log.warning(f"Error comparing versions: {e}") + + except Exception as e: + self.log.warning(f"Failed to check for updates: {e}") + def isBaseCoinActive(self, c) -> bool: if c not in chainparams: return False @@ -10798,6 +10864,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.checkDelayedAutoAccept() self._last_checked_delayed_auto_accept = now + if now - self._last_checked_updates >= self.check_updates_seconds: + self.checkForUpdates() + except Exception as ex: self.logException(f"update {ex}") diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 43bddef..a4a065f 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -244,6 +244,7 @@ class NotificationTypes(IntEnum): BID_RECEIVED = auto() BID_ACCEPTED = auto() SWAP_COMPLETED = auto() + UPDATE_AVAILABLE = auto() class ConnectionRequestTypes(IntEnum): diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 433c274..4b37d6f 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -842,9 +842,19 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes: if not swap_client.debug: raise ValueError("Debug mode not active.") - r = random.randint(0, 3) + r = random.randint(0, 4) if r == 0: - swap_client.notify(NT.OFFER_RECEIVED, {"offer_id": random.randbytes(28).hex()}) + swap_client.notify( + NT.OFFER_RECEIVED, + { + "offer_id": random.randbytes(28).hex(), + "coin_from": 2, + "coin_to": 6, + "amount_from": 100000000, + "amount_to": 15500000000000, + "rate": 15500000000000, + }, + ) elif r == 1: swap_client.notify( NT.BID_RECEIVED, @@ -852,6 +862,13 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes: "type": "atomic", "bid_id": random.randbytes(28).hex(), "offer_id": random.randbytes(28).hex(), + "coin_from": 2, + "coin_to": 6, + "amount_from": 100000000, + "amount_to": 15500000000000, + "bid_amount": 50000000, + "bid_amount_to": 7750000000000, + "rate": 15500000000000, }, ) elif r == 2: @@ -863,12 +880,71 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes: "type": "ads", "bid_id": random.randbytes(28).hex(), "offer_id": random.randbytes(28).hex(), + "coin_from": 1, + "coin_to": 3, + "amount_from": 500000000, + "amount_to": 100000000, + "bid_amount": 250000000, + "bid_amount_to": 50000000, + "rate": 20000000, }, ) + elif r == 4: + swap_client.notify(NT.SWAP_COMPLETED, {"bid_id": random.randbytes(28).hex()}) return bytes(json.dumps({"type": r}), "UTF-8") +def js_checkupdates(self, url_split, post_string, is_json) -> bytes: + swap_client = self.server.swap_client + from basicswap import __version__ + + if not swap_client.settings.get("check_updates", True): + return bytes( + json.dumps({"error": "Update checking is disabled in settings"}), "UTF-8" + ) + + import time + + now = time.time() + last_manual_check = getattr(swap_client, "_last_manual_update_check", 0) + + if not swap_client.debug and (now - last_manual_check) < 3600: + remaining = int(3600 - (now - last_manual_check)) + return bytes( + json.dumps( + { + "error": f"Please wait {remaining // 60} minutes before checking again" + } + ), + "UTF-8", + ) + + swap_client._last_manual_update_check = now + swap_client.log.info("Manual update check requested via web interface") + + swap_client.checkForUpdates() + + if swap_client._update_available: + swap_client.log.info( + f"Manual check result: Update available v{swap_client._latest_version} (current: v{__version__})" + ) + else: + swap_client.log.info(f"Manual check result: Up to date (v{__version__})") + + return bytes( + json.dumps( + { + "message": "Update check completed", + "current_version": __version__, + "latest_version": swap_client._latest_version, + "update_available": swap_client._update_available, + } + ), + "UTF-8", + ) + + def js_notifications(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() @@ -1377,6 +1453,7 @@ endpoints = { "rates": js_rates, "rateslist": js_rates_list, "generatenotification": js_generatenotification, + "checkupdates": js_checkupdates, "notifications": js_notifications, "identities": js_identities, "automationstrategies": js_automationstrategies, diff --git a/basicswap/static/js/modules/notification-manager.js b/basicswap/static/js/modules/notification-manager.js index 97d6d0c..751faa1 100644 --- a/basicswap/static/js/modules/notification-manager.js +++ b/basicswap/static/js/modules/notification-manager.js @@ -8,6 +8,7 @@ const NotificationManager = (function() { showBalanceChanges: true, showOutgoingTransactions: true, showSwapCompleted: true, + showUpdateNotifications: true, notificationDuration: 20000 }; @@ -67,6 +68,7 @@ const NotificationManager = (function() { coinSymbol: options.coinSymbol || '', coinFrom: options.coinFrom || null, coinTo: options.coinTo || null, + releaseUrl: options.releaseUrl || null, timestamp: new Date().toLocaleString(), timestampMs: Date.now() }; @@ -183,6 +185,10 @@ const NotificationManager = (function() { return `window.location.href='/bids'`; } + if (item.type === 'update_available' && item.releaseUrl) { + return `window.open('${item.releaseUrl}', '_blank')`; + } + if (item.title.includes('offer') || item.title.includes('Offer')) { return `window.location.href='/offers'`; } @@ -231,6 +237,9 @@ function ensureToastContainer() { 'balance_change': ` `, + 'update_available': ` + + `, 'success': ` ` @@ -245,6 +254,7 @@ function ensureToastContainer() { 'bid_accepted': 'bg-purple-500', 'swap_completed': 'bg-green-600', 'balance_change': 'bg-yellow-500', + 'update_available': 'bg-blue-600', 'success': 'bg-blue-500' }; @@ -425,6 +435,18 @@ function ensureToastContainer() { ); }, 4000); + setTimeout(() => { + this.createToast( + 'Update Available: v0.15.0', + 'update_available', + { + subtitle: 'Current: v0.14.6 • Click to view release', + releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0', + releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.' + } + ); + }, 4500); + }, initializeBalanceTracking: function() { @@ -466,7 +488,6 @@ function ensureToastContainer() { const staleThreshold = 10 * 60 * 1000; if (!lastFetch || (now - parseInt(lastFetch)) > staleThreshold) { - console.log('Resetting stale balance tracking to prevent false notifications'); this.resetBalanceTracking(); } }, @@ -504,6 +525,8 @@ function ensureToastContainer() { const iconColor = getToastColor(type, options); const icon = getToastIcon(type); + const isPersistent = type === 'update_available'; + let coinIconHtml = ''; if (options.coinSymbol) { const coinIcon = getCoinIcon(options.coinSymbol); @@ -523,6 +546,9 @@ function ensureToastContainer() { } else if (options.coinSymbol) { clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`; cursorStyle = 'cursor-pointer'; + } else if (options.releaseUrl) { + clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`; + cursorStyle = 'cursor-pointer'; } message.innerHTML = ` @@ -558,17 +584,19 @@ function ensureToastContainer() { `; messages.appendChild(message); - setTimeout(() => { - if (message.parentNode) { - message.classList.add('toast-slide-out'); - setTimeout(() => { - if (message.parentNode) { - message.parentNode.removeChild(message); - } + if (!isPersistent) { + setTimeout(() => { + if (message.parentNode) { + message.classList.add('toast-slide-out'); + setTimeout(() => { + if (message.parentNode) { + message.parentNode.removeChild(message); + } - }, 300); - } - }, config.notificationDuration); + }, 300); + } + }, config.notificationDuration); + } }, handleWebSocketEvent: function(data) { @@ -633,6 +661,15 @@ function ensureToastContainer() { shouldShowToast = config.showSwapCompleted; break; + case 'update_available': + toastTitle = `Update Available: v${data.latest_version}`; + toastOptions.subtitle = `Current: v${data.current_version} • Click to view release`; + toastOptions.releaseUrl = data.release_url; + toastOptions.releaseNotes = data.release_notes; + toastType = 'update_available'; + shouldShowToast = config.showUpdateNotifications; + break; + case 'coin_balance_updated': if (data.coin && config.showBalanceChanges) { this.handleBalanceUpdate(data); diff --git a/basicswap/templates/settings.html b/basicswap/templates/settings.html index 31de22f..8d25a7d 100644 --- a/basicswap/templates/settings.html +++ b/basicswap/templates/settings.html @@ -503,6 +503,17 @@

Show notifications when swaps complete successfully

+
+
+ + + +
+

Check for BasicSwap updates and show notifications when available

+
+ @@ -537,10 +548,16 @@

Test Notifications

-
+
+ +
@@ -570,6 +587,7 @@ showBalanceChanges: document.getElementById('notifications_balance_changes').checked, showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions').checked, showSwapCompleted: document.getElementById('notifications_swap_completed').checked, + showUpdateNotifications: document.getElementById('check_updates').checked, notificationDuration: parseInt(document.getElementById('notifications_duration').value) * 1000 }; @@ -581,6 +599,141 @@ setTimeout(syncNotificationSettings, 100); }); + function testUpdateNotification() { + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Available: v0.15.0', + 'update_available', + { + subtitle: 'Current: v{{ version }} • Click to view release', + releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0', + releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.' + } + ); + } + } + + function testLiveUpdateCheck() { + const button = event.target; + const originalText = button.textContent; + button.textContent = 'Checking...'; + button.disabled = true; + + fetch('/json/checkupdates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (window.NotificationManager) { + if (data.update_available) { + window.NotificationManager.createToast( + `Live Update Available: v${data.latest_version}`, + 'update_available', + { + subtitle: `Current: v${data.current_version} • Click to view release`, + releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${data.latest_version}`, + releaseNotes: 'This is a real update check from GitHub API.' + } + ); + } else { + window.NotificationManager.createToast( + 'No Updates Available', + 'success', + { + subtitle: `Current version v${data.current_version} is up to date` + } + ); + } + } + }) + .catch(error => { + console.error('Update check failed:', error); + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: 'Could not check for updates. See console for details.' + } + ); + } + }) + .finally(() => { + button.textContent = originalText; + button.disabled = false; + }); + } + + function checkForUpdatesNow() { + const button = event.target; + const originalText = button.textContent; + button.textContent = 'Checking...'; + button.disabled = true; + + fetch('/json/checkupdates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: data.error + } + ); + } + return; + } + + if (window.NotificationManager) { + if (data.update_available) { + window.NotificationManager.createToast( + `Update Available: v${data.latest_version}`, + 'update_available', + { + subtitle: `Current: v${data.current_version} • Click to view release`, + releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${data.latest_version}`, + releaseNotes: `New version v${data.latest_version} is available. Click to view details on GitHub.` + } + ); + } else { + window.NotificationManager.createToast( + 'You\'re Up to Date!', + 'success', + { + subtitle: `Current version v${data.current_version} is the latest` + } + ); + } + } + }) + .catch(error => { + console.error('Update check failed:', error); + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: 'Network error. Please try again later.' + } + ); + } + }) + .finally(() => { + button.textContent = originalText; + button.disabled = false; + }); + } + document.addEventListener('DOMContentLoaded', function() { syncNotificationSettings(); }); diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index 021c32b..d72fbc2 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -665,7 +665,7 @@ function fillDonationAddress(address, type) { {% endif %} - {% if donation_info %} + {% if debug_ui and donation_info %}
diff --git a/basicswap/ui/page_settings.py b/basicswap/ui/page_settings.py index 5d61458..221a9e9 100644 --- a/basicswap/ui/page_settings.py +++ b/basicswap/ui/page_settings.py @@ -94,6 +94,9 @@ def page_settings(self, url_split, post_string): "notifications_duration": int( get_data_entry_or(form_data, "notifications_duration", "20") ), + "check_updates": toBool( + get_data_entry_or(form_data, "check_updates", "true") + ), } swap_client.editGeneralSettings(data) messages.append("Notification settings applied.") @@ -207,6 +210,7 @@ def page_settings(self, url_split, post_string): "debug": swap_client.debug, "debug_ui": swap_client.debug_ui, "expire_db_records": swap_client._expire_db_records, + "check_updates": swap_client.settings.get("check_updates", True), } chart_api_key = get_api_key_setting( diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 7062f6d..04aa9ef 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -410,5 +410,6 @@ def page_wallet(self, url_split, post_string): "summary": summary, "block_unknown_seeds": swap_client._restrict_unknown_seed_wallets, "donation_info": donation_info, + "debug_ui": swap_client.debug_ui, }, )