2026-09-23 20:52:16 -04:00
|
|
|
|
/** Which step of the desktop update failed (the native side prefixes its errors). */
|
|
|
|
|
|
export type UpdatePhase = 'check' | 'download' | 'install';
|
|
|
|
|
|
|
|
|
|
|
|
/** Where to get the installer by hand when the in-app update keeps failing. */
|
|
|
|
|
|
export const MANUAL_DOWNLOAD_URL = {
|
|
|
|
|
|
windows:
|
|
|
|
|
|
'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest/LotusChat-x86_64-setup.exe',
|
|
|
|
|
|
other: 'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/tag/latest',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const manualDownloadUrl = (userAgent: string): string =>
|
|
|
|
|
|
/Windows/i.test(userAgent) ? MANUAL_DOWNLOAD_URL.windows : MANUAL_DOWNLOAD_URL.other;
|
|
|
|
|
|
|
|
|
|
|
|
const NETWORK_PATTERN =
|
|
|
|
|
|
/error sending request|timed out|timeout|connection|connect|dns|network|reset|refused|unreachable|Could not fetch a valid release/i;
|
|
|
|
|
|
|
|
|
|
|
|
export const isNetworkUpdateError = (message: string): boolean => NETWORK_PATTERN.test(message);
|
|
|
|
|
|
|
|
|
|
|
|
/** Split `download: error sending request …` into its phase and the raw message. */
|
|
|
|
|
|
export const parseUpdateError = (
|
|
|
|
|
|
raw: string,
|
|
|
|
|
|
fallback: UpdatePhase,
|
|
|
|
|
|
): { phase: UpdatePhase; message: string } => {
|
|
|
|
|
|
const match = /^(check|download|install): ([\s\S]*)$/.exec(raw);
|
|
|
|
|
|
return match
|
|
|
|
|
|
? { phase: match[1] as UpdatePhase, message: match[2] }
|
|
|
|
|
|
: { phase: fallback, message: raw };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A plain-language sentence for an update failure. The raw error ("error
|
|
|
|
|
|
* sending request for url (…nsis.zip)") told users nothing about what to do.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const describeUpdateError = (phase: UpdatePhase, message: string): string => {
|
|
|
|
|
|
const network = isNetworkUpdateError(message);
|
|
|
|
|
|
if (phase === 'check') {
|
|
|
|
|
|
return network
|
|
|
|
|
|
? 'Couldn’t reach the update server. It may be busy, or your connection dropped. Lotus Chat still works; try again in a minute.'
|
|
|
|
|
|
: 'Couldn’t check for updates. Lotus Chat still works; try again later.';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (phase === 'download') {
|
|
|
|
|
|
return network
|
|
|
|
|
|
? 'The update didn’t finish downloading after several tries. The update server may be busy. Try again in a minute, or download the installer yourself.'
|
|
|
|
|
|
: /signature|minisign|base64/i.test(message)
|
|
|
|
|
|
? 'The downloaded update didn’t pass its safety check, so it wasn’t installed. Try again, or download the installer yourself.'
|
|
|
|
|
|
: 'The update couldn’t be downloaded. Try again, or download the installer yourself.';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'The update downloaded but couldn’t be installed. Download the installer yourself and run it; your chats and settings are kept.';
|
|
|
|
|
|
};
|
2026-09-30 19:00:10 -04:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* How this desktop install gets updated (cinny-desktop `update_install_kind`).
|
|
|
|
|
|
* Linux package installs can't be replaced in place by the in-app updater
|
|
|
|
|
|
* (it tried to write into /usr/bin: "Permission denied (os error 13)"), so
|
|
|
|
|
|
* they get their package manager's command instead of an Install button.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type InstallKind = 'in-app' | 'pacman' | 'deb' | 'manual';
|
|
|
|
|
|
|
|
|
|
|
|
const RELEASE_DOWNLOAD =
|
|
|
|
|
|
'https://code.lotusguild.org/LotusGuild/cinny-desktop/releases/download/latest';
|
|
|
|
|
|
|
|
|
|
|
|
export const toInstallKind = (value: unknown): InstallKind =>
|
|
|
|
|
|
value === 'pacman' || value === 'deb' || value === 'manual' ? value : 'in-app';
|
|
|
|
|
|
|
|
|
|
|
|
export type PackageUpdateHelp = { command?: string; url: string; download: string };
|
|
|
|
|
|
|
2026-10-19 00:37:34 -04:00
|
|
|
|
/** The desktop updater's minisign public key (cinny-desktop tauri.conf.json). */
|
|
|
|
|
|
export const RELEASE_MINISIGN_KEY = 'RWRZZdyii05/NUcz337A5SAbiZK+NPVDWuiL2mM5Jk1p/LfRmNfiZ/6l';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* [cinny-desktop #36] Download a Linux package and install it only after
|
|
|
|
|
|
* checking the release's minisign-signed SHA256SUMS.txt. Runs in a fresh temp
|
|
|
|
|
|
* directory so a stale SHA256SUMS.txt can't be picked up.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const verifiedInstall = (file: string, install: string): string =>
|
|
|
|
|
|
[
|
|
|
|
|
|
`cd "$(mktemp -d)"`,
|
|
|
|
|
|
`curl -fLO ${RELEASE_DOWNLOAD}/${file} -fLO ${RELEASE_DOWNLOAD}/SHA256SUMS.txt -fLO ${RELEASE_DOWNLOAD}/SHA256SUMS.txt.minisig`,
|
|
|
|
|
|
`minisign -Vm SHA256SUMS.txt -P ${RELEASE_MINISIGN_KEY}`,
|
|
|
|
|
|
`sha256sum -c --ignore-missing SHA256SUMS.txt`,
|
|
|
|
|
|
`${install} ./${file}`,
|
|
|
|
|
|
].join(' && ');
|
|
|
|
|
|
|
2026-09-30 19:00:10 -04:00
|
|
|
|
export const packageUpdateHelp = (kind: InstallKind): PackageUpdateHelp | undefined => {
|
|
|
|
|
|
if (kind === 'pacman') {
|
2026-10-19 00:37:34 -04:00
|
|
|
|
// Install the local file: `pacman -U <url>` also fetches `<url>.sig` and
|
|
|
|
|
|
// fails without it (packages aren't signed for pacman), while a local file
|
|
|
|
|
|
// falls under LocalFileSigLevel. Integrity comes from SHA256SUMS.txt.
|
2026-09-30 19:57:12 -04:00
|
|
|
|
return {
|
2026-10-19 00:37:34 -04:00
|
|
|
|
command: verifiedInstall('LotusChat-x86_64.pkg.tar.zst', 'sudo pacman -U'),
|
|
|
|
|
|
url: `${RELEASE_DOWNLOAD}/LotusChat-x86_64.pkg.tar.zst`,
|
2026-09-30 19:57:12 -04:00
|
|
|
|
download: 'Download package',
|
|
|
|
|
|
};
|
2026-09-30 19:00:10 -04:00
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'deb') {
|
|
|
|
|
|
return {
|
2026-10-19 00:37:34 -04:00
|
|
|
|
command: verifiedInstall('LotusChat-x86_64.deb', 'sudo apt install'),
|
|
|
|
|
|
url: `${RELEASE_DOWNLOAD}/LotusChat-x86_64.deb`,
|
2026-09-30 19:00:10 -04:00
|
|
|
|
download: 'Download package',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'manual') return { url: MANUAL_DOWNLOAD_URL.other, download: 'Open downloads' };
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** The native side refuses an in-app install on a package install. */
|
|
|
|
|
|
export const isPackageManagedError = (message: string): boolean => /package-managed/.test(message);
|