mobile: simplex links in UI, core: trusted uri for simplex links (#1410)

This commit is contained in:
Evgeny Poberezkin
2022-11-24 17:14:56 +00:00
committed by GitHub
parent a7345ee4d9
commit 4485d46307
13 changed files with 158 additions and 14 deletions
@@ -1491,12 +1491,21 @@ object MsgContentSerializer : KSerializer<MsgContent> {
@Serializable
class FormattedText(val text: String, val format: Format? = null) {
// TODO make it dependent on simplexLinkMode preference
val link: String? = when (format) {
is Format.Uri -> text
is Format.SimplexLink -> format.simplexUri
is Format.Email -> "mailto:$text"
is Format.Phone -> "tel:$text"
else -> null
}
// TODO make it dependent on simplexLinkMode preference
val viewText: String =
if (format is Format.SimplexLink) simplexLinkText(format.linkType, format.smpHosts) else text
fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List<String>): String =
"${linkType.description} (${String.format(generalGetString(R.string.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})"
}
@Serializable
@@ -1508,6 +1517,8 @@ sealed class Format {
@Serializable @SerialName("secret") class Secret: Format()
@Serializable @SerialName("colored") class Colored(val color: FormatColor): Format()
@Serializable @SerialName("uri") class Uri: Format()
// TODO trustedUri: Boolean
@Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List<String>): Format()
@Serializable @SerialName("email") class Email: Format()
@Serializable @SerialName("phone") class Phone: Format()
@@ -1519,6 +1530,7 @@ sealed class Format {
is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor)
is Colored -> SpanStyle(color = this.color.uiColor)
is Uri -> linkStyle
is SimplexLink -> linkStyle
is Email -> linkStyle
is Phone -> linkStyle
}
@@ -1528,6 +1540,19 @@ sealed class Format {
}
}
@Serializable
enum class SimplexLinkType(val linkType: String) {
contact("contact"),
invitation("invitation"),
group("group");
val description: String get() = generalGetString(when (this) {
contact -> R.string.simplex_link_contact
invitation -> R.string.simplex_link_invitation
group -> R.string.simplex_link_group
})
}
@Serializable
enum class FormatColor(val color: String) {
red("red"),
@@ -60,6 +60,16 @@ enum class CallOnLockScreen {
}
}
enum class SimplexLinkMode {
DESCRIPTION,
FULL,
BROWSER;
companion object {
val default = SimplexLinkMode.DESCRIPTION
}
}
class AppPreferences(val context: Context) {
private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
@@ -91,6 +101,18 @@ class AppPreferences(val context: Context) {
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, false)
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
private val _simplexLinkMode = mkStrPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default.name)
val simplexLinkMode: Preference<SimplexLinkMode> = Preference(
get = fun(): SimplexLinkMode {
val value = _simplexLinkMode.get() ?: return SimplexLinkMode.default
return try {
SimplexLinkMode.valueOf(value)
} catch (e: Error) {
SimplexLinkMode.default
}
},
set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) }
)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null)
@@ -181,6 +203,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
@@ -84,7 +84,7 @@ fun MarkdownText (
hasLinks = true
val ftStyle = ft.format.style
withAnnotation(tag = "URL", annotation = link) {
withStyle(ftStyle) { append(ft.text) }
withStyle(ftStyle) { append(ft.viewText) }
}
} else {
withStyle(ft.format.style) { append(ft.text) }
@@ -41,6 +41,12 @@
<string name="description_via_one_time_link">über einen Einmal-Link</string>
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
<!-- FormattedText, SimpleX links - ChatModel.kt -->
<string name="simplex_link_contact">SimpleX Kontaktadressen-Link</string>
<string name="simplex_link_invitation">SimpleX Einmal-Link</string>
<string name="simplex_link_group">SimpleX Gruppen-Link</string>
<string name="simplex_link_connection">über <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
@@ -41,6 +41,12 @@
<string name="description_via_one_time_link">через одноразовую ссылку</string>
<string name="description_via_one_time_link_incognito">инкогнито через одноразовую ссылку</string>
<!-- FormattedText, SimpleX links - ChatModel.kt -->
<string name="simplex_link_contact">SimpleX ссылка-контакт</string>
<string name="simplex_link_invitation">SimpleX одноразовая ссылка</string>
<string name="simplex_link_group">SimpleX ссылка группы</string>
<string name="simplex_link_connection">через <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Ошибка при сохранении SMP серверов</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Пожалуйста, проверьте, что адреса SMP серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</string>
@@ -41,6 +41,12 @@
<string name="description_via_one_time_link">via one-time link</string>
<string name="description_via_one_time_link_incognito">incognito via one-time link</string>
<!-- FormattedText, SimpleX links - ChatModel.kt -->
<string name="simplex_link_contact">SimpleX contact address</string>
<string name="simplex_link_invitation">SimpleX 1-time invitation</string>
<string name="simplex_link_group">SimpleX group link</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Error saving SMP servers</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
@@ -70,6 +70,12 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "")
case let .simplexLink(linkType, simplexUri, smpHosts):
switch privacySimplexLinkModeDefault.get() {
case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "")
case .full: return linkText(t, simplexUri, preview, prefix: "")
case .browser: return linkText(t, t, preview, prefix: "")
}
case .email: return linkText(t, t, preview, prefix: "mailto:")
case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:")
}
@@ -88,6 +94,10 @@ private func linkText(_ s: String, _ link: String,
]))).underline()
}
private func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
linkType.description + " " + "(via \(smpHosts.first ?? "?"))"
}
struct MsgContentView_Previews: PreviewProvider {
static var previews: some View {
let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
@@ -14,6 +14,7 @@ struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = false
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
var body: some View {
VStack {
@@ -21,7 +22,8 @@ struct PrivacySettings: View {
Section("Device") {
SimplexLockSetting()
}
Section("Chats") {
Section {
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
@@ -36,6 +38,23 @@ struct PrivacySettings: View {
settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews)
}
settingsRow("link") {
Picker("SimpleX links", selection: $simplexLinkMode) {
ForEach(SimpleXLinkMode.values) { mode in
Text(mode.text)
}
}
}
.frame(height: 36)
.onChange(of: simplexLinkMode) { mode in
privacySimplexLinkModeDefault.set(mode)
}
} header: {
Text("Chats")
} footer: {
if case .browser = simplexLinkMode {
Text("Opening the link in the browser may reduce connection privacy and security.")
}
}
}
}
@@ -23,6 +23,7 @@ let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
@@ -43,6 +44,7 @@ let appDefaults: [String: Any] = [
DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: "description",
DEFAULT_EXPERIMENTAL_CALLS: false,
DEFAULT_CHAT_V3_DB_MIGRATION: "offer",
DEFAULT_DEVELOPER_TOOLS: false,
@@ -54,6 +56,24 @@ let appDefaults: [String: Any] = [
DEFAULT_CONNECT_VIA_LINK_TAB: "scan"
]
enum SimpleXLinkMode: String, Identifiable {
case description
case full
case browser
static var values: [SimpleXLinkMode] = [.description, .full, .browser]
public var id: Self { self }
var text: LocalizedStringKey {
switch self {
case .description: return "Description"
case .full: return "Full link"
case .browser: return "Via browser"
}
}
}
private var indent: CGFloat = 36
let chatArchiveTimeDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
@@ -64,6 +84,8 @@ let encryptionStartedAtDefault = DateDefault(defaults: UserDefaults.standard, fo
let connectViaLinkTabDefault = EnumDefault<ConnectViaLinkTab>(defaults: UserDefaults.standard, forKey: DEFAULT_CONNECT_VIA_LINK_TAB, withDefault: .scan)
let privacySimplexLinkModeDefault = EnumDefault<SimpleXLinkMode>(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_SIMPLEX_LINK_MODE, withDefault: .description)
func setGroupDefaults() {
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
}
+16
View File
@@ -1713,10 +1713,26 @@ public enum Format: Decodable, Equatable {
case secret
case colored(color: FormatColor)
case uri
// TODO trustedUri: Bool
case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String])
case email
case phone
}
public enum SimplexLinkType: String, Decodable {
case contact
case invitation
case group
public var description: String {
switch self {
case .contact: return NSLocalizedString("SimpleX contact address", comment: "simplex link type")
case .invitation: return NSLocalizedString("SimpleX 1-time invitation", comment: "simplex link type")
case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type")
}
}
}
public enum FormatColor: String, Decodable {
case red = "red"
case green = "green"
+7 -4
View File
@@ -31,7 +31,7 @@ import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqScheme (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fstToLower, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..))
import Simplex.Messaging.Protocol (ProtocolServer (..), SrvLoc (..))
import Simplex.Messaging.Util (safeDecodeUtf8)
import System.Console.ANSI.Types
import qualified Text.Email.Validate as Email
@@ -47,7 +47,7 @@ data Format
| Secret
| Colored {color :: FormatColor}
| Uri
| SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, smpHosts :: NonEmpty Text}
| SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, trustedUri :: Bool, smpHosts :: NonEmpty Text}
| Email
| Phone
deriving (Eq, Show, Generic)
@@ -222,12 +222,15 @@ markdownP = mconcat <$> A.many' fragmentP
simplexUriFormat = \case
ACR _ (CRContactUri crData) ->
let uri = safeDecodeUtf8 . strEncode $ CRContactUri crData {crScheme = CRSSimplex}
in SimplexLink (linkType' crData) uri $ uriHosts crData
in SimplexLink (linkType' crData) uri (trustedUri' crData) $ uriHosts crData
ACR _ (CRInvitationUri crData e2e) ->
let uri = safeDecodeUtf8 . strEncode $ CRInvitationUri crData {crScheme = CRSSimplex} e2e
in SimplexLink XLInvitation uri $ uriHosts crData
in SimplexLink XLInvitation uri (trustedUri' crData) $ uriHosts crData
where
uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues
trustedUri' ConnReqUriData {crScheme} = case crScheme of
CRSSimplex -> True
CRSAppServer (SrvLoc host _) -> host == "simplex.chat"
linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of
Just (CRDataGroup _) -> XLGroup
Nothing -> XLContact
+9 -3
View File
@@ -27,7 +27,7 @@ import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Types
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
import System.FilePath ((</>))
import Test.Hspec
@@ -2900,6 +2900,12 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $
testSetContactPrefs :: IO ()
testSetContactPrefs = testChat2 aliceProfile bobProfile $
\alice bob -> do
alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok")
bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok")
createDirectoryIfMissing True "./tests/tmp/alice"
createDirectoryIfMissing True "./tests/tmp/bob"
copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test.txt"
copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test.txt"
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
bob <## "profile image removed"
bob <## "updated preferences:"
@@ -2912,7 +2918,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
let startFeatures = [(0, "Full deletion: off"), (0, "Voice messages: off")]
alice #$> ("/_get chat @2 count=100", chat, startFeatures)
bob #$> ("/_get chat @2 count=100", chat, startFeatures)
let sendVoice = "/_send @2 json {\"filePath\": \"./tests/fixtures/test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}"
let sendVoice = "/_send @2 json {\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}"
voiceNotAllowed = "bad chat command: feature not allowed Voice messages"
alice ##> sendVoice
alice <## voiceNotAllowed
@@ -2929,7 +2935,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
alice <## voiceNotAllowed
bob ##> sendVoice
bob <# "@alice voice message (00:10)"
bob <# "/f @alice ./tests/fixtures/test.txt"
bob <# "/f @alice test.txt"
bob <## "completed sending file 1 (test.txt) to alice"
alice <# "bob> voice message (00:10)"
alice <# "bob> sends file test.txt (11 bytes / 11 bytes)"
+7 -5
View File
@@ -137,8 +137,8 @@ textColor = describe "text color (red)" do
uri :: Text -> Markdown
uri = Markdown $ Just Uri
simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown
simplexLink linkType simplexUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, smpHosts}
simplexLink :: SimplexLinkType -> Text -> Bool -> NonEmpty Text -> Text -> Markdown
simplexLink linkType simplexUri trustedUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, trustedUri, smpHosts}
textWithUri :: Spec
textWithUri = describe "text with Uri" do
@@ -152,11 +152,13 @@ textWithUri = describe "text with Uri" do
parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat"
it "SimpleX links" do
let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv)
parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("https://simplex.chat" <> inv)
parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("simplex:" <> inv)
parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) False ["smp.simplex.im"] ("https://example.com" <> inv)
let ct = "/contact#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D"
parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("https://simplex.chat" <> ct)
parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) True ["smp.simplex.im"] ("https://simplex.chat" <> ct)
let gr = "/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D"
parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr)
parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) True ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr)
email :: Text -> Markdown
email = Markdown $ Just Email