mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 23:00:09 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -338,24 +338,24 @@ struct AddChannelView: View {
|
||||
.compactSectionSpacing()
|
||||
|
||||
Section {
|
||||
Button("Channel link") {
|
||||
Button("Continue") {
|
||||
if activeCount >= total {
|
||||
showLinkStep = true
|
||||
} else if activeCount > 0 {
|
||||
let actions: [UIAlertAction] = if activeCount + failedCount < total {
|
||||
[
|
||||
UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true },
|
||||
UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true },
|
||||
UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in }
|
||||
]
|
||||
} else {
|
||||
[
|
||||
UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true },
|
||||
UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true },
|
||||
cancelAlertAction
|
||||
]
|
||||
}
|
||||
showAlert(
|
||||
NSLocalizedString("Not all relays connected", comment: "alert title"),
|
||||
message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Proceed?", comment: "alert message"), activeCount, total),
|
||||
message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Continue?", comment: "alert message"), activeCount, total),
|
||||
actions: { actions }
|
||||
)
|
||||
}
|
||||
@@ -367,7 +367,12 @@ struct AddChannelView: View {
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Cancel") { cancelChannelCreation(gInfo) }
|
||||
Button("Delete channel") { showCancelChannelAlert(gInfo) }
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if !showLinkStep && m.creatingChannelId == gInfo.id {
|
||||
showCancelChannelAlert(gInfo)
|
||||
}
|
||||
}
|
||||
.onChange(of: channelRelaysModel.groupRelays) { relays in
|
||||
@@ -429,6 +434,24 @@ struct AddChannelView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showCancelChannelAlert(_ gInfo: GroupInfo) {
|
||||
let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count
|
||||
let total = groupRelays.count
|
||||
showAlert(
|
||||
NSLocalizedString("Cancel creating channel?", comment: "alert title"),
|
||||
message: String.localizedStringWithFormat(
|
||||
NSLocalizedString("Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again.", comment: "alert message"),
|
||||
gInfo.groupProfile.displayName, activeCount, total
|
||||
),
|
||||
actions: {[
|
||||
UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in },
|
||||
UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .destructive) { _ in
|
||||
cancelChannelCreation(gInfo)
|
||||
}
|
||||
]}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func showInvalidChannelNameAlert() {
|
||||
|
||||
+20
-13
@@ -364,16 +364,23 @@ private fun ProgressStepView(
|
||||
val activeCount = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }
|
||||
val total = groupRelays.value.size
|
||||
|
||||
fun showCancelAlert() {
|
||||
val active = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null }
|
||||
val tot = groupRelays.value.size
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.cancel_creating_channel_question),
|
||||
text = String.format(generalGetString(MR.strings.cancel_channel_alert_msg), gInfo.groupProfile.displayName, active, tot),
|
||||
confirmText = generalGetString(MR.strings.cancel_verb),
|
||||
onConfirm = cancelChannelCreation,
|
||||
dismissText = generalGetString(MR.strings.wait_verb),
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
DisposableEffect(Unit) {
|
||||
chatModel.centerPanelBackgroundClickHandler = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.cancel_creating_channel_question),
|
||||
confirmText = generalGetString(MR.strings.cancel_creating_channel_confirm),
|
||||
onConfirm = cancelChannelCreation,
|
||||
dismissText = generalGetString(MR.strings.wait_verb),
|
||||
destructive = true,
|
||||
)
|
||||
showCancelAlert()
|
||||
true
|
||||
}
|
||||
onDispose {
|
||||
@@ -395,11 +402,11 @@ private fun ProgressStepView(
|
||||
}
|
||||
|
||||
ModalView(
|
||||
close = cancelChannelCreation,
|
||||
close = { showCancelAlert() },
|
||||
showClose = false,
|
||||
endButtons = {
|
||||
TextButton(onClick = cancelChannelCreation) {
|
||||
Text(generalGetString(MR.strings.cancel_verb))
|
||||
TextButton(onClick = { showCancelAlert() }) {
|
||||
Text(generalGetString(MR.strings.button_delete_channel))
|
||||
}
|
||||
}
|
||||
) {
|
||||
@@ -477,7 +484,7 @@ private fun ProgressStepView(
|
||||
val enabled = activeCount > 0
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_link),
|
||||
generalGetString(MR.strings.channel_link),
|
||||
generalGetString(MR.strings.continue_to_next_step),
|
||||
click = {
|
||||
if (activeCount >= total) {
|
||||
onLinkReady()
|
||||
@@ -499,7 +506,7 @@ private fun ProgressStepView(
|
||||
AlertManager.shared.hideAlert()
|
||||
onLinkReady()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.proceed_verb))
|
||||
Text(generalGetString(MR.strings.continue_to_next_step))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,7 +515,7 @@ private fun ProgressStepView(
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.not_all_relays_connected),
|
||||
text = alertText,
|
||||
confirmText = generalGetString(MR.strings.proceed_verb),
|
||||
confirmText = generalGetString(MR.strings.continue_to_next_step),
|
||||
onConfirm = { onLinkReady() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2542,7 +2542,6 @@
|
||||
<string name="relay_status_accepted">وافقت</string>
|
||||
<string name="relay_status_active">نشط</string>
|
||||
<string name="block_subscriber_for_all_question">احظر المشترك للكل؟</string>
|
||||
<string name="cancel_creating_channel_confirm">ألغِ</string>
|
||||
<string name="chat_relays">مُرحلات الدردشة</string>
|
||||
<string name="channel_relays_title">مُرحلات الدردشة</string>
|
||||
<string name="button_delete_channel">احذف القناة</string>
|
||||
@@ -2613,7 +2612,6 @@
|
||||
<string name="channel_members_section_owners">المالكون</string>
|
||||
<string name="preset_relay_address">عنوان المُرحل مسبق الضبط</string>
|
||||
<string name="preset_relay_name">اسم المُرحل مسبق الضبط</string>
|
||||
<string name="proceed_verb">تابِع</string>
|
||||
<string name="group_member_role_relay">مُرحل</string>
|
||||
<string name="member_info_section_title_relay">مُرحل</string>
|
||||
<string name="info_row_relay_address">عنوان المُرحل</string>
|
||||
|
||||
@@ -3035,7 +3035,7 @@
|
||||
<string name="network_error">Network error</string>
|
||||
<string name="error_prefix">Error</string>
|
||||
<string name="cancel_creating_channel_question">Cancel creating channel?</string>
|
||||
<string name="cancel_creating_channel_confirm">Cancel</string>
|
||||
<string name="cancel_channel_alert_msg">Your new channel %1$s is connected to %2$d of %3$d relays.\nIf you cancel, the channel will be deleted - you can create it again.</string>
|
||||
<string name="enable_at_least_one_chat_relay">Enable at least one chat relay to create a channel.</string>
|
||||
<string name="your_profile_shared_with_channel_relays">Your profile %1$s will be shared with channel relays and subscribers.\nRelays can access channel messages.</string>
|
||||
<string name="configure_relays">Configure relays</string>
|
||||
@@ -3043,8 +3043,7 @@
|
||||
<string name="relay_connection_failed">Relay connection failed</string>
|
||||
<string name="not_all_relays_connected">Not all relays connected</string>
|
||||
<string name="wait_verb">Wait</string>
|
||||
<string name="proceed_verb">Proceed</string>
|
||||
<string name="channel_will_start_with_relays">Channel will start working with %1$d of %2$d relays. Proceed?</string>
|
||||
<string name="channel_will_start_with_relays">Channel will start working with %1$d of %2$d relays. Continue?</string>
|
||||
|
||||
<!-- ConnectPlan.kt channel-related -->
|
||||
<string name="relay_address_alert_title">Relay address</string>
|
||||
|
||||
@@ -2547,7 +2547,6 @@
|
||||
<string name="member_info_member_failed">selhal</string>
|
||||
<string name="down_migration_warning_chat_relays">Pokud jste se připojili k nějakým kanálům nebo je vytvořili, přestanou trvale fungovat.</string>
|
||||
<string name="relay_status_active">aktivní</string>
|
||||
<string name="cancel_creating_channel_confirm">Zrušit</string>
|
||||
<string name="why_built_heading">Narodili jste se bez účtu.</string>
|
||||
<string name="why_built_p1">Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života.</string>
|
||||
<string name="why_built_p2">Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět - vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla - telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob.</string>
|
||||
|
||||
@@ -2639,7 +2639,6 @@
|
||||
<string name="relay_status_active">Aktiv</string>
|
||||
<string name="block_subscriber_for_all_question">Abonnent für alle blockieren?</string>
|
||||
<string name="compose_view_broadcast">Broadcast</string>
|
||||
<string name="cancel_creating_channel_confirm">Abbrechen</string>
|
||||
<string name="cancel_creating_channel_question">Kanalerstellung abbrechen?</string>
|
||||
<string name="test_relay_to_retrieve_name"><![CDATA[<b>Relais testen</b>, um dessen Namen abzurufen.]]></string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Dies ist Ihr Link für den Kanal <b>%1$s</b>!]]></string>
|
||||
@@ -2700,7 +2699,6 @@
|
||||
<string name="channel_members_section_owners">Eigentümer</string>
|
||||
<string name="preset_relay_address">Voreingestellte Relais-Adresse</string>
|
||||
<string name="preset_relay_name">Voreingestellter Relais-Name</string>
|
||||
<string name="proceed_verb">Fortfahren</string>
|
||||
<string name="group_member_role_relay">Relais</string>
|
||||
<string name="member_info_section_title_relay">RELAIS</string>
|
||||
<string name="info_row_relay_address">Relais-Adresse</string>
|
||||
|
||||
@@ -2585,7 +2585,6 @@
|
||||
<string name="channel_subscriber_count_plural">%1$d suscriptores</string>
|
||||
<string name="block_subscriber_for_all_question">¿Bloquear al suscriptor para todos?</string>
|
||||
<string name="compose_view_broadcast">Retransmisión</string>
|
||||
<string name="cancel_creating_channel_confirm">Cancelar</string>
|
||||
<string name="cancel_creating_channel_question">¿Cancelar la creación del canal?</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[¡Este es tu enlace para el canal <b>%1$s</b>!]]></string>
|
||||
<string name="channel_role_label">canal</string>
|
||||
@@ -2636,7 +2635,6 @@
|
||||
<string name="channel_members_section_owners">Propietarios</string>
|
||||
<string name="preset_relay_address">Direcciones predefinidas</string>
|
||||
<string name="preset_relay_name">Nombres predefinidos</string>
|
||||
<string name="proceed_verb">Continuar</string>
|
||||
<string name="group_member_role_relay">servidor</string>
|
||||
<string name="member_info_section_title_relay">SERVIDOR</string>
|
||||
<string name="info_row_relay_address">Dirección servidor</string>
|
||||
|
||||
@@ -2368,7 +2368,6 @@
|
||||
<string name="allow_files_and_media_only_if">Permettre des fichiers et des médias seulement si votre contact les permet.</string>
|
||||
<string name="allow_your_contacts_to_send_files_and_media">Permettre à vos contacts d\'envoyer des fichiers et des médias.</string>
|
||||
<string name="network_smp_web_port_all">Tous les serveurs</string>
|
||||
<string name="cancel_creating_channel_confirm">Annuler</string>
|
||||
<string name="compose_view_send_contact_request_alert_text"><![CDATA[Vous allez pouvoir envoyer des messages <b>seulement après que votre requête soit acceptée</b>.]]></string>
|
||||
<string name="check_relay_address">Vérifiez l\'adresse de relais et essayez à nouveau.</string>
|
||||
<string name="check_relay_name">Vérifiez le nom du relais et essayez à nouveau.</string>
|
||||
@@ -2405,7 +2404,6 @@
|
||||
<string name="preset_relay_name">Nom de relais prédéfini</string>
|
||||
<string name="network_smp_web_port_preset">Serveurs prédéfinis</string>
|
||||
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Politique de confidentialité et conditions d\'utilisation.</string>
|
||||
<string name="proceed_verb">Continuer</string>
|
||||
<string name="reject_pending_member_button">Rejeter</string>
|
||||
<string name="reject_contact_request">Rejeter la demande de contact</string>
|
||||
<string name="group_preview_rejected">rejeté</string>
|
||||
|
||||
@@ -2524,7 +2524,6 @@
|
||||
<string name="down_migration_warning_chat_relays">Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni.</string>
|
||||
<string name="relay_status_active">aktív</string>
|
||||
<string name="compose_view_broadcast">Közvetítés…</string>
|
||||
<string name="cancel_creating_channel_confirm">Mégse</string>
|
||||
<string name="channel_role_label">csatorna</string>
|
||||
<string name="chat_banner_channel">Csatorna</string>
|
||||
<string name="info_row_channel">Csatorna</string>
|
||||
@@ -2624,7 +2623,6 @@
|
||||
<string name="configure_relays">Átjátszók konfigurálása</string>
|
||||
<string name="relay_connection_failed">Nem sikerült kapcsolódni az átjátszóhoz</string>
|
||||
<string name="not_all_relays_connected">Nem minden átjátszó kapcsolódott</string>
|
||||
<string name="proceed_verb">Folytatás</string>
|
||||
<string name="channel_will_start_with_relays">A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja?</string>
|
||||
<string name="relay_address_alert_title">Átjátszó címe</string>
|
||||
<string name="relay_address_alert_message">Ez egy csevegési átjátszó címe, nem használható kapcsolódásra.</string>
|
||||
|
||||
@@ -2567,7 +2567,6 @@
|
||||
<string name="relay_status_accepted">accettato</string>
|
||||
<string name="relay_status_active">attivo</string>
|
||||
<string name="block_subscriber_for_all_question">Bloccare l\'iscritto per tutti?</string>
|
||||
<string name="cancel_creating_channel_confirm">Annulla</string>
|
||||
<string name="cancel_creating_channel_question">Annullare la creazione del canale?</string>
|
||||
<string name="test_relay_to_retrieve_name"><![CDATA[<b>Prova il relay</b> per recuperare il suo nome.]]></string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Questo è il tuo link per il canale <b>%1$s</b>!]]></string>
|
||||
@@ -2625,7 +2624,6 @@
|
||||
<string name="channel_members_section_owners">Proprietari</string>
|
||||
<string name="preset_relay_address">Indirizzo relay preimpostato</string>
|
||||
<string name="preset_relay_name">Nome relay preimpostato</string>
|
||||
<string name="proceed_verb">Procedi</string>
|
||||
<string name="group_member_role_relay">relay</string>
|
||||
<string name="member_info_section_title_relay">RELAY</string>
|
||||
<string name="info_row_relay_address">Indirizzo del relay</string>
|
||||
|
||||
@@ -2675,7 +2675,6 @@
|
||||
<string name="relay_status_active">активный</string>
|
||||
<string name="block_subscriber_for_all_question">Заблокировать подписчика для всех?</string>
|
||||
<string name="compose_view_broadcast">Опубликовать</string>
|
||||
<string name="cancel_creating_channel_confirm">Отменить</string>
|
||||
<string name="channel_will_start_with_relays">Канал начнёт работу с %1$d из %2$d релеев. Продолжить?</string>
|
||||
<string name="chat_relay">Чат-релей</string>
|
||||
<string name="button_channel_relays">Чат-релеи</string>
|
||||
@@ -2721,7 +2720,6 @@
|
||||
<string name="connect_plan_open_new_channel">Открыть новый канал</string>
|
||||
<string name="channel_members_section_owners">Владельцы</string>
|
||||
<string name="member_info_section_title_owner">ВЛАДЕЛЕЦ</string>
|
||||
<string name="proceed_verb">Продолжить</string>
|
||||
<string name="group_member_role_relay">релей</string>
|
||||
<string name="member_info_section_title_relay">РЕЛЕЙ</string>
|
||||
<string name="info_row_relay_address">Адрес релея</string>
|
||||
|
||||
@@ -2553,7 +2553,6 @@
|
||||
<string name="relay_status_active">活跃</string>
|
||||
<string name="block_subscriber_for_all_question">为所有人拦截订阅者?</string>
|
||||
<string name="compose_view_broadcast">广播</string>
|
||||
<string name="cancel_creating_channel_confirm">取消</string>
|
||||
<string name="cancel_creating_channel_question">取消创建频道?</string>
|
||||
<string name="test_relay_to_retrieve_name"><![CDATA[<b>测试中继</b> 来获取其名称。]]></string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[这是 <b>%1$s</b> 频道的链接!]]></string>
|
||||
@@ -2614,7 +2613,6 @@
|
||||
<string name="channel_members_section_owners">所有者</string>
|
||||
<string name="preset_relay_address">预设中继地址</string>
|
||||
<string name="preset_relay_name">预设中继名</string>
|
||||
<string name="proceed_verb">继续</string>
|
||||
<string name="group_member_role_relay">中继</string>
|
||||
<string name="member_info_section_title_relay">中继</string>
|
||||
<string name="info_row_relay_address">中继地址</string>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
data/
|
||||
.env
|
||||
+4
-43
@@ -12,7 +12,7 @@
|
||||
"@simplex-chat/types": "^0.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^14.0.3",
|
||||
"simplex-chat": "^6.5.0-beta.10"
|
||||
"simplex-chat": "^6.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
@@ -523,9 +523,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -540,9 +537,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -557,9 +551,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -574,9 +565,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -591,9 +579,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -608,9 +593,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -625,9 +607,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -642,9 +621,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -659,9 +635,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -676,9 +649,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -693,9 +663,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -710,9 +677,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -727,9 +691,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1714,9 +1675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/simplex-chat": {
|
||||
"version": "6.5.0-beta.10",
|
||||
"resolved": "https://registry.npmjs.org/simplex-chat/-/simplex-chat-6.5.0-beta.10.tgz",
|
||||
"integrity": "sha512-K5yt4zAA04Ds0XvrSLhuknXx1rmCM8ByjgjJ0iHcQmPU5us9aaiIuez9HPP8FG1a+9xj8XMcgJfi0W1f2fTCdQ==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/simplex-chat/-/simplex-chat-6.5.0.tgz",
|
||||
"integrity": "sha512-QFGI734HhYJ7trSrEKiZ2mbodI0V8CLDGEv2+yt5zsg0FqftxSpFik6zUSezTRZtN1M8WmSlT44qlEt2a1fXQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"@simplex-chat/types": "^0.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^14.0.3",
|
||||
"simplex-chat": "^6.5.0-beta.10"
|
||||
"simplex-chat": "^6.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -383,7 +383,7 @@ export class SupportBot {
|
||||
await this.withMainProfile(() =>
|
||||
this.chat.apiSendTextMessage(
|
||||
[T.ChatType.Direct, contactId],
|
||||
`Please use my business address to ask questions: ${this.businessAddress}`,
|
||||
`Please re-connect to this address for any questions: ${this.businessAddress}`,
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
|
||||
@@ -165,7 +165,7 @@ async function main(): Promise<void> {
|
||||
|
||||
// Step 5: List contacts, resolve Grok contact
|
||||
const contacts = await chat.apiListContacts(mainUser.userId)
|
||||
log(`Contacts: ${contacts.map(c => `${c.contactId}:${c.profile.displayName}`).join(", ") || "(none)"}`)
|
||||
log(`Contacts connected: ${contacts.length || "(none)"}`)
|
||||
|
||||
// Always restore grokContactId so the one-way gate can find and remove
|
||||
// Grok members even when Grok API is disabled.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {isWeekend} from "./util.js"
|
||||
|
||||
export const welcomeMessage = `Hello! This is a *SimpleX team* support bot - not an AI.
|
||||
Please ask any question about SimpleX Chat.`
|
||||
*Join public groups* at https://simplex.chat/directory or [via directory bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok)
|
||||
Please ask any questions about SimpleX Chat.`
|
||||
|
||||
export function queueMessage(timezone: string, grokEnabled: boolean): string {
|
||||
const hours = isWeekend(timezone) ? "48" : "24"
|
||||
@@ -14,7 +15,7 @@ If your question is about SimpleX, click /grok for an *instant Grok answer*.
|
||||
Send /team to switch back.`
|
||||
}
|
||||
|
||||
export const grokActivatedMessage = `*You are chatting with Grok* - use any language.`
|
||||
export const grokActivatedMessage = `*You are now chatting with Grok* - use any language.`
|
||||
|
||||
export function teamAddedMessage(timezone: string, grokPresent: boolean): string {
|
||||
const hours = isWeekend(timezone) ? "48" : "24"
|
||||
@@ -24,9 +25,9 @@ export function teamAddedMessage(timezone: string, grokPresent: boolean): string
|
||||
Grok will be answering your questions until then.`
|
||||
}
|
||||
|
||||
export const teamAlreadyInvitedMessage = "A team member has already been invited to this conversation and will reply when available."
|
||||
export const teamAlreadyInvitedMessage = "A team member was invited to this conversation and will reply when available."
|
||||
|
||||
export const teamLockedMessage = "You are now in team mode. A team member will reply to your message."
|
||||
export const teamLockedMessage = "Only the team will now receive your messages."
|
||||
|
||||
export function noTeamMembersMessage(grokEnabled: boolean): string {
|
||||
return grokEnabled
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
layout: layouts/article.html
|
||||
title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech"
|
||||
date: 2026-04-30
|
||||
# previewBody: blog_previews/20260421.html
|
||||
# image: images/20260421-channel.png
|
||||
# imageBottom: true
|
||||
draft: true
|
||||
permalink: "/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"
|
||||
---
|
||||
|
||||
# SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech
|
||||
|
||||
**To be published:** Apr 30, 2026
|
||||
|
||||
This is a permalink for a blog post about:
|
||||
|
||||
- SimpleX Channels - a new model for online publishing that preserves participation privacy, protecting both user and network operators. It is being released in v6.5
|
||||
- SimpleX Network Consortium - a cross-jurisdictional governance and licensing structure to ensure long term availability and sustainability of SimpleX Network.
|
||||
- Testing the water for community crowdfunding under Reg CF.
|
||||
|
||||
## SimpleX Channels - more public, more freedom, more private
|
||||
|
||||
TODO
|
||||
|
||||
## SimpleX Network Consortium - to govern SimpleX Network
|
||||
|
||||
TODO
|
||||
|
||||
## Community Crowdfunding
|
||||
|
||||
TODO
|
||||
|
||||
*Register your interest* to participate in crowdfunding here: https://simplexchat.typeform.com/crowdfunding
|
||||
|
||||
Join the channel for updates here: https://smp4.simplex.im/g#g6pdBGlLoeOwqYmbmyvRye8EBiFd2inNUzKc87Pt3y4
|
||||
|
||||
_Disclaimer: SimpleX Chat is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._
|
||||
@@ -2,37 +2,70 @@
|
||||
layout: layouts/article.html
|
||||
title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech"
|
||||
date: 2026-04-30
|
||||
# previewBody: blog_previews/20260421.html
|
||||
# image: images/20260421-channel.png
|
||||
# imageBottom: true
|
||||
draft: true
|
||||
previewBody: blog_previews/20260430.html
|
||||
image: images/20260430-home.png
|
||||
imageLight: images/20260430-home-light.png
|
||||
permalink: "/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"
|
||||
---
|
||||
|
||||
# SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech
|
||||
# SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding — to Preserve Freedom of Speech
|
||||
|
||||
**To be published:** Apr 30, 2026
|
||||
**Published:** Apr 30, 2026
|
||||
|
||||
This is a permalink for a blog post about:
|
||||
Freedom of speech needs infrastructure that protects it by design — not only the protocols and servers, but the governance and funding to support them.
|
||||
|
||||
- SimpleX Channels - a new model for online publishing that preserves participation privacy, protecting both user and network operators. It is being released in v6.5
|
||||
- SimpleX Network Consortium - a cross-jurisdictional governance and licensing structure to ensure long term availability and sustainability of SimpleX Network.
|
||||
- Testing the water for community crowdfunding under Reg CF.
|
||||
## SimpleX Channels — more public, more freedom, more private
|
||||
|
||||
## SimpleX Channels - more public, more freedom, more private
|
||||
<img src="./images/20260430-channel.png" width="264" class="float-to-right">
|
||||
|
||||
TODO
|
||||
v6.5 release[^release] brings SimpleX Channels: a new model for online publishing built for participation privacy.
|
||||
|
||||
## SimpleX Network Consortium - to govern SimpleX Network
|
||||
Channel content is visible to chat relay operators. And each channel uses multiple relays, so no single relay can block the channel[^preset].
|
||||
|
||||
TODO
|
||||
But the real identities of channel owners and subscribers are unknown to relay operators, to each other, and to the network. This is important for freedom of speech and for our ability to say the truth[^wilde].
|
||||
|
||||
This is the opposite of the usual approach: instead of trying (and failing [^public]) to hide publicly available content from operators while exposing participants, we designed the protocols to protect people. Anybody can join a public channel via its link and see what is sent, but not who sent it, and not who else is reading. This is win-win for both users and chat relays operators. Users' privacy is protected, operators can decide what content to deliver in public spaces, and anybody can run chat relays.
|
||||
|
||||
This is only possible because SimpleX network was built without user profile identifiers of any kind. You can't add participation privacy to a network that identifies its users — as you can't add privacy to a messenger built on phone numbers.
|
||||
|
||||
v6.5 is the first beta version of channels:
|
||||
- channel owners hold their own channel keys,
|
||||
- each channel uses multiple relays for reliability,
|
||||
- publishers can run their own chat relays,
|
||||
- channels can be added to our [SimpleX Directory](https://simplex.chat/directory/).
|
||||
|
||||
This release is a beginning of a very important new layer of SimpleX Network. Read more about channels in [whitepaper](https://github.com/simplex-chat/simplex-chat/blob/master/docs/protocol/channels-overview.md): their purpose, architecture, security model and planned future work.
|
||||
|
||||
## SimpleX Network Consortium — to preserve network independence
|
||||
|
||||
No single company should control protocols and network that people depend on to speak freely. If a network is run by a single company, the network has a risk that business and users interests diverge — if it happens, users lose.
|
||||
|
||||
To protect network neutrality and make sure its protocols and intellectual property are available to the users, we're launching [SimpleX Network Consortium](https://simplexnetwork.org) within a few months — the agreement between the new SimpleX Network Foundation and SimpleX Chat company that will govern protocols and licensing — perpetual, irrevocable, surviving if any party is sold or shut down. Other organizations will join.
|
||||
|
||||
We are currently forming the board for SimpleX Network Foundation — initially, [Heather Meeker](https://heathermeeker.com/about-me/), who drafted the Consortium agreement, and several other people will join. We will announce the board soon.
|
||||
|
||||
As the power over the network protocols moves away from the company, it cannot move back[^ulysses]. It is a structural guarantee — the same principle we applied to privacy.
|
||||
|
||||
## Community Crowdfunding
|
||||
|
||||
TODO
|
||||
We've seen open-source privacy-focussed projects die without funding, or worse — being captured by their sponsors. We've seen "don't be evil" companies get lured off course by growth and board pressure. Neither pure ideology nor pure commerce survives the long run alone.
|
||||
|
||||
*Register your interest* to participate in crowdfunding here: https://simplexchat.typeform.com/crowdfunding
|
||||
So we're building both: a governance structure and a real business. The governance protects the network neutrality. The commercial model funds the network and makes our and other businesses on the network profitable, ensuring their independence. Neither works without the other.
|
||||
|
||||
Join the channel for updates here: https://smp4.simplex.im/g#g6pdBGlLoeOwqYmbmyvRye8EBiFd2inNUzKc87Pt3y4
|
||||
We recently published [a preliminary design of commercial model](https://simplex.chat/vouchers/) — private Community Credits that fund servers, development, and governance without surveillance or speculation. The full investment case will be published when crowdfunding launches.
|
||||
|
||||
You can *register your interest* to participate in crowdfunding here: https://simplexchat.typeform.com/crowdfunding
|
||||
|
||||
Join the channel for updates [here](https://smp10.simplex.im/c#q09nMBmWFGz1m2TvgfZFaEOG5D2a7Ma9mSkl6pHXEsg) — you must install v6.5 to join it — or you can join a [read-only group](https://smp12.simplex.im/g#gJzy7ETpuvltqARIB73TQUpJ11Lz4Xpl9xeH9qNoGCg) from the previous app versions.
|
||||
|
||||
_Disclaimer: SimpleX Chat is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._
|
||||
|
||||
[^release]: v6.5 release also improved how new users make the first connection, increased security of sending web links, and has many other improvements — see *What's new* in the app or full release notes.
|
||||
|
||||
[^preset]: Currently there is only one preset operator of chat relays in the app. It will change in the next release.
|
||||
|
||||
[^wilde]: Oscar Wilde wrote: *"Man is least himself when he talks in his own person. Give him a mask, and he will tell you the truth"*. Privacy is essential for our ability to say the truth, and without truth we cannot survive as society.
|
||||
|
||||
[^public]: From whitepaper: any channel joinable via a public link, whether encrypted or not, must be considered completely public — the cost of joining through automated means has collapsed with large language models. End-to-end encrypting such content provides no privacy; it only undermines users' security by creating false expectations and increases infrastructure operators' risks by making them unable to see what they deliver.
|
||||
|
||||
[^ulysses]: Ulysses pact — adding constraints to reduce future options. Sé Reed used this analogy for the WordPress Foundation: tying the project to the mast before the siren songs of commercial capture (https://www.wpwatercooler.com/wpwatercooler/ep484-whose-wordpress-is-it-anyway/).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: ba6af65c547cf941af0a1d1645188f7b7f234de1
|
||||
tag: 1f173abf6d6fccb617be1e7994629c405983c431
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
Revision 1, 2026-04-28
|
||||
|
||||
# SimpleX Channels: stateful information delivery and management
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [What are SimpleX Channels](#what-are-simplex-channels)
|
||||
- [Channels as transport layer](#channels-as-transport-layer)
|
||||
- [Content visibility and participant privacy](#content-visibility-and-participant-privacy)
|
||||
- [In comparison](#in-comparison)
|
||||
- [Non-goals](#non-goals)
|
||||
- [Architecture](#architecture)
|
||||
- [State and distribution](#state-and-distribution)
|
||||
- [Identity and ownership](#identity-and-ownership)
|
||||
- [Governance](#governance)
|
||||
- [Roles](#roles)
|
||||
- [Cryptographic primitives](#cryptographic-primitives)
|
||||
- [Security](#security)
|
||||
- [Design objectives](#design-objectives)
|
||||
- [Signing scope: roster only, content optional](#signing-scope-roster-only-content-optional)
|
||||
- [Threat model](#threat-model)
|
||||
- [Current gaps](#current-gaps)
|
||||
- [Future work](#future-work)
|
||||
- [Stateful access and history navigation](#stateful-access-and-history-navigation)
|
||||
- [Transcript integrity](#transcript-integrity)
|
||||
- [End-to-end encrypted side conversations](#end-to-end-encrypted-side-conversations)
|
||||
- [Relay addition and removal](#relay-addition-and-removal)
|
||||
- [Governance evolution](#governance-evolution)
|
||||
- [Pre-moderation](#pre-moderation)
|
||||
- [Scheduled delivery](#scheduled-delivery)
|
||||
- [Link preview proxying](#link-preview-proxying)
|
||||
- [Conclusion](#conclusion)
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
The SimpleX network provides private point-to-point communication without user or endpoint identifiers, but most speech that matters is public. Every existing platform that distributes content at scale identifies both publishers and their audiences to the operator - none protect participation privacy. SimpleX Chat supported peer-to-peer groups, but they cannot scale to large audiences. SimpleX Channels close this gap.
|
||||
|
||||
### What are SimpleX Channels
|
||||
|
||||
SimpleX Channels are a stateful information delivery and management layer built on the [SimpleX network](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md). SMP queues provide stateless, unidirectional packet delivery between two endpoints. Channels add persistence, state, and scalable distribution - enabling one-to-many publishing with cryptographic identity independent of infrastructure operators.
|
||||
|
||||
[SimpleX Chat](https://simplex.chat) is the first application, presenting channels as a broadcast publication model where owners publish and subscribers read, react, and comment. But channels are not limited to this use case - they are a general-purpose layer for distributing and managing stateful information (feeds, telemetry, automated pipelines, coordination services, social media). This document describes channels as a transport mechanism - the same mechanism will also be used for large groups, communities, wikis, forums, and other social media primitives.
|
||||
|
||||
The critical difference from conventional publish-subscribe systems is that channel identity and governance are controlled cryptographically by the channel owners, not by the infrastructure operators. Relays - SimpleX network clients that forward and optionally cache channel content - can be added, removed, and replaced without changing the channel's identity, address, content, or cryptographic trust chain. A channel's relationship with its relays is transient; its identity is permanent. The authoritative record of content is hosted on channel owners' devices; relays perform transmission and caching similar to CDN infrastructure.
|
||||
|
||||
Channel owners hold full control of the channel - its identity, content, governance rules, and membership - through self-custody of cryptographic keys. No infrastructure operator, relay provider, or third party can control or alter a channel without the owner's keys. Blockchain systems achieve a related property for financial assets - no third party can control holdings - through network-wide consensus. Channels achieve it through local authority and cryptographic signatures, without global consensus or a public ledger. Unlike blockchain state, channel state is mutable by the owner and not publicly verifiable by third parties.
|
||||
|
||||
### Channels as transport layer
|
||||
|
||||
The SimpleX network has three transport layers, each built on the one below:
|
||||
|
||||
1. **SMP** ([SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md)) - stateless, unidirectional packet delivery between two endpoints through SMP routers. Provides fixed-size blocks, 2-node onion routing, and transport metadata protection.
|
||||
|
||||
2. **SimpleX agents** ([agent protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/agent-protocol.md)) - bidirectional, redundant connections between endpoints, with end-to-end post-quantum double ratchet encryption. The [SimpleX Chat Protocol](./simplex-chat.md) runs on top of this layer, providing direct messaging, group communication, and metadata delivery for file transfers via [XFTP protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/xftp.md).
|
||||
|
||||
3. **Channels** - stateful, one-to-many information delivery and management with cryptographic ownership and programmable governance. This layer runs on top of chat and agent layer 2, and it is described in this document.
|
||||
|
||||
No network-wide user profile identifiers exist at any of these layers. Just as SMP enables private messaging by providing transport without user identifiers, channels enable public communication while preserving participation privacy at the distribution layer.
|
||||
|
||||
Channel relays are themselves SimpleX clients in the SMP network, connecting to SMP routers using the same protocol, the same 2-node onion routing, and the same fixed-size transport blocks as any other endpoint. Even though the SMP network can distinguish a relay from a person's phone by its transport patterns, it prevents relays from learning anything about other network endpoints. In the case of SimpleX Chat, any CLI client can act as a chat relay without modifications.
|
||||
|
||||
Channels therefore inherit all of SMP's transport privacy properties:
|
||||
|
||||
- **Relays cannot observe subscriber network addresses.** The relay sees SMP queue addresses, not IP addresses or network sessions. The subscriber's IP is known only to their SMP router, which cannot see the message content (encrypted at the agent layer) or the IP addresses of whoever sends messages.
|
||||
|
||||
- **SMP routers cannot see channel content.** Messages between relay and subscriber are end-to-end encrypted. The SMP router forwards fixed-size encrypted blocks without knowing whether they carry channel messages, direct messages, or anything else.
|
||||
|
||||
- **Participation in multiple channels is unlinkable.** Each channel connection uses independent SMP queues with separate cryptographic credentials. Because of packet-level anonymity in 2-node routing, even if a subscriber uses the same SMP routers for all channels, the sending relays cannot determine this without collusion with those routers. Clients choose independently operated routers by default.
|
||||
|
||||
No single point in the system sees both content and network identity. SMP routers see network addresses but not content, and no single SMP router can see which endpoints are communicating because clients choose independently operated routers. Relays see content but not network addresses.
|
||||
|
||||
### Content visibility and participant privacy
|
||||
|
||||
Any channel joinable via a public link, whether encrypted or not, must be considered completely public - the cost of joining through automated means has collapsed with large language models and is approaching zero. End-to-end encrypting such content provides no privacy; it only undermines users' security by creating false expectations and increases infrastructure operators' risks by making them unable to see what they deliver. Private channels with encrypted content are a separate use case discussed in [Future work](#end-to-end-encrypted-side-conversations).
|
||||
|
||||
Content of public channels is therefore not end-to-end encrypted between owner and subscriber. Relays can read the messages they forward. Relay operators cannot undetectably alter channel content when multiple relays serve the channel, and cannot alter signed content at all - the authoritative state is held by owners. That each channel can use multiple chat relays provides both technical reliability and censorship resistance against any relay-specific content policies.
|
||||
|
||||
The achievable privacy property for public communication is participation privacy - protecting who reads and writes content. The SMP transport carries no user identifiers, and relays are ordinary SMP clients, so subscribers connect without revealing their identity, network address, or any information that persists across channels. If an adversary joins a SimpleX channel, they see everything that is sent, but cannot determine who sent it or link any participant to anything outside the channel.
|
||||
|
||||
Other systems make the opposite choice: content encryption in exchange for participant identification. For groups and channels joinable via public links this is the opposite of what is needed - the content encryption is meaningless (anyone can join and read), while the participant identification is the security threat.
|
||||
|
||||
### In comparison
|
||||
|
||||
**Telegram channels** - the operator controls channel identity (usernames are revocable), has full access to both content and participant identity. Channels cannot exist without Telegram's permission.
|
||||
|
||||
**Nostr relays** - a single persistent key is used for publishing, following, and identity. Relays see content, the user's key, and their IP address. All posts and follow lists are signed and non-repudiable, linked to the same key - making both publishing and reading activity traceable and undeniable.
|
||||
|
||||
**Signal groups** - content is end-to-end encrypted, but the operator manages group state and can observe the membership graph. Groups are capped at 1,000 members with no concept of a channel.
|
||||
|
||||
**Matrix rooms** - server operators see room membership and metadata. Room identity is bound to the creating server's domain - if the server disappears, the room identity is lost.
|
||||
|
||||
**Mastodon / ActivityPub** - publisher identity is bound to a server domain - if the server disappears, the identity is lost. Server operators see all content and all follower relationships. No encryption or privacy of any kind.
|
||||
|
||||
| Property | Telegram | Nostr | Signal | Matrix | Mastodon | **SimpleX** |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Content visible to operator | Yes | Yes | No | Configurable | Yes | **Yes** |
|
||||
| Participant identity visible to operator | Yes | Yes | Yes | Yes | Yes | **No** |
|
||||
| Channel identity independent of infrastructure | No | Yes | No | No | No | **Yes** |
|
||||
| Sovereign ownership (no 3rd party can seize) | No | Yes | No | No | No | **Yes** |
|
||||
| Programmable governance | No | No | No | No | No | **Planned** |
|
||||
| Cryptographic content deniability | No | No | Yes | Yes | No | **Yes (default)** |
|
||||
| Scalable one-to-many delivery | Yes | Yes | No | Limited | Yes | **Yes** |
|
||||
|
||||
### Non-goals
|
||||
|
||||
Channels do not attempt to:
|
||||
|
||||
- **Encrypt public content from relay operators.** See [Content visibility and participant privacy](#content-visibility-and-participant-privacy).
|
||||
- **Assign persistent identities to participants.** There are no usernames, public keys, or any identifiers that persist across channels or link activity across contexts.
|
||||
- **Require network-wide consensus.** Channel state is authoritative on owner devices. The network does not validate channel transactions.
|
||||
- **Guarantee immutability of content.** Channel state is fully controlled and mutable by owners, unlike blockchain state, which is immutable by design.
|
||||
|
||||
## Architecture
|
||||
|
||||
The introduction established what channels provide and why. This section describes how: where state lives, how identity and ownership work, how governance evolves, and what each participant does.
|
||||
|
||||
### State and distribution
|
||||
|
||||
The authoritative record of a channel - content, member roster, profile, cryptographic keys, governance rules - is held by channel owners on their own devices, not on relays, not on any server, and not on any shared ledger. Relays hold transient copies for distribution and optional caching, analogous to CDN edge nodes: the origin holds the truth, CDN nodes come and go. Consensus is only required between channel owners, not across the entire network.
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Owner │ <- authoritative state
|
||||
└────┬─────┘
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
│ │ │
|
||||
┌────▼───┐ ┌────▼───┐ ┌────▼───┐
|
||||
│Relay A │ │Relay B │ │Relay C │ <- cache / distribution
|
||||
└────┬───┘ └────┬───┘ └────┬───┘
|
||||
│ │ │
|
||||
┌─────┼─────┐ ... ┌─────┼─────┐
|
||||
│ │ │ │ │ │
|
||||
S1 S2 S3 S7 S8 S9 <- received copies
|
||||
```
|
||||
|
||||
Content originates on the owner's device and flows through relays to subscribers. Each relay independently forwards to all of its subscribers. Subscribers do not connect to owners or to each other - this provides better scalability than peer-to-peer SimpleX groups, where adding a member requires N new connections. When multiple relays serve the same channel, subscribers deduplicate at the client level.
|
||||
|
||||
**Failure modes:**
|
||||
|
||||
- **Loss of a relay is loss of a cache node, not loss of data.** The owner can send the same content through a replacement relay.
|
||||
|
||||
- **Loss of all owner devices is the catastrophic event** - relay caches become orphaned and the channel's private keys are gone. Multiple owners and backups mitigate this risk.
|
||||
|
||||
- **Disagreements between relays are resolved by the origin.** The owner's version is authoritative, settling cache inconsistency through any reachable relay.
|
||||
|
||||
Subscribers hold their own received copies. Signed messages are independently verifiable without consulting the relay or owner. Unsigned content depends on cross-relay consistency or future transcript integrity mechanisms.
|
||||
|
||||
### Identity and ownership
|
||||
|
||||
A channel's identity is the SHA-256 hash of the genesis root public key, computed at creation time and never changed - even if relays are added, removed, or the channel link is rotated. It is self-authenticating: derived from a key pair that only the channel creator held. It is embedded in the channel's link, distributed in the profile to all members, and used as a binding prefix in all signed messages.
|
||||
|
||||
Subscribers validate that the identity in the link matches the identity in the profile, preventing link substitution. Profile updates that attempt to change the identity are rejected. Full validation that the identity equals the hash of the root key is deferred: if current clients enforced this check, they would reject future rotated links as invalid. The identity is correctly managed today; validation will be enforced with the key rotation protocol. See the [group identity binding RFC](../rfcs/2026-03-28-group-identity-binding.md).
|
||||
|
||||
The root key does not sign messages directly. Instead, it authorizes owner keys through a signed chain. At creation, the owner generates a root key pair and a separate member key pair for signing. The member key is published as an authorization entry signed by the root key. New owners can be added by any previously authorized owner signing a new entry. Anyone retrieving the channel link can verify this chain without network access.
|
||||
|
||||
The root key is a bootstrap key - it certifies owners, then need not be used again. All owners are cryptographically indistinguishable to subscribers (they all have equally valid authorization chains), which - provided multiple owners were signed by the root key - conceals the creator's identity.
|
||||
|
||||
The channel link is the out-of-band trust anchor - relays and SMP routers cannot modify link content. All members announce their signing keys on joining. Owner keys are verifiable against the link. Role changes (promoting members to admin, moderator) are signed by owners at the protocol level.
|
||||
|
||||
A planned extension will record role changes as a linearly ordered signed roster log with consistent sequencing across all owners, relays, and subscribers. This linearization prevents ambiguous roster states from concurrent unordered changes, and creates a verifiable chain of trust from the channel link through owners to all elevated roles. Out-of-band key verification for non-owner members will further extend this to E2E encrypted conversations.
|
||||
|
||||
### Governance
|
||||
|
||||
"Management" in "information delivery and management" refers not only to managing content but to managing the channel itself - who can make decisions, and how.
|
||||
|
||||
The low-level protocol supports multiple owners from the initial release. The application-level governance model evolves through a planned progression:
|
||||
|
||||
**Current (v6.5): Single owner.** One owner controls the channel. All administrative actions (profile changes, roster modifications, relay management) are decided by this single owner. The protocol-level owners chain supports verification of multiple entries, but the application creates and manages only one.
|
||||
|
||||
**Near-term (v7): Multiple owners, any-owner-decides.** Multiple owners share control of the channel. Any owner can independently make any administrative decision - add or remove members, change the profile, manage relays. This is the most common decision-making model in practice (equivalent to "all admins are equal" in most online platforms). No coordination between owners is required for any action.
|
||||
|
||||
**Future: Multisig and programmable governance.** Further stages include M-of-N multisig for administrative actions and, eventually, programmable governance rules defined as code in the channel's definition. The protocol must support these without prescribing a specific governance model.
|
||||
|
||||
### Roles
|
||||
|
||||
- **Owners** create the channel, hold the authoritative state and private keys on their devices, publish content, and manage the member roster. Owners sign administrative messages and optionally content messages. A channel must have at least one owner.
|
||||
|
||||
- **Relays** receive content from owners and members with posting rights, optionally cache it, and forward it to subscribers. They accept new subscriber connections and introduce them to the channel owners. Relays cannot author messages. A channel must have at least one active relay. Relays are ordinary SimpleX clients - a relay can be operated by anyone (a channel operator, a third-party service provider, or a self-hosted instance) and each creates its own contact address link, bound to the channel's identity. The relay's relationship with the channel is transient - owners can add and remove relays without changing the channel's identity.
|
||||
|
||||
- **Subscribers** connect to relays and receive content. They cannot send messages by default, but can be given posting rights.
|
||||
|
||||
Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol.
|
||||
|
||||
For protocol-level detail - wire formats, message types, signing and verification mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md).
|
||||
|
||||
|
||||
## Cryptographic primitives
|
||||
|
||||
- **Ed25519** - channel identity (root key pair), owner authorization chain, and message signing. The signature binding prefix includes the channel's entity ID and the sender's member ID, preventing cross-channel replay.
|
||||
|
||||
- **SHA-256** - derives the channel's entity ID from the genesis root public key. Immutable, serves as the channel's permanent identity.
|
||||
|
||||
- **Double ratchet with post-quantum KEM** (inherited from [SimpleX agent layer](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/agent-protocol.md)) - end-to-end encryption for all SMP transport. Not channel-specific - channels inherit it by being built on the agent layer. Future E2E side conversations (support scope, member DMs, private channels) will use the same mechanism.
|
||||
|
||||
Content messages are not signed by default to preserve cryptographic deniability - see [Signing scope](#signing-scope-roster-only-content-optional). Owners may opt into signing all content in a future release.
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
This section examines what the architectural properties protect against, where they hold, and where gaps remain.
|
||||
|
||||
### Design objectives
|
||||
|
||||
The channel protocol is designed to achieve the following security objectives:
|
||||
|
||||
1. **Stable message delivery** between channel participants, resilient to individual relay failures.
|
||||
2. **No possibility for a relay to substitute the channel** - the channel's identity is cryptographically bound to the link and profile controlled by channel owners.
|
||||
3. **No possibility for a relay to impersonate an owner** - administrative messages require valid signatures.
|
||||
4. **Prevention of relay-initiated roster manipulation** - member removal, role changes, and other roster modifications require valid owner signatures.
|
||||
5. **Relay transience** - the owner can add and remove relays, including the last relay, without permanently losing the channel. Subscribers can restore connectivity by retrieving updated link data.
|
||||
6. **Sender anonymity within multi-owner channels** - owners can publish as the channel, hiding which specific owner authored a message from subscribers.
|
||||
7. **Participant privacy** - relay operators cannot determine subscriber identity or network address, and subscribers cannot determine each other's identity. This is inherited from the SMP transport layer.
|
||||
|
||||
### Signing scope: roster only, content optional
|
||||
|
||||
By default, only roster-modifying and administrative messages are signed. Content messages are not signed. Two reasons:
|
||||
|
||||
1. **Cryptographic deniability.** Signing creates non-repudiable proof of authorship verifiable by any third party. Without signatures, no such proof exists - a relay could have fabricated any unsigned message.
|
||||
|
||||
2. **Proportional defense.** Changes to roster, channel profile, and permissions can be disruptive and irreversible - they must be authenticated at processing time. Content manipulation is detectable post-hoc through cross-relay consistency, and the authoritative record on the owner's device is unaffected.
|
||||
|
||||
Owners will be able to opt into signing content on a per-channel or per-message basis - some publishers want non-repudiable authorship, others prefer deniability.
|
||||
|
||||
### Threat model
|
||||
|
||||
This threat model assumes the [SimpleX network threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/security.md) and addresses threats specific to the channel layer.
|
||||
|
||||
**A single compromised relay**
|
||||
|
||||
*can:*
|
||||
|
||||
- Substitute unsigned content or selectively drop messages for its subscribers. Detectable by subscribers connected to other relays - the owner's version is authoritative. TODO: difference detection not yet implemented.
|
||||
- Selectively target specific subscribers while delivering correctly to others.
|
||||
- Ignore the "message from channel" directive, revealing which owner sent a message. Detectable out-of-band.
|
||||
- Fabricate or hide subscriber connections, inflating or deflating counts. Detectable if subscribers are connected to other relays.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Undetectably substitute content - subscribers on honest relays receive the original.
|
||||
- Alter the channel's authoritative state on the owner's device.
|
||||
- Substitute the channel profile or impersonate an owner - these require valid signatures.
|
||||
- Redirect subscribers to a different channel - the entity ID is validated across link and profile.
|
||||
- Determine subscriber identity or network address - inherited from SMP transport.
|
||||
- Correlate subscriber participation across channels - each connection uses independent SMP queues. The subscriber chooses their SMP router independently, so collusion between a relay and the relay's SMP router does not compromise connections through a different router.
|
||||
|
||||
**All relays compromised and colluding**
|
||||
|
||||
*can:*
|
||||
|
||||
- Undetectably substitute unsigned content for all subscribers, unless owners sign content messages.
|
||||
- Prevent delivery of any messages, including signed ones (signing prevents substitution, not dropping).
|
||||
- Fabricate or hide subscriber connections undetectably.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Forge signed administrative messages or substitute the channel profile.
|
||||
- Alter the authoritative state on the owner's device.
|
||||
|
||||
**Compromise of owner keys**
|
||||
|
||||
An attacker who obtains the root private key or an owner's member private key (through device compromise, backup theft, or coercion) can impersonate the owner and sign arbitrary administrative messages. This is a different threat from key loss - the channel continues operating, but under adversarial control. Mitigation depends on owner-side operational security and future multisig governance. For the threat model of the channel link itself (the trust anchor), see the [short links for groups RFC](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2025-04-04-short-links-for-groups.md).
|
||||
|
||||
**Loss of all owner devices**
|
||||
|
||||
The channel can have no new content, no administrative updates, no new owners. Relay caches continue delivering existing content but cannot be refreshed, and will eventually expire in the absence of the owner connection. Multiple owners and key backups mitigate this risk.
|
||||
|
||||
**A subscriber**
|
||||
|
||||
*can:*
|
||||
|
||||
- See all public content, by design.
|
||||
- Join multiple times with different profiles, inflating counts.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Identify other subscribers, send messages to the channel (unless given posting rights), or forge messages of the owner or other subscribers.
|
||||
|
||||
**A passive network observer**
|
||||
|
||||
*can:*
|
||||
|
||||
- Observe communication with an SMP router, but not whether it is channel-related.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Determine which channel a subscriber uses, correlate channel activity with other SimpleX activity, or identify a relay as distinct from an ordinary user, other than by traffic volume. Inherited from SMP transport.
|
||||
|
||||
### Current gaps
|
||||
|
||||
1. **Cross-relay consistency detection.** Duplicate messages are silently deduplicated without hash comparison. Designed but not implemented.
|
||||
2. **Link entity ID validation.** Deferred to a future version with key rotation. See [group identity binding RFC](../rfcs/2026-03-28-group-identity-binding.md).
|
||||
3. **Multi-relay UX.** Protocol supports multiple relays per subscriber; no UX for monitoring relay-level delivery health. It will be added in v6.5.x.
|
||||
|
||||
|
||||
## Future work
|
||||
|
||||
### Stateful access and history navigation
|
||||
|
||||
Currently, relays send recent cached history on join but do not support navigation or search. Planned: history pagination by timestamp or message ID, remote search against relay caches, and selective retrieval of specific message ranges. Relay operators can differentiate on cache depth and search capabilities.
|
||||
|
||||
### Transcript integrity
|
||||
|
||||
- **Opt-in content signing.** Per-channel or per-message choice to sign content, making it non-repudiable. This will be released in SimpleX Chat v7.
|
||||
- **Subscriber transcript acknowledgment.** Subscribers periodically sign a digest of received history ("I've seen it" rather than "I've authored it"), enabling detection of relay manipulation through diverging digests.
|
||||
- **Merkle tree signing.** Owner periodically publishes a signed Merkle root. Subscribers verify their copies against the owner's authoritative record.
|
||||
|
||||
### End-to-end encrypted side conversations
|
||||
|
||||
- **E2E encrypted support scope** between subscriber and moderator/owner.
|
||||
- **E2E encrypted DMs between members** where channel settings permit, using standard SimpleX connection establishment.
|
||||
- **Private channels** where the entire content stream is encrypted to authorized subscribers. The relay becomes a conduit that sees neither content nor identity.
|
||||
|
||||
### Relay addition and removal
|
||||
|
||||
Dynamic relay addition with cache population from existing relays or owner. Relay removal with subscriber migration. Relay rotation with continuity - new relay connects before old relay is removed. It will be added in v6.5.x.
|
||||
|
||||
### Governance evolution
|
||||
|
||||
- **Multiple owners (v7):** concurrent administrative authority, any owner acts independently.
|
||||
- **Multisig:** M-of-N approval for administrative actions, with per-action quorums.
|
||||
- **Programmable governance:** rules defined as code in the channel definition.
|
||||
|
||||
### Pre-moderation
|
||||
|
||||
Subscriber messages reviewed by moderators before becoming visible to all subscribers.
|
||||
|
||||
### Scheduled delivery
|
||||
|
||||
Messages scheduled for future delivery, cached by relay until the scheduled time.
|
||||
|
||||
### Link preview proxying
|
||||
|
||||
The relay loads link previews on behalf of the sender - it already sees message content, so it learns nothing new, and unlike the sender its IP is not linked to any identity.
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
SimpleX Channels enable a publisher to reach an unlimited audience without any infrastructure operator knowing who that audience is. No third party can seize the channel because owners hold the keys and the authoritative state on their own devices - relays only cache and forward. Owner signatures protect content integrity and the trust chain extends to all administrative roles. These properties require a network without participant identifiers - they cannot be added to a system that has them.
|
||||
@@ -0,0 +1,243 @@
|
||||
Revision 1, 2026-04-28
|
||||
|
||||
# SimpleX Channels Protocol
|
||||
|
||||
For architecture, design rationale, security properties, and threat model, see [SimpleX Channels Overview](./channels-overview.md).
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Protocol](#protocol)
|
||||
- [Channel creation](#channel-creation)
|
||||
- [Relay acceptance](#relay-acceptance)
|
||||
- [Subscriber connection](#subscriber-connection)
|
||||
- [Message signing](#message-signing)
|
||||
- [Message forwarding](#message-forwarding)
|
||||
- [Binary batch format](#binary-batch-format)
|
||||
- [Delivery pipeline](#delivery-pipeline)
|
||||
- [Message deduplication](#message-deduplication)
|
||||
- [Channel-as-sender messages](#channel-as-sender-messages)
|
||||
- [Member support scope](#member-support-scope)
|
||||
|
||||
|
||||
## Protocol
|
||||
|
||||
This document describes the channel protocol as currently implemented. It builds on the [SimpleX Chat Protocol](./simplex-chat.md), using the same message format and connection model, with extensions for relay-mediated distribution and cryptographic message signing.
|
||||
|
||||
### Channel creation
|
||||
|
||||
Creating a channel involves generating cryptographic material, creating the channel link, and connecting relay members:
|
||||
|
||||
1. **Key generation.** The owner generates an Ed25519 root key pair. The entity ID is computed as `sha256(rootPubKey)`. A separate member key pair is generated for message signing, and an `OwnerAuth` entry is created, signed by the root key.
|
||||
|
||||
2. **Link creation.** The owner calls the agent's `prepareConnectionLink` API with the root key pair and entity ID. This returns a prepared link (including a `ConnShortLink` address) without any network calls. The link address is deterministic, derived from the fixed data hash, so it can be embedded in the group profile immediately.
|
||||
|
||||
3. **Link data upload.** The owner calls `createConnectionForLink`, which makes a single network call to create the SMP queue and upload the encrypted link data. The link's fixed data contains the root public key and connection request. The mutable user data contains the `OwnerAuth` array, the channel profile (including the entity ID and the link itself), and the initial subscriber count.
|
||||
|
||||
4. **Relay invitation.** For each selected relay, the owner sends a contact request containing an `x.grp.relay.inv` message with the channel's short link. The relay retrieves the link data, validates the channel profile, creates its own relay link (with the channel's entity ID in its immutable data), and responds with `x.grp.relay.acpt` containing its relay link.
|
||||
|
||||
5. **Link update.** As each relay accepts and provides its relay link, the owner validates that the relay link contains the correct entity ID, then adds the relay link to the channel link's mutable data.
|
||||
|
||||
6. **Local record.** The channel is stored on the owner's device with the root private key, member private key, and channel profile. This local record is the authoritative state of the channel.
|
||||
|
||||
### Relay acceptance
|
||||
|
||||
When a relay receives an invitation to serve a channel, it validates the channel and creates its own relay link. This flow is currently part of channel creation; adding relays to an existing channel is planned but not yet implemented.
|
||||
|
||||
1. Owner sends `x.grp.relay.inv` to the relay's contact address. This message includes the relay's member ID and role, the owner's profile, and the channel's short link.
|
||||
|
||||
2. Relay receives the invitation and creates a relay request record. A relay request worker processes it asynchronously.
|
||||
|
||||
3. The worker retrieves the channel's link data from the SMP server, extracts and validates the channel profile and owner authorization.
|
||||
|
||||
4. The relay creates its own contact address link (the relay link) with the channel's entity ID in the immutable fixed data.
|
||||
|
||||
5. The relay accepts the owner's connection request, sending its relay link in the acceptance.
|
||||
|
||||
6. The owner retrieves the relay link data, validates that the entity ID in the relay link matches the channel's entity ID, and adds the relay link to the channel link's user data.
|
||||
|
||||
TODO: Periodic monitoring where the relay retrieves channel link data to verify its relay link is still listed is planned but not yet implemented.
|
||||
|
||||
### Subscriber connection
|
||||
|
||||
A subscriber joins a channel through the following flow:
|
||||
|
||||
1. **Link retrieval.** The subscriber scans or receives the channel's short link. The client retrieves the link data, which contains the channel profile, owner authorization chain, and list of relay links.
|
||||
|
||||
2. **Relay link resolution.** For each relay link listed, the client resolves the `ConnectionRequestUri` from the relay's short link.
|
||||
|
||||
3. **Connection.** The client connects to relays - the first synchronously, the rest asynchronously. Each connection sends an `x.member` message with the subscriber's profile (or an incognito profile, created once and shared with all relays), member ID, and member signing key.
|
||||
|
||||
4. **Relay acceptance.** Each relay accepts the connection, creates a member record for the subscriber with the configured subscriber role (default `observer`), and sends an `x.grp.link.inv` message with the channel profile and group link invitation data.
|
||||
|
||||
5. **Introduction.** The relay introduces the new subscriber to the channel's moderators and owners by sending an `x.grp.mem.new` message. It also sends moderator/owner profiles to the subscriber.
|
||||
|
||||
6. **History.** If the channel has history sharing enabled, the relay sends recent cached history to the new subscriber.
|
||||
|
||||
The subscriber is functional (can receive messages) as soon as at least one relay connection succeeds. Additional relay connections provide redundancy and cross-relay consistency checking.
|
||||
|
||||
### Message signing
|
||||
|
||||
Messages that alter the channel's roster, profile, or administrative state are cryptographically signed by the sending owner. Content messages are not signed by default; see [Signing scope](#signing-scope-roster-only-content-optional) for the rationale.
|
||||
|
||||
**Which messages require signatures:**
|
||||
|
||||
| Message | Description | Signed |
|
||||
|---|---|---|
|
||||
| `x.grp.del` | Delete channel | Required |
|
||||
| `x.grp.info` | Update channel profile | Required |
|
||||
| `x.grp.prefs` | Update channel preferences | Required |
|
||||
| `x.grp.mem.del` | Remove member | Required |
|
||||
| `x.grp.mem.role` | Change member role | Required |
|
||||
| `x.grp.mem.restrict` | Restrict member | Required |
|
||||
| `x.grp.leave` | Leave channel | Required (unverified allowed between subscribers) |
|
||||
| `x.info` | Update member profile | Required (unverified allowed between subscribers) |
|
||||
| `x.msg.new` | Content message | Not signed |
|
||||
| `x.msg.update` | Edit message | Not signed |
|
||||
| `x.msg.del` | Delete message | Not signed |
|
||||
|
||||
**Signing process:**
|
||||
|
||||
The signing context binds the signature to a specific channel and sender:
|
||||
|
||||
```
|
||||
bindingPrefix = smpEncode(CBGroup) <> smpEncode(publicGroupId, memberId)
|
||||
signedBytes = bindingPrefix <> messageBody
|
||||
signature = Ed25519.sign(memberPrivKey, signedBytes)
|
||||
```
|
||||
|
||||
The binding prefix includes the chat binding tag (`"G"` for group), the channel's entity ID, and the sender's member ID. This prevents cross-channel and cross-member replay attacks - a signature valid in one channel cannot be reused in another.
|
||||
|
||||
**Verification process:**
|
||||
|
||||
When a subscriber receives a signed message:
|
||||
|
||||
1. The signature is present: reconstruct the binding prefix from the channel's stored entity ID and the sender's member ID. Verify all signatures against the sender's stored public key. If all verify, the message is accepted as verified.
|
||||
|
||||
2. The signature is present but the sender's key is unknown: the message is accepted as signed-but-unverified only if the event does not require a signature. For `x.grp.leave` and `x.info` between subscribers whose keys haven't been exchanged yet, unverified signatures are permitted as a temporary measure.
|
||||
|
||||
3. No signature is present: the message is accepted only if the event does not require a signature (i.e., the channel does not use relays, or the event is a content message).
|
||||
|
||||
If verification fails for a message that requires a signature, the message is rejected and a bad signature event is shown to the user.
|
||||
|
||||
### Message forwarding
|
||||
|
||||
Content originates on the owner's device and flows through relays to subscribers. The forwarding mechanism preserves the original message bytes, including any signature, without re-encoding:
|
||||
|
||||
**Owner to Relay:** The owner sends messages directly to each relay over their SMP connection. Messages are encoded in binary batch format.
|
||||
|
||||
**Relay processing:** When a relay receives a message from an owner, it:
|
||||
|
||||
1. Parses and processes the message locally (updating its cached state, e.g. for roster changes).
|
||||
2. If the relay is configured to forward for this channel, creates a **delivery task** for each message that should be forwarded to subscribers. The task records the message ID, the sender's member ID, the broker timestamp, and whether the message was sent as the channel (not attributed to a specific owner).
|
||||
3. The delivery task is persisted to the database for delivery reliability - ensuring forwarding can resume after a relay crash.
|
||||
|
||||
**Relay to Subscribers:** A delivery task worker reads pending tasks, batches them into delivery jobs, and a delivery job worker sends each job to subscribers in paginated batches (using a cursor over group member IDs).
|
||||
|
||||
For forwarded messages from subscribers to owners (e.g. support scope messages), the relay wraps the message in a forwarding envelope:
|
||||
|
||||
```
|
||||
forwardEnvelope = ">" <> smpEncode(GrpMsgForward) <> encodeBatchElement(signedMsg, msgBody)
|
||||
```
|
||||
|
||||
This preserves the original message's signature bytes verbatim.
|
||||
|
||||
### Binary batch format
|
||||
|
||||
Channels use a binary batch format that preserves exact message bytes for signature verification. This is distinct from the JSON array batching used by regular groups.
|
||||
|
||||
```abnf
|
||||
binaryBatch = %s"=" elementCount *batchElement
|
||||
elementCount = 1*1 OCTET ; 1-255 elements
|
||||
batchElement = elementLen elementBody
|
||||
elementLen = 2*2 OCTET ; 16-bit big-endian length
|
||||
|
||||
elementBody = signedElement / forwardElement / plainElement / fileElement
|
||||
|
||||
signedElement = %s"/" chatBinding sigCount *msgSignature jsonBody
|
||||
forwardElement = %s">" grpMsgForward (signedElement / plainElement)
|
||||
plainElement = %s"{" *OCTET ; JSON message body
|
||||
fileElement = %s"F" *OCTET ; binary file chunk
|
||||
|
||||
chatBinding = 1*1 OCTET ; "G" (group), "D" (direct), "C" (channel)
|
||||
sigCount = 1*1 OCTET ; number of signatures (1-255)
|
||||
msgSignature = keyRef sigBytes
|
||||
keyRef = %s"M" ; member key reference
|
||||
sigBytes = 64*64 OCTET ; Ed25519 signature
|
||||
|
||||
grpMsgForward = fwdSender brokerTs
|
||||
fwdSender = memberFwd / channelFwd
|
||||
memberFwd = %s"M" memberId memberName ; attributed to specific member
|
||||
channelFwd = %s"C" ; attributed to channel as sender
|
||||
brokerTs = 8*8 OCTET ; UTC system time
|
||||
```
|
||||
|
||||
The parser (`parseChatMessages`) dispatches on the first byte:
|
||||
|
||||
- `'='` -> binary batch (new format, used by channels)
|
||||
- `'X'` -> compressed (decompress, then re-parse)
|
||||
- `'['` -> JSON array (legacy group format)
|
||||
- `'{'` -> single JSON message
|
||||
|
||||
Forward elements contain the original message bytes verbatim. The relay does not re-encode the inner message. This is what makes signature verification possible after forwarding: the exact bytes that were signed by the owner are preserved through the relay.
|
||||
|
||||
Nested forwarding (`>` inside `>`) is explicitly rejected by the parser.
|
||||
|
||||
### Delivery pipeline
|
||||
|
||||
The relay's delivery pipeline has two stages, both backed by persistent database tables for delivery reliability (not for authoritative storage - the relay's database is a delivery queue, not a content database):
|
||||
|
||||
**Stage 1: Delivery tasks.** When the relay receives a message from an owner that should be forwarded, it creates a `delivery_task` record:
|
||||
|
||||
```
|
||||
delivery_task:
|
||||
group_id, worker_scope, job_scope,
|
||||
sender_group_member_id, message_id,
|
||||
message_from_channel (bool),
|
||||
task_status (new -> processed)
|
||||
```
|
||||
|
||||
A **task worker** (one per group per scope) reads pending tasks, batches multiple tasks into a single binary batch body, and creates a delivery job.
|
||||
|
||||
**Stage 2: Delivery jobs.** A delivery job contains the pre-encoded batch body and a cursor for paginated delivery:
|
||||
|
||||
```
|
||||
delivery_job:
|
||||
group_id, worker_scope, job_scope,
|
||||
body (pre-encoded binary batch),
|
||||
cursor_group_member_id,
|
||||
job_status (pending -> complete)
|
||||
```
|
||||
|
||||
A **job worker** reads the body and delivers it to subscribers in paginated batches. For each page, it loads a bucket of subscribers by cursor position, sends the body to all of them, advances the cursor, and continues until all subscribers have been served. This avoids loading all subscribers into memory at once.
|
||||
|
||||
For subsequent subscribers in a batch, the agent uses a value reference to the first subscriber's message body, avoiding redundant data transmission to the SMP server.
|
||||
|
||||
### Message deduplication
|
||||
|
||||
When multiple relays serve the same channel, each subscriber receives the same message from each relay independently. Deduplication is performed at the subscriber's client level using the message's shared message ID:
|
||||
|
||||
- When saving a received message, the client checks whether a message with the same shared ID already exists for this group.
|
||||
- If a duplicate is found, the message is silently dropped (in channels with relays).
|
||||
- In non-relay groups, duplicate detection triggers a `x.grp.mem.con` notification to the forwarding member.
|
||||
|
||||
This is essentially cache coherence verification - comparing what was received from one cache node against another. TODO: Currently, deduplication only detects the presence of duplicates. The protocol design includes provisions for detecting differences between relay-delivered copies of the same message (hash comparison, UI indicators for discrepancies). This is described in the [channels forwarding RFC](../rfcs/2025-08-11-channels-forwarding.md) and is not yet implemented.
|
||||
|
||||
### Channel-as-sender messages
|
||||
|
||||
Owners can send messages attributed to the channel rather than to themselves. When `asGroup = True` is set in the message container, the relay forwards the message with a channel-as-sender tag instead of attributing it to a specific member. On the subscriber side, such messages are displayed as coming from the channel (using the channel's profile image and name) rather than from a specific owner.
|
||||
|
||||
This will be useful for channels with multiple owners (not yet implemented at application level) where the identity of the specific sender should not be visible to subscribers. The relay must respect this directive; ignoring it and revealing the sending owner's identity is a threat vector (detectable out-of-band by members communicating with the owner).
|
||||
|
||||
The forwarding binding prefix for channel-as-sender messages uses `CBChannel` instead of `CBGroup`, and includes only the channel's entity ID (not the sender's member ID):
|
||||
|
||||
```
|
||||
channelBinding = smpEncode(CBChannel) <> smpEncode(publicGroupId)
|
||||
```
|
||||
|
||||
### Member support scope
|
||||
|
||||
Channels support a **member support scope** - a private side-channel between a subscriber and the channel's moderators/owners. Messages sent in the support scope are delivered only to moderators and the scoped subscriber, not to all subscribers.
|
||||
|
||||
A support-scoped message includes the target member's ID. The delivery pipeline uses a separate job scope for support messages, loading only the scoped member and moderators rather than all subscribers.
|
||||
|
||||
Support scope messages are visible only to the subscriber who initiated the support conversation and to the channel's moderators. Other subscribers cannot see them. This allows subscribers to report issues, appeal moderation decisions, or communicate with administrators without revealing their identity to other subscribers.
|
||||
@@ -266,6 +266,12 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o
|
||||
|
||||
`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting.
|
||||
|
||||
### Channels: relay-mediated groups
|
||||
|
||||
Channels are groups where message delivery is mediated by dedicated relay members rather than by direct connections between all members. Channels extend the group sub-protocol with additional roles (`relay`, `observer`), message signing for administrative actions, a binary batch format for signed and forwarded messages, and an asynchronous delivery pipeline.
|
||||
|
||||
For architecture and design rationale, see [SimpleX Channels Overview](./channels-overview.md). For protocol-level detail - wire formats, message types, signing mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md).
|
||||
|
||||
## Sub-protocol for WebRTC audio/video calls
|
||||
|
||||
This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information.
|
||||
@@ -282,12 +288,13 @@ These message are used for WebRTC calls:
|
||||
|
||||
## Threat model
|
||||
|
||||
This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models:
|
||||
This threat model complements SMP, XFTP, push notifications and XRCP protocols threat models, as well as the channel-specific threat model:
|
||||
|
||||
- [SimpleX Messaging Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#threat-model);
|
||||
- [SimpleX File Transfer Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xftp.md#threat-model);
|
||||
- [Push notifications threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/push-notifications.md#threat-model);
|
||||
- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model).
|
||||
- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model);
|
||||
- [SimpleX Channels threat model](./channels-overview.md#threat-model).
|
||||
|
||||
#### A user's contact
|
||||
|
||||
@@ -342,3 +349,27 @@ This threat model compliments SMP, XFTP, push notifications and XRCP protocols t
|
||||
*cannot:*
|
||||
|
||||
- prove that two group members with incognito profiles is the same user.
|
||||
|
||||
#### A channel relay
|
||||
|
||||
For the full channel threat model, see [SimpleX Channels: threat model](./channels-overview.md#threat-model).
|
||||
|
||||
*can:*
|
||||
|
||||
- send arbitrary unsigned content messages to subscribers, effectively fabricating the content stream while the channel identity and signed profile remain intact.
|
||||
|
||||
- selectively drop any messages, both content and signed administrative events, for some or all subscribers.
|
||||
|
||||
- ignore the "message from channel" directive, revealing which specific owner sent a message.
|
||||
|
||||
- fabricate subscriber connections, inflating subscriber counts.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- impersonate an owner - administrative messages (roster changes, profile updates, channel deletion) require valid cryptographic signatures that the relay cannot produce.
|
||||
|
||||
- substitute the channel profile - profile changes require a valid owner signature.
|
||||
|
||||
- redirect joining subscribers to a different channel - the channel's entity ID is baked into both the channel link and the relay link's immutable data.
|
||||
|
||||
- determine the real-world identity of subscribers - subscriber connections carry no persistent identity.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "6.5.0-beta.10",
|
||||
"version": "6.5.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
|
||||
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
|
||||
const RELEASE_TAG = 'v6.5.0-beta.10';
|
||||
const RELEASE_TAG = 'v6.5.0';
|
||||
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
|
||||
|
||||
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Plan: Relay Request Worker Retry Limit
|
||||
|
||||
## Context
|
||||
|
||||
The relay request worker (`runRelayRequestWorker`) processes channel setup requests sequentially using a single worker (`relayRequestWorkerKey = 1`). When a request requires network calls to an unreachable server (e.g., fetching group link data via `getShortLinkConnReq'`), the worker retries indefinitely via `withRetryInterval` + `retryTmpError` — temp/host errors call `loop` with no limit. This blocks all subsequent relay requests from processing.
|
||||
|
||||
This is an attack vector: a channel owner can create a channel link on a server unreachable by the relay, causing the relay request worker to retry forever and blocking all other channel setup requests.
|
||||
|
||||
## Approach
|
||||
|
||||
Follow the XFTP worker retry pattern (`runXFTPDelWorker` in `simplexmq/src/Simplex/FileTransfer/Agent.hs:667`):
|
||||
|
||||
1. **Track retries and delay in DB**: Add `relay_request_retries` and `relay_request_delay` columns to the `groups` table
|
||||
2. **Order by retries**: Query for next work item ordered by `relay_request_retries ASC, created_at ASC` — items with fewer retries are processed first, stuck items get pushed to the back
|
||||
3. **Limit consecutive retries**: Replace `withRetryInterval` with `withRetryIntervalCount`, limiting to a small number of consecutive retries per pickup cycle (3, matching XFTP's `xftpConsecutiveRetries`). After the limit, the worker yields and picks the next item.
|
||||
4. **Store delay for resumption**: On each retry, store the current backoff delay in DB. On next pickup, resume backoff from the stored delay (XFTP pattern: `ri {initialInterval = d, increaseAfter = 0}`)
|
||||
5. **Expire old requests**: On temp error, before retrying, check if the request is older than 1 day and has 10+ retries — if so, mark as failed instead of retrying. Both conditions must hold — a request that's old but has few retries may just have been delayed, while a request with many retries that's recent is still being actively worked on.
|
||||
|
||||
### How this neutralizes the attack
|
||||
|
||||
- Attacker's request gets picked up, retried 3 times with backoff (~15s total), then yielded
|
||||
- Worker picks the next item by retry count — legitimate requests (retries=0) go first
|
||||
- Attacker's request accumulates retries, always processed last
|
||||
- After 1 day and 10+ retries, the request is marked failed and permanently excluded
|
||||
|
||||
---
|
||||
|
||||
## Detailed changes
|
||||
|
||||
### 1. Database migration
|
||||
|
||||
New migration: `M20260429_relay_request_retries.hs`
|
||||
|
||||
```sql
|
||||
ALTER TABLE groups ADD COLUMN relay_request_retries INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE groups ADD COLUMN relay_request_delay INTEGER;
|
||||
```
|
||||
|
||||
**Files:**
|
||||
- `src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs` (new)
|
||||
- `src/Simplex/Chat/Store/SQLite/Migrations.hs` (register)
|
||||
- `src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs` (new)
|
||||
- `src/Simplex/Chat/Store/Postgres/Migrations.hs` (register)
|
||||
- `simplex-chat.cabal` (add modules)
|
||||
|
||||
### 2. Extend RelayRequestData
|
||||
|
||||
**File:** `src/Simplex/Chat/Types.hs`
|
||||
|
||||
```haskell
|
||||
data RelayRequestData = RelayRequestData
|
||||
{ relayInvId :: InvitationId,
|
||||
reqGroupLink :: ShortLinkContact,
|
||||
reqChatVRange :: VersionRangeChat,
|
||||
relayRequestDelay :: Maybe Int64,
|
||||
relayRequestRetries :: Int,
|
||||
relayRequestCreatedAt :: UTCTime
|
||||
}
|
||||
```
|
||||
|
||||
- `relayRequestDelay`: resume backoff from stored position (XFTP pattern)
|
||||
- `relayRequestRetries`: current retry count, used with `relayRequestCreatedAt` to decide expiry in `retryTmpError`
|
||||
- `relayRequestCreatedAt`: group creation time, used for the 1-day expiry check
|
||||
|
||||
### 3. Update store functions
|
||||
|
||||
**File:** `src/Simplex/Chat/Store/RelayRequests.hs`
|
||||
|
||||
**`getNextPendingRelayRequest`** — two changes:
|
||||
- Order by `relay_request_retries ASC, created_at ASC` instead of `group_id ASC`
|
||||
- SELECT and return `relay_request_delay`, `relay_request_retries`, `created_at` in the data query
|
||||
|
||||
```haskell
|
||||
getNextPendingRelayRequest db =
|
||||
getWorkItem "relay request" getNextRequestGroupId getRelayRequestData (markRelayRequestFailed db)
|
||||
where
|
||||
getNextRequestGroupId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query db
|
||||
[sql|
|
||||
SELECT group_id FROM groups
|
||||
WHERE relay_own_status = ?
|
||||
AND relay_request_failed = 0
|
||||
AND relay_request_err_reason IS NULL
|
||||
ORDER BY relay_request_retries ASC, created_at ASC
|
||||
LIMIT 1
|
||||
|]
|
||||
(Only RSInvited)
|
||||
getRelayRequestData groupId =
|
||||
firstRow' toRelayRequestData (SEGroupNotFound groupId) $
|
||||
DB.query db
|
||||
[sql|
|
||||
SELECT relay_request_inv_id, relay_request_group_link,
|
||||
relay_request_peer_chat_min_version, relay_request_peer_chat_max_version,
|
||||
relay_request_delay, relay_request_retries, created_at
|
||||
FROM groups WHERE group_id = ?
|
||||
|]
|
||||
(Only groupId)
|
||||
where
|
||||
toRelayRequestData (Just relayInvId, Just reqGroupLink, Just minV, Just maxV, relayRequestDelay, relayRequestRetries, relayRequestCreatedAt) =
|
||||
Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV, relayRequestDelay, relayRequestRetries, relayRequestCreatedAt})
|
||||
toRelayRequestData _ = Left $ SEInternalError "missing relay request data"
|
||||
```
|
||||
|
||||
**New function: `updateRelayRequestRetries`**:
|
||||
|
||||
```haskell
|
||||
updateRelayRequestRetries :: DB.Connection -> GroupId -> Int64 -> IO ()
|
||||
updateRelayRequestRetries db groupId delay = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db
|
||||
"UPDATE groups SET relay_request_retries = relay_request_retries + 1, relay_request_delay = ?, updated_at = ? WHERE group_id = ?"
|
||||
(delay, currentTs, groupId)
|
||||
```
|
||||
|
||||
Export `updateRelayRequestRetries` and `markRelayRequestFailed` from module (the latter is currently internal, used only as a callback in `getWorkItem`).
|
||||
|
||||
### 4. Worker changes
|
||||
|
||||
**File:** `src/Simplex/Chat/Library/Subscriber.hs`
|
||||
|
||||
**Import change**: Add `withRetryIntervalCount` to the import from `Simplex.Messaging.Agent.RetryInterval`.
|
||||
|
||||
**Replace `withRetryInterval` with limited retry** in `runRelayRequestOperation`:
|
||||
|
||||
```haskell
|
||||
runRelayRequestOperation vr user uclId =
|
||||
withWork_ a doWork (withStore' getNextPendingRelayRequest) $
|
||||
\(groupId, rrd@RelayRequestData {relayRequestDelay}) -> do
|
||||
ri <- asks $ reconnectInterval . agentConfig . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) relayRequestDelay
|
||||
withRetryIntervalLimit ri' $ \delay loop -> do
|
||||
liftIO $ waitWhileSuspended a
|
||||
liftIO $ waitForUserNetwork a
|
||||
processRelayRequest groupId rrd `catchAllErrors` retryTmpError loop groupId rrd delay
|
||||
where
|
||||
maxConsecutiveRetries :: Int
|
||||
maxConsecutiveRetries = 3
|
||||
withRetryIntervalLimit :: RetryInterval -> (Int64 -> CM () -> CM ()) -> CM ()
|
||||
withRetryIntervalLimit ri action =
|
||||
withRetryIntervalCount ri $ \n delay loop ->
|
||||
when (n < maxConsecutiveRetries) $ action delay loop
|
||||
retryTmpError :: CM () -> GroupId -> RelayRequestData -> Int64 -> ChatError -> CM ()
|
||||
retryTmpError loop groupId RelayRequestData {relayRequestRetries, relayRequestCreatedAt} delay = \case
|
||||
ChatErrorAgent {agentError} | temporaryOrHostError agentError -> do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
if relayRequestRetries >= 10 && diffUTCTime currentTs relayRequestCreatedAt > nominalDay
|
||||
then withStore' $ \db -> markRelayRequestFailed db groupId
|
||||
else do
|
||||
withStore' $ \db -> updateRelayRequestRetries db groupId delay
|
||||
loop
|
||||
e -> do
|
||||
withStore' $ \db -> setRelayRequestErr db groupId (tshow e)
|
||||
eToView e
|
||||
```
|
||||
|
||||
Key changes from current code:
|
||||
- `withRetryInterval` → `withRetryIntervalCount` wrapped in local `withRetryIntervalLimit`
|
||||
- Resume from stored delay via `ri'` (XFTP pattern)
|
||||
- `retryTmpError` receives the full `RelayRequestData` record and destructures the fields it needs
|
||||
- On temp error: checks if request is older than 1 day with 10+ retries — if so, marks as failed instead of retrying; otherwise increments retries and calls `loop`
|
||||
- After `maxConsecutiveRetries` (3), the `when` guard exits, worker picks next item
|
||||
|
||||
---
|
||||
|
||||
## Files to modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs` | New migration |
|
||||
| `src/Simplex/Chat/Store/SQLite/Migrations.hs` | Register migration |
|
||||
| `src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs` | New migration |
|
||||
| `src/Simplex/Chat/Store/Postgres/Migrations.hs` | Register migration |
|
||||
| `simplex-chat.cabal` | Add migration modules |
|
||||
| `src/Simplex/Chat/Types.hs` | Add `relayRequestDelay`, `relayRequestRetries`, `relayRequestCreatedAt` to `RelayRequestData` |
|
||||
| `src/Simplex/Chat/Store/RelayRequests.hs` | Retry ordering, `updateRelayRequestRetries` |
|
||||
| `src/Simplex/Chat/Library/Subscriber.hs` | Limited retry with delay storage, expiry check in `retryTmpError` |
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Build**: `cabal build --ghc-options=-O0`
|
||||
2. **Run relay tests**: `cabal test simplex-chat-test --test-options='-m "relay"'`
|
||||
3. **Scenarios**:
|
||||
- Request to unreachable server: retried 3 times per cycle, pushed to back of queue, marked failed after 1 day and 10+ retries
|
||||
- Request to reachable server: succeeds on first attempt, unaffected by changes
|
||||
- Multiple pending requests: stuck request doesn't block others — items with fewer retries processed first
|
||||
- App restart with expired pending requests: worker starts, picks up expired request, attempts it — if it succeeds (server now reachable), completes normally; if it fails, `retryTmpError` marks it failed
|
||||
|
||||
## Known considerations
|
||||
|
||||
1. **Single stuck item re-pickup**: If only one request is pending and it's stuck, the worker picks it up repeatedly (3 retries each cycle, immediate re-pickup). This is acceptable — backoff grows via stored delay, and the request is marked failed after 1 day and 10+ retries. The main protection is that other requests aren't blocked.
|
||||
|
||||
2. **`hasPendingRelayRequests` unchanged**: Expired requests still match the `hasPendingRelayRequests` query at startup, so the worker starts. It picks them up, attempts processing — if the server became reachable, the request succeeds normally. If it fails, `retryTmpError` checks the expiry condition and marks it failed. This is strictly better than filtering at query time: expired items get one last chance.
|
||||
|
||||
3. **Delay resumption across pickups**: Stored delay resumes backoff at the last level (XFTP pattern). After many cycles, delay reaches `maxInterval` and stays there. This means retry frequency stabilizes at a low rate for stuck items.
|
||||
|
||||
4. **Permanent errors unchanged**: Non-temp errors (validation, logic) still call `setRelayRequestErr` immediately, permanently excluding the item. The retry mechanism only affects `temporaryOrHostError`.
|
||||
|
||||
5. **`withWork_` re-signals work**: After the action returns (hitting max consecutive retries), `withWork_` has already called `hasWork` (re-signaling the doWork TMVar). The outer `forever` loop immediately proceeds to the next iteration. This is the desired behavior — the worker processes all pending items before waiting.
|
||||
|
||||
6. **`retries` count is from pickup time**: The `relayRequestRetries` value in `retryTmpError` is the count loaded when the item was picked up. Within a single pickup cycle (up to 3 consecutive retries), `updateRelayRequestRetries` increments the DB count but the local value stays the same. The expiry check uses the pickup-time count, which is at most 3 behind the DB. This is acceptable — the threshold (10) has margin.
|
||||
|
||||
7. **Migration column defaults**: `relay_request_retries NOT NULL DEFAULT 0` ensures existing pending requests start with 0 retries. `relay_request_delay` is nullable (NULL = use default reconnectInterval), matching the `Maybe Int64` field.
|
||||
@@ -38,6 +38,28 @@
|
||||
</description>
|
||||
|
||||
<releases>
|
||||
<release version="6.5.0" date="2026-04-30">
|
||||
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
|
||||
<description>
|
||||
<p>New in v6.5.</p>
|
||||
<p>Public channels - speak freely!</p>
|
||||
<ul>
|
||||
<li>Reliability: many relays per channel.</li>
|
||||
<li>Ownership: you can run your own relays.</li>
|
||||
<li>Security: owners hold channel keys.</li>
|
||||
<li>Privacy: for owners and subscribers.</li>
|
||||
</ul>
|
||||
<p>Easier to invite your friends: we made connecting simpler for new users.</p>
|
||||
<p>Safe web links:</p>
|
||||
<ul>
|
||||
<li>opt-in to send link previews.</li>
|
||||
<li>use SOCKS proxy for previews (if enabled).</li>
|
||||
<li>prevent hyperlink phishing.</li>
|
||||
<li>remove link tracking.</li>
|
||||
</ul>
|
||||
<p>Non-profit governance: to make SimpleX Network last.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.4.11" date="2026-03-30">
|
||||
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
|
||||
<description>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."ba6af65c547cf941af0a1d1645188f7b7f234de1" = "0sqdj0yawjvgqf92vm0jzzckbi9b2xh8wl8j4ygxikg3d919ksdh";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."1f173abf6d6fccb617be1e7994629c405983c431" = "1myfs7yi8bmbrzapbhz6rmvxknpdzv6rxyypg811mhsw7rfphn65";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
+3
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.5.0.19
|
||||
version: 6.5.1.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -130,6 +130,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -282,6 +283,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
@@ -26,7 +27,7 @@ import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Clock (getCurrentTime, nominalDay)
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Operators
|
||||
@@ -42,6 +43,7 @@ import Simplex.Chat.Util (shuffle)
|
||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg)
|
||||
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew))
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
@@ -115,6 +117,8 @@ defaultChatConfig =
|
||||
deliveryWorkerDelay = 0,
|
||||
deliveryBucketSize = 10000,
|
||||
channelSubscriberRole = GRObserver,
|
||||
relayRequestRetryInterval = RetryInterval {initialInterval = 5_000000, increaseAfter = 0, maxInterval = 600_000000},
|
||||
relayRequestExpiry = (10, nominalDay),
|
||||
deviceNameForRemote = "",
|
||||
remoteCompression = True,
|
||||
chatHooks = defaultChatHooks
|
||||
|
||||
@@ -73,6 +73,7 @@ import Simplex.Messaging.Agent (AgentClient, DatabaseDiff, SubscriptionsInfo)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg, Worker)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority)
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration)
|
||||
@@ -158,6 +159,8 @@ data ChatConfig = ChatConfig
|
||||
deliveryWorkerDelay :: Int64, -- microseconds
|
||||
deliveryBucketSize :: Int,
|
||||
channelSubscriberRole :: GroupMemberRole, -- TODO [relays] starting role should be communicated in protocol from owner to relays
|
||||
relayRequestRetryInterval :: RetryInterval,
|
||||
relayRequestExpiry :: (Int, NominalDiffTime),
|
||||
highlyAvailable :: Bool,
|
||||
deviceNameForRemote :: Text,
|
||||
remoteCompression :: Bool,
|
||||
|
||||
@@ -37,7 +37,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime)
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime)
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as V4
|
||||
import Data.Word (Word32)
|
||||
@@ -77,7 +77,7 @@ import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, wai
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
|
||||
import Simplex.Messaging.Agent.RetryInterval (withRetryInterval)
|
||||
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Client (NetworkRequestMode (..), ProxyClientError (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -94,8 +94,9 @@ import Simplex.Messaging.Transport (TransportError (..))
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import qualified System.FilePath as FP
|
||||
import System.Mem.Weak (Weak)
|
||||
import Text.Read (readMaybe)
|
||||
import UnliftIO.Concurrent (forkIO)
|
||||
import UnliftIO.Concurrent (ThreadId, forkIO, mkWeakThreadId)
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -1492,7 +1493,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason
|
||||
xGrpRelayInv :: InvitationId -> VersionRangeChat -> GroupRelayInvitation -> CM ()
|
||||
xGrpRelayInv invId chatVRange groupRelayInv = do
|
||||
(_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange
|
||||
initialDelay <- asks $ initialInterval . relayRequestRetryInterval . config
|
||||
(_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay
|
||||
lift $ void $ getRelayRequestWorker True
|
||||
xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM ()
|
||||
xGrpRelayTest invId chatVRange challenge = do
|
||||
@@ -3710,23 +3712,55 @@ runRelayRequestWorker a Worker {doWork} = do
|
||||
user <- getRelayUser db
|
||||
UserContactLink {userContactLinkId} <- getUserAddress db user
|
||||
pure (user, userContactLinkId)
|
||||
delayThreads <- liftIO TM.emptyIO
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
runRelayRequestOperation vr user uclId
|
||||
runRelayRequestOperation delayThreads vr user uclId
|
||||
where
|
||||
runRelayRequestOperation :: VersionRangeChat -> User -> Int64 -> CM ()
|
||||
runRelayRequestOperation vr user uclId =
|
||||
withWork_ a doWork (withStore' getNextPendingRelayRequest) $
|
||||
runRelayRequestOperation :: TM.TMap GroupId (TMVar (Weak ThreadId)) -> VersionRangeChat -> User -> Int64 -> CM ()
|
||||
runRelayRequestOperation delayThreads vr user uclId =
|
||||
withWork_ a doWork getReadyRelayRequest $
|
||||
\(groupId, rrd) -> do
|
||||
ri <- asks $ reconnectInterval . agentConfig . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended a
|
||||
liftIO $ waitForUserNetwork a
|
||||
processRelayRequest groupId rrd `catchAllErrors` retryTmpError loop groupId
|
||||
ChatConfig {relayRequestExpiry} <- asks config
|
||||
liftIO $ waitWhileSuspended a
|
||||
liftIO $ waitForUserNetwork a
|
||||
processRelayRequest groupId rrd `catchAllErrors` retryTmpError relayRequestExpiry groupId rrd
|
||||
where
|
||||
retryTmpError :: CM () -> GroupId -> ChatError -> CM ()
|
||||
retryTmpError loop groupId = \case
|
||||
ChatErrorAgent {agentError} | temporaryOrHostError agentError -> loop
|
||||
getReadyRelayRequest :: CM (Either StoreError (Maybe (GroupId, RelayRequestData)))
|
||||
getReadyRelayRequest =
|
||||
withStore' getNextPendingRelayRequest >>= \case
|
||||
Right (Just (groupId, rrd@RelayRequestData {reqExecuteAt})) -> do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let delay = diffUTCTime reqExecuteAt currentTs
|
||||
if delay <= 1
|
||||
then pure $ Right (Just (groupId, rrd))
|
||||
else Right Nothing <$ scheduleRequest groupId delay
|
||||
r -> pure r
|
||||
scheduleRequest :: GroupId -> NominalDiffTime -> CM ()
|
||||
scheduleRequest groupId delay = do
|
||||
v_ <- liftIO $ atomically $
|
||||
ifM
|
||||
(isNothing <$> TM.lookup groupId delayThreads)
|
||||
(newEmptyTMVar >>= \v -> TM.insert groupId v delayThreads $> Just v)
|
||||
(pure Nothing)
|
||||
forM_ v_ $ \v -> do
|
||||
tId <- liftIO $ forkIO $ do
|
||||
threadDelay' $ diffToMicroseconds delay
|
||||
atomically $ TM.delete groupId delayThreads
|
||||
void $ atomically $ tryPutTMVar doWork ()
|
||||
weakTId <- liftIO $ mkWeakThreadId tId
|
||||
liftIO $ atomically $ putTMVar v weakTId
|
||||
retryTmpError :: (Int, NominalDiffTime) -> GroupId -> RelayRequestData -> ChatError -> CM ()
|
||||
retryTmpError (retriesThreshold, ttl) groupId RelayRequestData {reqDelay, reqRetries, reqCreatedAt} = \case
|
||||
ChatErrorAgent {agentError} | temporaryOrHostError agentError -> do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
if reqRetries >= retriesThreshold && diffUTCTime currentTs reqCreatedAt >= ttl
|
||||
then withStore' $ \db -> setRelayRequestErr db groupId "expired"
|
||||
else do
|
||||
ri <- asks $ relayRequestRetryInterval . config
|
||||
let executeAt = addUTCTime (fromIntegral reqDelay / 1000000) currentTs
|
||||
nextDelay = nextRetryDelay 0 reqDelay ri
|
||||
withStore' $ \db -> updateRelayRequestRetries db groupId nextDelay executeAt
|
||||
e -> do
|
||||
withStore' $ \db -> setRelayRequestErr db groupId (tshow e)
|
||||
eToView e
|
||||
|
||||
@@ -93,7 +93,8 @@ disabledSimplexChatSMPServers =
|
||||
simplexChatRelays :: [NewUserChatRelay]
|
||||
simplexChatRelays =
|
||||
[ presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 1" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp5.simplex.im/r#Fp5RWXkiRFg-hgcDwC2v-MWnPfvEf42RgCqREntW0mw"),
|
||||
presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 2" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ")
|
||||
presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 2" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ"),
|
||||
presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 3" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp4.simplex.im/r#yxNOMJcry5jMTRPEBVtGBATYaKeoRIsZRBPIDLx7x6M")
|
||||
]
|
||||
|
||||
fluxSMPServers :: [NewUserServer 'PSMP]
|
||||
|
||||
@@ -1515,8 +1515,8 @@ setGroupInProgressDone db GroupInfo {groupId} = do
|
||||
"UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?"
|
||||
(currentTs, groupId)
|
||||
|
||||
createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||
createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange = do
|
||||
createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||
createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
-- Create group with placeholder profile
|
||||
let Profile {displayName = fromMemberLDN} = fromMemberProfile
|
||||
@@ -1532,7 +1532,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe
|
||||
}
|
||||
(groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs
|
||||
-- Store relay request data for recovery
|
||||
liftIO $ setRelayRequestData_ groupId
|
||||
liftIO $ setRelayRequestData_ groupId currentTs
|
||||
ownerMemberId <- insertOwner_ currentTs groupId
|
||||
let relayMember = MemberIdRole relayMemberId GRRelay
|
||||
-- TODO [member keys] should relays use member keys?
|
||||
@@ -1541,7 +1541,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe
|
||||
g <- getGroupInfo db vr user groupId
|
||||
pure (g, ownerMember)
|
||||
where
|
||||
setRelayRequestData_ groupId =
|
||||
setRelayRequestData_ groupId currentTs =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
@@ -1549,10 +1549,12 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe
|
||||
SET relay_request_inv_id = ?,
|
||||
relay_request_group_link = ?,
|
||||
relay_request_peer_chat_min_version = ?,
|
||||
relay_request_peer_chat_max_version = ?
|
||||
relay_request_peer_chat_max_version = ?,
|
||||
relay_request_delay = ?,
|
||||
relay_request_execute_at = ?
|
||||
WHERE group_id = ?
|
||||
|]
|
||||
(Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, groupId)
|
||||
(Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, initialDelay, currentTs, groupId)
|
||||
insertOwner_ currentTs groupId = do
|
||||
let MemberIdRole {memberId, memberRole} = fromMember
|
||||
VersionRange minV maxV = reqChatVRange
|
||||
|
||||
@@ -28,6 +28,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -55,7 +56,8 @@ schemaMigrations =
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed)
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260429_relay_request_retries :: Text
|
||||
m20260429_relay_request_retries =
|
||||
[r|
|
||||
ALTER TABLE groups ADD COLUMN relay_request_retries BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE groups ADD COLUMN relay_request_delay BIGINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE groups ADD COLUMN relay_request_execute_at TIMESTAMPTZ NOT NULL DEFAULT (now());
|
||||
|]
|
||||
|
||||
down_m20260429_relay_request_retries :: Text
|
||||
down_m20260429_relay_request_retries =
|
||||
[r|
|
||||
ALTER TABLE groups DROP COLUMN relay_request_retries;
|
||||
ALTER TABLE groups DROP COLUMN relay_request_delay;
|
||||
ALTER TABLE groups DROP COLUMN relay_request_execute_at;
|
||||
|]
|
||||
@@ -959,7 +959,10 @@ CREATE TABLE test_chat_schema.groups (
|
||||
root_priv_key bytea,
|
||||
root_pub_key bytea,
|
||||
member_priv_key bytea,
|
||||
public_member_count bigint
|
||||
public_member_count bigint,
|
||||
relay_request_retries bigint DEFAULT 0 NOT NULL,
|
||||
relay_request_delay bigint DEFAULT 0 NOT NULL,
|
||||
relay_request_execute_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
module Simplex.Chat.Store.RelayRequests
|
||||
( hasPendingRelayRequests,
|
||||
getNextPendingRelayRequest,
|
||||
updateRelayRequestRetries,
|
||||
setRelayRequestErr,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import Simplex.Chat.Store.Shared
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
@@ -64,7 +66,7 @@ getNextPendingRelayRequest db =
|
||||
WHERE relay_own_status = ?
|
||||
AND relay_request_failed = 0
|
||||
AND relay_request_err_reason IS NULL
|
||||
ORDER BY group_id ASC
|
||||
ORDER BY relay_request_execute_at ASC
|
||||
LIMIT 1
|
||||
|]
|
||||
(Only RSInvited)
|
||||
@@ -76,18 +78,27 @@ getNextPendingRelayRequest db =
|
||||
[sql|
|
||||
SELECT
|
||||
relay_request_inv_id, relay_request_group_link,
|
||||
relay_request_peer_chat_min_version, relay_request_peer_chat_max_version
|
||||
relay_request_peer_chat_min_version, relay_request_peer_chat_max_version,
|
||||
relay_request_delay, relay_request_retries, created_at, relay_request_execute_at
|
||||
FROM groups
|
||||
WHERE group_id = ?
|
||||
|]
|
||||
(Only groupId)
|
||||
where
|
||||
toRelayRequestData :: (Maybe InvitationId, Maybe ShortLinkContact, Maybe VersionChat, Maybe VersionChat) -> Either StoreError (GroupId, RelayRequestData)
|
||||
toRelayRequestData :: (Maybe InvitationId, Maybe ShortLinkContact, Maybe VersionChat, Maybe VersionChat, Int64, Int, UTCTime, UTCTime) -> Either StoreError (GroupId, RelayRequestData)
|
||||
toRelayRequestData = \case
|
||||
(Just relayInvId, Just reqGroupLink, Just minV, Just maxV) ->
|
||||
Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV})
|
||||
(Just relayInvId, Just reqGroupLink, Just minV, Just maxV, reqDelay, reqRetries, reqCreatedAt, reqExecuteAt) ->
|
||||
Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV, reqDelay, reqRetries, reqCreatedAt, reqExecuteAt})
|
||||
_ -> Left $ SEInternalError "missing relay request data"
|
||||
|
||||
updateRelayRequestRetries :: DB.Connection -> GroupId -> Int64 -> UTCTime -> IO ()
|
||||
updateRelayRequestRetries db groupId delay executeAt = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE groups SET relay_request_retries = relay_request_retries + 1, relay_request_delay = ?, relay_request_execute_at = ?, updated_at = ? WHERE group_id = ?"
|
||||
(delay, executeAt, currentTs, groupId)
|
||||
|
||||
markRelayRequestFailed :: DB.Connection -> GroupId -> IO ()
|
||||
markRelayRequestFailed db groupId = do
|
||||
currentTs <- getCurrentTime
|
||||
|
||||
@@ -151,6 +151,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -301,7 +302,8 @@ schemaMigrations =
|
||||
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices),
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed)
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260429_relay_request_retries :: Query
|
||||
m20260429_relay_request_retries =
|
||||
[sql|
|
||||
ALTER TABLE groups ADD COLUMN relay_request_retries INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE groups ADD COLUMN relay_request_delay INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE groups ADD COLUMN relay_request_execute_at TEXT NOT NULL DEFAULT(datetime('now'));
|
||||
|]
|
||||
|
||||
down_m20260429_relay_request_retries :: Query
|
||||
down_m20260429_relay_request_retries =
|
||||
[sql|
|
||||
ALTER TABLE groups DROP COLUMN relay_request_retries;
|
||||
ALTER TABLE groups DROP COLUMN relay_request_delay;
|
||||
ALTER TABLE groups DROP COLUMN relay_request_execute_at;
|
||||
|]
|
||||
@@ -695,7 +695,8 @@ SEARCH delivery_jobs USING INTEGER PRIMARY KEY (rowid=?)
|
||||
Query:
|
||||
SELECT
|
||||
relay_request_inv_id, relay_request_group_link,
|
||||
relay_request_peer_chat_min_version, relay_request_peer_chat_max_version
|
||||
relay_request_peer_chat_min_version, relay_request_peer_chat_max_version,
|
||||
relay_request_delay, relay_request_retries, created_at, relay_request_execute_at
|
||||
FROM groups
|
||||
WHERE group_id = ?
|
||||
|
||||
@@ -993,11 +994,12 @@ Query:
|
||||
WHERE relay_own_status = ?
|
||||
AND relay_request_failed = 0
|
||||
AND relay_request_err_reason IS NULL
|
||||
ORDER BY group_id ASC
|
||||
ORDER BY relay_request_execute_at ASC
|
||||
LIMIT 1
|
||||
|
||||
Plan:
|
||||
SCAN groups
|
||||
USE TEMP B-TREE FOR ORDER BY
|
||||
|
||||
Query:
|
||||
SELECT i.chat_item_id
|
||||
@@ -1775,7 +1777,9 @@ Query:
|
||||
SET relay_request_inv_id = ?,
|
||||
relay_request_group_link = ?,
|
||||
relay_request_peer_chat_min_version = ?,
|
||||
relay_request_peer_chat_max_version = ?
|
||||
relay_request_peer_chat_max_version = ?,
|
||||
relay_request_delay = ?,
|
||||
relay_request_execute_at = ?
|
||||
WHERE group_id = ?
|
||||
|
||||
Plan:
|
||||
|
||||
@@ -173,7 +173,10 @@ CREATE TABLE groups(
|
||||
root_priv_key BLOB,
|
||||
root_pub_key BLOB,
|
||||
member_priv_key BLOB,
|
||||
public_member_count INTEGER, -- received
|
||||
public_member_count INTEGER,
|
||||
relay_request_retries INTEGER NOT NULL DEFAULT 0,
|
||||
relay_request_delay INTEGER NOT NULL DEFAULT 0,
|
||||
relay_request_execute_at TEXT NOT NULL DEFAULT(datetime('now')), -- received
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
|
||||
@@ -1045,7 +1045,11 @@ data GroupMember = GroupMember
|
||||
data RelayRequestData = RelayRequestData
|
||||
{ relayInvId :: InvitationId,
|
||||
reqGroupLink :: ShortLinkContact,
|
||||
reqChatVRange :: VersionRangeChat
|
||||
reqChatVRange :: VersionRangeChat,
|
||||
reqDelay :: Int64,
|
||||
reqRetries :: Int,
|
||||
reqCreatedAt :: UTCTime,
|
||||
reqExecuteAt :: UTCTime
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
|
||||
@@ -235,7 +235,6 @@ chatGroupTests = do
|
||||
it "should respect support preference in channel" testSupportPreferenceChannel
|
||||
-- TODO [relays] add tests for channels
|
||||
-- TODO - tests with delivery loop over members restored after restart
|
||||
-- TODO - delivery in support scopes inside channels
|
||||
-- TODO - connect plans for relay groups
|
||||
-- TODO - cancellation on failure to create relay group (for owner)
|
||||
-- TODO - async retry connecting to relay (for members)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const markdownIt = require("markdown-it")
|
||||
const markdownItAnchor = require("markdown-it-anchor")
|
||||
const markdownItReplaceLink = require('markdown-it-replace-link')
|
||||
const markdownItFootnote = require('markdown-it-footnote')
|
||||
const slugify = require("slugify")
|
||||
const uri = require('fast-uri')
|
||||
const i18n = require('eleventy-plugin-i18n')
|
||||
@@ -438,6 +439,38 @@ module.exports = function (ty) {
|
||||
strict: true,
|
||||
})
|
||||
}).use(markdownItReplaceLink)
|
||||
.use(markdownItFootnote)
|
||||
|
||||
markdownLib.renderer.rules.footnote_anchor_name = function (tokens, idx, options, env) {
|
||||
var token = tokens[idx]
|
||||
var label = token.meta.label
|
||||
if (label) return label
|
||||
var n = Number(token.meta.id + 1).toString()
|
||||
var prefix = typeof env.docId === 'string' ? '-' + env.docId + '-' : ''
|
||||
return prefix + n
|
||||
}
|
||||
markdownLib.renderer.rules.footnote_caption = function (tokens, idx) {
|
||||
var n = Number(tokens[idx].meta.id + 1).toString()
|
||||
if (tokens[idx].meta.subId > 0) n += ':' + tokens[idx].meta.subId
|
||||
return n
|
||||
}
|
||||
markdownLib.renderer.rules.footnote_ref = function (tokens, idx, options, env, slf) {
|
||||
var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
|
||||
var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf)
|
||||
var refid = id
|
||||
if (tokens[idx].meta.subId > 0) refid += ':' + tokens[idx].meta.subId
|
||||
return '<sup class="footnote-ref"><a href="#note-' + id + '" id="ref-' + refid + '">' + caption + '</a></sup>'
|
||||
}
|
||||
markdownLib.renderer.rules.footnote_open = function (tokens, idx, options, env, slf) {
|
||||
var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
|
||||
if (tokens[idx].meta.subId > 0) id += ':' + tokens[idx].meta.subId
|
||||
return '<li id="note-' + id + '" class="footnote-item">'
|
||||
}
|
||||
markdownLib.renderer.rules.footnote_anchor = function (tokens, idx, options, env, slf) {
|
||||
var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf)
|
||||
if (tokens[idx].meta.subId > 0) id += ':' + tokens[idx].meta.subId
|
||||
return ' <a href="#ref-' + id + '" class="footnote-backref">↩︎</a>'
|
||||
}
|
||||
|
||||
// replace the default markdown-it instance
|
||||
ty.setLibrary("md", markdownLib)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsdom": "^22.1.0",
|
||||
"lottie-web": "5.12.2",
|
||||
"markdown-it": "^13.0.1"
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-footnote": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<p class="mb-[12px]">Freedom of speech needs infrastructure that protects it by design — protocols, governance and funding.</p>
|
||||
|
||||
<p class="mb-[12px]"><strong>v6.5 release</strong> brings SimpleX Channels: a new model for online publishing built for participation privacy.</p>
|
||||
@@ -52,12 +52,15 @@ active_blog: true
|
||||
<div class="min-h-[inherit] h-full w-full flex items-end px-4 pt-4 justify-center relative">
|
||||
{% if blog.data.image %}
|
||||
{% if blog.data.imageBottom %}
|
||||
<img class="w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
<img class="w-full max-w-[240px] h-auto{% if blog.data.imageLight %} dark:hidden{% endif %}" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
{% if blog.data.imageLight %}<img class="w-full max-w-[240px] h-auto hidden dark:inline-block" src="{{ blog.data.imageLight }}" alt="" srcset="" />{% endif %}
|
||||
{% elif blog.data.imageWide %}
|
||||
<img class="mb-4 self-center w-full h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
<img class="mb-4 self-center w-full h-auto{% if blog.data.imageLight %} dark:hidden{% endif %}" src="{{ blog.data.image }}" alt="" srcset="" />
|
||||
{% if blog.data.imageLight %}<img class="mb-4 self-center w-full h-auto hidden dark:inline-block" src="{{ blog.data.imageLight }}" alt="" srcset="" />{% endif %}
|
||||
{% else %}
|
||||
<img class="mb-4 self-center w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt=""
|
||||
<img class="mb-4 self-center w-full max-w-[240px] h-auto{% if blog.data.imageLight %} dark:hidden{% endif %}" src="{{ blog.data.image }}" alt=""
|
||||
srcset="" />
|
||||
{% if blog.data.imageLight %}<img class="mb-4 self-center w-full max-w-[240px] h-auto hidden dark:inline-block" src="{{ blog.data.imageLight }}" alt="" srcset="" />{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img class="h-[44px] self-center dark:hidden" src="/img/new/logo-symbol-light.svg" alt=""
|
||||
|
||||
@@ -296,4 +296,30 @@ h3::before {
|
||||
|
||||
.dark #article th {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.footnotes-list {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.footnotes-sep {
|
||||
margin: 2.4rem 0 1.2rem;
|
||||
}
|
||||
|
||||
.footnotes-list > li {
|
||||
list-style-position: outside !important;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.footnotes-list > li::marker {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.footnotes-list > li > p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#article .footnotes-list .footnote-backref {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -35,10 +35,12 @@ async function initDirectory() {
|
||||
mode = 'live';
|
||||
comparator = byActiveAtDesc;
|
||||
btn = liveBtn;
|
||||
break;
|
||||
case '#new':
|
||||
mode = 'new';
|
||||
comparator = byCreatedAtDesc;
|
||||
btn = newBtn;
|
||||
break;
|
||||
default:
|
||||
mode = 'top';
|
||||
comparator = bySortPriority;
|
||||
|
||||
Reference in New Issue
Block a user