mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-12 05:18:57 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -57,7 +57,7 @@ func openBrowserAlert(uri: String) {
|
||||
cancelAlertAction,
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Open full link", comment: "alert action"),
|
||||
style: .destructive,
|
||||
style: .default,
|
||||
handler: { _ in UIApplication.shared.open(url.uri) }
|
||||
),
|
||||
UIAlertAction(
|
||||
|
||||
@@ -353,7 +353,7 @@ struct ComposeView: View {
|
||||
@UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false
|
||||
@State private var updatingCompose = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -14,7 +14,7 @@ struct PrivacySettings: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
||||
|
||||
@@ -98,7 +98,7 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false,
|
||||
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true,
|
||||
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT: true,
|
||||
GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: true,
|
||||
GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: false,
|
||||
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ class AppPreferences {
|
||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||
val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true)
|
||||
val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, true)
|
||||
val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, false)
|
||||
// TODO remove
|
||||
val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } }
|
||||
val simplexLinkMode: SharedPreference<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default)
|
||||
|
||||
+3
-1
@@ -43,6 +43,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import kotlin.math.min
|
||||
|
||||
const val MAX_NUMBER_OF_MENTIONS = 3
|
||||
|
||||
@@ -907,7 +908,8 @@ fun ComposeView(
|
||||
if (sanitizedPos == null) {
|
||||
composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage)
|
||||
} else {
|
||||
val message = if (sanitizedPos < s.selection.start) s.copy(text = updatedMsg) else ComposeMessage(updatedMsg, TextRange(sanitizedPos, sanitizedPos))
|
||||
val pos = min(updatedMsg.count(), if (sanitizedPos < s.selection.start) s.selection.start else sanitizedPos)
|
||||
val message = s.copy(text = updatedMsg, selection = TextRange(pos))
|
||||
composeState.value = composeState.value.copy(message = message, parsedMessage = updatedParsedMsg)
|
||||
parsedMessage = updatedParsedMsg
|
||||
}
|
||||
|
||||
+1
-1
@@ -391,7 +391,7 @@ fun openBrowserAlert(uri: String, uriHandler: UriHandler) {
|
||||
AlertManager.shared.hideAlert()
|
||||
safeOpenUri(uri, uriHandler)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.privacy_chat_list_open_full_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
Text(generalGetString(MR.strings.privacy_chat_list_open_full_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Channels
|
||||
|
||||
This supersedes the previous [super-peers RFC](./2025-02-13-super-peer-groups-mvp.md), by reducing its scope to channels.
|
||||
|
||||
## Problem
|
||||
|
||||
This doc proposes the scope for MVP for channels based on chat relays, as the first step to arbitrary large groups based on chat relays.
|
||||
|
||||
The technical difference of channels is that only owners can send messages and all members are "observers" who can send reactions.
|
||||
|
||||
From UX point of view, channels will be a separate thing, as one more action in the menu.
|
||||
|
||||
From threat model point of view, we want to protect from chat relays being able to act as owners - channel "identity" will be based on owners keys, as described in [this rfc](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2025-04-04-short-links-for-groups.md).
|
||||
|
||||
## Solution
|
||||
|
||||
Minimal testable scope:
|
||||
- protocol for managing owners changes and channel profile changes - probably based on some consensus protocol.
|
||||
- message signing by owners - to authorize channel profile and ownership changes.
|
||||
- make group link part of group profile, while delegating connection to channel to chat relays included in link data.
|
||||
- messages sent from channel name.
|
||||
- message forwarding by chat relays:
|
||||
- all relays forward each message, client de-duplicates by message ID and highlights any differences between chat relays.
|
||||
- forwarding operation should be persistent, resumable and batched, optimizing between latency and load.
|
||||
|
||||
Channels MVP implementation scope:
|
||||
- split directory service from chat relay.
|
||||
- message services to allow for fast subscriptions of chat relays to SMP servers using service certificates. It is in progress in [this PR](https://github.com/simplex-chat/simplexmq/pull/1565).
|
||||
- fully migrate SMP servers to PostgreSQL.
|
||||
- chat relay management (started in [this PR](https://github.com/simplex-chat/simplex-chat/pull/5653)), include chat relays into pre-configured operators.
|
||||
- catch up with skipped messages (related to history navigation).
|
||||
- manage chat relays in UI.
|
||||
- member keys, for owners, profile signatures and for establishing e2e encryption in chat with admins. Possibly, profile keys as described in [this RFC](./2025-04-14-signing-messages.md) (option 2).
|
||||
- reuploading files by relays for indefinite storage and maintaining channel quotas for files.
|
||||
- operator content moderation.
|
||||
- UI changes for channel/group creation.
|
||||
|
||||
Post MVP:
|
||||
- pagination in member list.
|
||||
- history "navigation" - request older messages.
|
||||
- e2e encryption in chats with admins.
|
||||
- message revocation from history by senders.
|
||||
- message comments.
|
||||
+2
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.4.3.0
|
||||
version: 6.4.3.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -587,6 +587,7 @@ test-suite simplex-chat-test
|
||||
, time ==1.12.*
|
||||
, unicode-transforms ==0.4.*
|
||||
, unliftio ==0.2.*
|
||||
, uri-bytestring >=0.3.3.1 && <0.4
|
||||
default-language: Haskell2010
|
||||
if flag(client_postgres)
|
||||
build-depends:
|
||||
|
||||
@@ -20,6 +20,7 @@ import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isAlpha, isAscii, isDigit, isPunctuation, isSpace)
|
||||
import Data.Either (fromRight)
|
||||
@@ -328,7 +329,7 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
strEncodeText :: StrEncoding a => a -> Text
|
||||
strEncodeText = safeDecodeUtf8 . strEncode
|
||||
|
||||
parseUri :: B.ByteString -> Either Text U.URI
|
||||
parseUri :: ByteString -> Either Text U.URI
|
||||
parseUri s = case U.parseURI U.laxURIParserOptions s of
|
||||
Left e -> Left $ "Invalid URI: " <> tshow e
|
||||
Right uri@U.URI {uriScheme = U.Scheme sch, uriAuthority}
|
||||
@@ -339,12 +340,54 @@ parseUri s = case U.parseURI U.laxURIParserOptions s of
|
||||
| '.' `B.notElem` h -> Left $ "Invalid URI host: " <> safeDecodeUtf8 h
|
||||
| otherwise -> Right uri
|
||||
|
||||
-- the heuristic for removing tracking parameters is the following:
|
||||
-- 1) if the URI path looks like page name* rather than an identifier, allow the first parameter, as long as it is not blacklisted,
|
||||
-- 2) also allow whitelisted parameters,
|
||||
-- 3) remove all other parameters.
|
||||
-- *page name: lowercase latin in snake-case or hyphen-case, allowing for sinlge leading or trailing hyphen or underscore.
|
||||
sanitizeUri :: U.URI -> Maybe U.URI
|
||||
sanitizeUri uri@U.URI {uriQuery = U.Query originalQS} =
|
||||
let sanitizedQS = filter (\(p, _) -> p == "q" || p == "search") originalQS
|
||||
sanitizeUri uri@U.URI {uriAuthority, uriPath, uriQuery = U.Query originalQS} =
|
||||
let sanitizedQS
|
||||
| isNamePath = case originalQS of
|
||||
p : ps -> (if isBlacklisted (fst p) then id else (p :)) $ filter (isWhitelisted . fst) ps
|
||||
[] -> []
|
||||
| otherwise = filter (isWhitelisted . fst) originalQS
|
||||
in if length sanitizedQS == length originalQS
|
||||
then Nothing
|
||||
else Just $ uri {U.uriQuery = U.Query sanitizedQS}
|
||||
where
|
||||
isBlacklisted p = any ($ p) qsBlacklist
|
||||
isWhitelisted p = any (\(f, ps) -> f host && p `elem` ps) qsWhitelist
|
||||
host = maybe "" (\U.Authority {authorityHost = U.Host h} -> h) uriAuthority
|
||||
isNamePath = B.all (\c -> (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '/') uriPath
|
||||
qsWhitelist :: [(ByteString -> Bool, [ByteString])]
|
||||
qsWhitelist =
|
||||
[ (const True, ["q", "search"]),
|
||||
(dom "youtube.com", ["v", "t"]),
|
||||
(dom "youtu.be", ["t"])
|
||||
]
|
||||
dom d h = d == h || (('.' `B.cons` d) `B.isSuffixOf` h)
|
||||
qsBlacklist :: [ByteString -> Bool]
|
||||
qsBlacklist =
|
||||
[ (B.any (== '_')),
|
||||
("ad" `B.isPrefixOf`),
|
||||
("af" `B.isPrefixOf`),
|
||||
("dc" `B.isPrefixOf`),
|
||||
("fb" `B.isPrefixOf`),
|
||||
("gc" `B.isPrefixOf`),
|
||||
("li" `B.isPrefixOf`),
|
||||
("ref" `B.isPrefixOf`),
|
||||
("si" `B.isPrefixOf`),
|
||||
("tw" `B.isPrefixOf`),
|
||||
("utm" `B.isPrefixOf`),
|
||||
("camp" `B.isInfixOf`),
|
||||
("cmp" `B.isInfixOf`),
|
||||
("dev" `B.isInfixOf`),
|
||||
("id" `B.isInfixOf`),
|
||||
("prom" `B.isInfixOf`),
|
||||
("source" `B.isInfixOf`),
|
||||
("src" `B.isInfixOf`)
|
||||
]
|
||||
|
||||
markdownText :: FormattedText -> Text
|
||||
markdownText (FormattedText f_ t) = case f_ of
|
||||
|
||||
@@ -11,8 +11,10 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util ((<$$>))
|
||||
import System.Console.ANSI.Types
|
||||
import Test.Hspec
|
||||
import qualified URI.ByteString as U
|
||||
|
||||
markdownTests :: Spec
|
||||
markdownTests = do
|
||||
@@ -26,6 +28,7 @@ markdownTests = do
|
||||
textWithMentions
|
||||
textWithCommands
|
||||
multilineMarkdownList
|
||||
testSanitizeUri
|
||||
|
||||
infixr 1 ==>, <==, <==>, ==>>, <<==, <<==>>
|
||||
|
||||
@@ -359,3 +362,25 @@ multilineMarkdownList = describe "multiline markdown" do
|
||||
it "command markdown" do
|
||||
"/link 1" <<==>> [command' "link 1" "/link 1"]
|
||||
" /link 1" <<==>> [command' "link 1" " /link 1"]
|
||||
|
||||
testSanitizeUri :: Spec
|
||||
testSanitizeUri = describe "sanitizeUri" $ do
|
||||
it "should allow the first parameter and whitelisted parameters on pages without IDs" $ do
|
||||
"https://example.com/page?ref=123" `sanitized` Just "https://example.com/page"
|
||||
"https://example.com/page?name" `sanitized` Nothing
|
||||
"https://example.com/page?name=abc" `sanitized` Nothing
|
||||
"https://example.com/page?name=abc&ref=123" `sanitized` Just "https://example.com/page?name=abc"
|
||||
"https://example.com/page?search=query" `sanitized` Nothing
|
||||
"https://example.com/page?q=query" `sanitized` Nothing
|
||||
"https://example.com/page?ref=123&q=query" `sanitized` Just "https://example.com/page?q=query"
|
||||
"https://youtube.com/watch?v=abc&t=123" `sanitized` Nothing
|
||||
"https://www.youtube.com/watch?v=abc" `sanitized` Nothing
|
||||
"https://www.youtube.com/watch?v=abc&t=123" `sanitized` Nothing
|
||||
"https://www.youtube.com/watch?ref=456&v=abc&t=123" `sanitized` Just "https://www.youtube.com/watch?v=abc&t=123"
|
||||
it "should only allow whitelisted parameters if path contains IDs" $ do
|
||||
"https://example.com/page/a123?name=abc" `sanitized` Just "https://example.com/page/a123"
|
||||
"https://youtu.be/a123?si=456" `sanitized` Just "https://youtu.be/a123"
|
||||
"https://youtu.be/a123?t=456" `sanitized` Nothing
|
||||
"https://youtu.be/a123?si=456&t=789" `sanitized` Just "https://youtu.be/a123?t=789"
|
||||
where
|
||||
s `sanitized` res = (U.serializeURIRef' <$$> (sanitizeUri <$> parseUri s)) `shouldBe` Right res
|
||||
|
||||
Reference in New Issue
Block a user