Merge pull request #358 from gerlofvanek/update_notification

Show notification when new release of BSX
This commit is contained in:
Gerlof van Ek
2025-08-30 21:29:58 +02:00
committed by GitHub
8 changed files with 357 additions and 15 deletions

View File

@@ -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}")

View File

@@ -244,6 +244,7 @@ class NotificationTypes(IntEnum):
BID_RECEIVED = auto()
BID_ACCEPTED = auto()
SWAP_COMPLETED = auto()
UPDATE_AVAILABLE = auto()
class ConnectionRequestTypes(IntEnum):

View File

@@ -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,

View File

@@ -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': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>`,
'update_available': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"></path>
</svg>`,
'success': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>`
@@ -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);

View File

@@ -503,6 +503,17 @@
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications when swaps complete successfully</p>
</div>
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="check_updates" name="check_updates" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if general_settings.check_updates %} checked{% endif %}>
<label for="check_updates" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Update Notifications</label>
<button type="button" onclick="checkForUpdatesNow()" class="ml-3 text-xs bg-gray-600 hover:bg-gray-700 text-white font-medium py-1 px-3 rounded transition-colors focus:outline-none">
Check Now
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Check for BasicSwap updates and show notifications when available</p>
</div>
</div>
@@ -537,10 +548,16 @@
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Test Notifications</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div>
<div class="space-y-3">
<button type="button" onclick="window.NotificationManager && window.NotificationManager.testToasts()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test All Notification Types
</button>
<button type="button" onclick="testUpdateNotification()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test Update Notification
</button>
<button type="button" onclick="testLiveUpdateCheck()" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test Live Update Check
</button>
</div>
</div>
</div>
@@ -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();
});

View File

@@ -665,7 +665,7 @@ function fillDonationAddress(address, type) {
</td>
</tr>
{% endif %}
{% if donation_info %}
{% if debug_ui and donation_info %}
<tr>
<td colspan="2" class="py-3 px-6">
<div class="p-4 bg-coolGray-100 dark:bg-gray-500 border border-coolGray-200 dark:border-gray-400 rounded-lg">

View File

@@ -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(

View File

@@ -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,
},
)