From 444c70a255c912bc2d1bfdf93684a968234523b2 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 2 Mar 2023 13:17:44 -0800 Subject: [PATCH] Autobindings, Remove Server code from Android, Debug mode Fixes --- .gitignore | 5 + LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 78 ++----- fetch-libcwtch-go-macos.sh | 4 +- fetch-libcwtch-go.ps1 | 2 +- fetch-libcwtch-go.sh | 4 +- lib/cwtch/cwtch.dart | 9 +- lib/cwtch/ffi.dart | 54 ++--- lib/cwtch/gomobile.dart | 11 + lib/models/profilelist.dart | 8 + lib/third_party/base32/LICENSE | 9 + lib/third_party/base32/base32.dart | 196 ++++++++++++++++++ lib/third_party/base32/encoding.dart | 52 +++++ lib/views/contactsview.dart | 5 +- lib/views/globalsettingsview.dart | 40 ++-- lib/views/messageview.dart | 18 +- lib/views/profilemgrview.dart | 9 +- lib/views/remoteserverview.dart | 9 +- lib/widgets/messagerow.dart | 63 ++++++ lib/widgets/remoteserverrow.dart | 2 +- lib/widgets/serverrow.dart | 8 +- 22 files changed, 438 insertions(+), 152 deletions(-) create mode 100644 lib/third_party/base32/LICENSE create mode 100644 lib/third_party/base32/base32.dart create mode 100644 lib/third_party/base32/encoding.dart diff --git a/.gitignore b/.gitignore index 341dac30..1a15686e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,11 @@ .buildlog/ .history .svn/ +package.json +package-lock.json +libCwtch* +cwtch.aar +node_modules # IntelliJ related *.iml diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 20efa54b..90ab6e94 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2023-02-08-16-57-v1.10.5 \ No newline at end of file +v0.0.2 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 1ea0cad7..90ab6e94 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2023-02-08-21-57-v1.10.5 \ No newline at end of file +v0.0.2 \ No newline at end of file diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 39303751..ba1f987b 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -323,7 +323,7 @@ class MainActivity: FlutterActivity() { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 val target: Int = call.argument("target") ?: 0 - result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong())) + result.success(Cwtch.sendInviteMessage(profile, conversation.toLong(), target.toLong())) return } @@ -345,14 +345,14 @@ class MainActivity: FlutterActivity() { "RestartSharing" -> { val profile: String = call.argument("ProfileOnion") ?: "" val filepath: String = call.argument("filekey") ?: "" - result.success(Cwtch.restartSharing(profile, filepath)) + result.success(Cwtch.restartFileShare(profile, filepath)) return } "StopSharing" -> { val profile: String = call.argument("ProfileOnion") ?: "" val filepath: String = call.argument("filekey") ?: "" - result.success(Cwtch.stopSharing(profile, filepath)) + result.success(Cwtch.stopFileShare(profile, filepath)) return } @@ -381,25 +381,18 @@ class MainActivity: FlutterActivity() { val passNew2: String = call.argument("NewPassAgain") ?: "" Cwtch.changePassword(profile, pass, passNew, passNew2) } - "GetMessage" -> { - val profile: String = call.argument("ProfileOnion") ?: "" - val conversation: Int = call.argument("conversation") ?: 0 - val indexI: Int = call.argument("index") ?: 0 - result.success(Cwtch.getMessage(profile, conversation.toLong(), indexI.toLong())) - return - } "GetMessageByID" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 val id: Int = call.argument("id") ?: 0 - result.success(Cwtch.getMessageByID(profile, conversation.toLong(), id.toLong())) + result.success(Cwtch.getMessageById(profile, conversation.toLong(), id.toLong())) return } "GetMessageByContentHash" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 val contentHash: String = call.argument("contentHash") ?: "" - result.success(Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash)) + result.success(Cwtch.getMessageByContentHash(profile, conversation.toLong(), contentHash)) return } "SetMessageAttribute" -> { @@ -409,7 +402,7 @@ class MainActivity: FlutterActivity() { val midx: Int = call.argument("Message") ?: 0 val key: String = call.argument("key") ?: "" val value: String = call.argument("value") ?: "" - Cwtch.setMessageAttribute(profile, conversation.toLong(), channel.toLong(), midx.toLong(), key, value) + Cwtch.updateMessageAttribute(profile, conversation.toLong(), channel.toLong(), midx.toLong(), key, value) } "AcceptConversation" -> { val profile: String = call.argument("ProfileOnion") ?: "" @@ -419,12 +412,12 @@ class MainActivity: FlutterActivity() { "BlockContact" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 - Cwtch.blockContact(profile, conversation.toLong()) + Cwtch.blockConversation(profile, conversation.toLong()) } "UnblockContact" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 - Cwtch.unblockContact(profile, conversation.toLong()) + Cwtch.unblockConversation(profile, conversation.toLong()) } "DownloadFile" -> { @@ -436,7 +429,7 @@ class MainActivity: FlutterActivity() { val filekey: String = call.argument("filekey") ?: "" // FIXME: Prevent spurious calls by Intent if (profile != "") { - Cwtch.downloadFile(profile, conversation.toLong(), filepath, manifestpath, filekey) + Cwtch.downloadFileDefaultLimit(profile, conversation.toLong(), filepath, manifestpath, filekey) } } "CheckDownloadStatus" -> { @@ -450,14 +443,9 @@ class MainActivity: FlutterActivity() { val fileKey: String = call.argument("fileKey") ?: "" Cwtch.verifyOrResumeDownload(profile, conversation.toLong(), fileKey) } - "SendProfileEvent" -> { - val onion: String= call.argument("onion") ?: "" - val jsonEvent: String = call.argument("jsonEvent") ?: "" - Cwtch.sendProfileEvent(onion, jsonEvent) - } - "SendAppEvent" -> { - val jsonEvent: String = call.argument("jsonEvent") ?: "" - Cwtch.sendAppEvent(jsonEvent) + "UpdateSettings" -> { + val json: String = call.argument("json") ?: "" + Cwtch.updateSettings(json) } "ResetTor" -> { Cwtch.resetTor() @@ -471,7 +459,7 @@ class MainActivity: FlutterActivity() { val profile: String = call.argument("ProfileOnion") ?: "" val server: String = call.argument("server") ?: "" val groupName: String = call.argument("groupName") ?: "" - Cwtch.createGroup(profile, server, groupName) + Cwtch.startGroup(profile, server, groupName) } "DeleteProfile" -> { val profile: String = call.argument("ProfileOnion") ?: "" @@ -486,7 +474,7 @@ class MainActivity: FlutterActivity() { "DeleteConversation" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 - Cwtch.deleteContact(profile, conversation.toLong()) + Cwtch.deleteConversation(profile, conversation.toLong()) } "SetProfileAttribute" -> { val profile: String = call.argument("ProfileOnion") ?: "" @@ -501,44 +489,6 @@ class MainActivity: FlutterActivity() { val v: String = call.argument("Val") ?: "" Cwtch.setConversationAttribute(profile, conversation.toLong(), key, v) } - "LoadServers" -> { - val password: String = call.argument("Password") ?: "" - Cwtch.loadServers(password) - } - "CreateServer" -> { - val password: String = call.argument("Password") ?: "" - val desc: String = call.argument("Description") ?: "" - val autostart: Boolean = call.argument("Autostart") ?: false - Cwtch.createServer(password, desc, autostart) - } - "DeleteServer" -> { - val serverOnion: String = call.argument("ServerOnion") ?: "" - val password: String = call.argument("Password") ?: "" - Cwtch.deleteServer(serverOnion, password) - } - "LaunchServers" -> { - Cwtch.launchServers() - } - "LaunchServer" -> { - val serverOnion: String = call.argument("ServerOnion") ?: "" - Cwtch.launchServer(serverOnion) - } - "StopServer" -> { - val serverOnion: String = call.argument("ServerOnion") ?: "" - Cwtch.stopServer(serverOnion) - } - "StopServers" -> { - Cwtch.stopServers() - } - "DestroyServers" -> { - Cwtch.destroyServers() - } - "SetServerAttribute" -> { - val serverOnion: String = call.argument("ServerOnion") ?: "" - val key: String = call.argument("Key") ?: "" - val v: String = call.argument("Val") ?: "" - Cwtch.setServerAttribute(serverOnion, key, v) - } "ImportProfile" -> { val file: String = call.argument("file") ?: "" val pass: String = call.argument("pass") ?: "" diff --git a/fetch-libcwtch-go-macos.sh b/fetch-libcwtch-go-macos.sh index 8f1a60f2..e1d16865 100755 --- a/fetch-libcwtch-go-macos.sh +++ b/fetch-libcwtch-go-macos.sh @@ -3,6 +3,6 @@ VERSION=`cat LIBCWTCH-GO-MACOS.version` echo $VERSION -curl --fail https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.x64.dylib --output libCwtch.x64.dylib -curl --fail https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.arm64.dylib --output libCwtch.arm64.dylib +curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/libCwtch.x64.dylib --output libCwtch.x64.dylib +curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/libCwtch.arm64.dylib --output libCwtch.arm64.dylib diff --git a/fetch-libcwtch-go.ps1 b/fetch-libcwtch-go.ps1 index 78740d7a..4de0bf84 100644 --- a/fetch-libcwtch-go.ps1 +++ b/fetch-libcwtch-go.ps1 @@ -2,7 +2,7 @@ $Env:VERSION = type LIBCWTCH-GO.version echo $Env:VERSION # This should automatically fail on error... -Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-go-$Env:VERSION/libCwtch.dll -OutFile windows/libCwtch.dll +Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-autobindings-$Env:VERSION/libCwtch.dll -OutFile windows/libCwtch.dll #Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-go-$Env:VERSION/cwtch.aar -OutFile android/cwtch/cwtch.aar #Invoke-WebRequest -Uri https://build.openprivacy.ca/files/libCwtch-go-$Env:VERSION/libCwtch.so -Outfile linux/libCwtch.so diff --git a/fetch-libcwtch-go.sh b/fetch-libcwtch-go.sh index b515010d..33f6a4a9 100755 --- a/fetch-libcwtch-go.sh +++ b/fetch-libcwtch-go.sh @@ -3,5 +3,5 @@ VERSION=`cat LIBCWTCH-GO.version` echo $VERSION -curl --fail https://build.openprivacy.ca/files/libCwtch-go-$VERSION/cwtch.aar --output android/cwtch/cwtch.aar -curl --fail https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.so --output linux/libCwtch.so \ No newline at end of file +curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/cwtch.aar --output android/cwtch/cwtch.aar +curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/libCwtch.so --output linux/libCwtch.so \ No newline at end of file diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 69b11de9..e58ae768 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -32,12 +32,7 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void ResetTor(); - - // todo: remove these - // ignore: non_constant_identifier_names - void SendProfileEvent(String onion, String jsonEvent); - // ignore: non_constant_identifier_names - void SendAppEvent(String jsonEvent); + void UpdateSettings(String json); // ignore: non_constant_identifier_names void AcceptContact(String profileOnion, int contactHandle); @@ -136,4 +131,6 @@ abstract class Cwtch { void dispose(); Future GetDebugInfo(); + + bool IsServersCompiled(); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index c7b6b45f..2e3ecbd2 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -29,6 +29,7 @@ typedef FreeFn = void Function(Pointer); typedef void_from_string_string_function = Void Function(Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringFn = void Function(Pointer, int, Pointer, int); +typedef VoidFromStringFn = void Function(Pointer, int); typedef void_from_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int); @@ -361,30 +362,6 @@ class CwtchFfi implements Cwtch { return jsonMessage; } - @override - // ignore: non_constant_identifier_names - void SendProfileEvent(String onion, String json) { - var sendAppBusEvent = library.lookup>("c_SendProfileEvent"); - // ignore: non_constant_identifier_names - final SendAppBusEvent = sendAppBusEvent.asFunction(); - final utf8onion = onion.toNativeUtf8(); - final utf8json = json.toNativeUtf8(); - SendAppBusEvent(utf8onion, utf8onion.length, utf8json, utf8json.length); - malloc.free(utf8onion); - malloc.free(utf8json); - } - - @override - // ignore: non_constant_identifier_names - void SendAppEvent(String json) { - var sendAppBusEvent = library.lookup>("c_SendAppEvent"); - // ignore: non_constant_identifier_names - final SendAppBusEvent = sendAppBusEvent.asFunction(); - final utf8json = json.toNativeUtf8(); - SendAppBusEvent(utf8json, utf8json.length); - malloc.free(utf8json); - } - @override // ignore: non_constant_identifier_names void AcceptContact(String profileOnion, int contactHandle) { @@ -437,7 +414,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names Future SendInvitation(String profileOnion, int contactHandle, int target) async { - var sendInvitation = library.lookup>("c_SendInvitation"); + var sendInvitation = library.lookup>("c_SendInviteMessage"); // ignore: non_constant_identifier_names final SendInvitation = sendInvitation.asFunction(); final u1 = profileOnion.toNativeUtf8(); @@ -467,7 +444,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) { - var dlFile = library.lookup>("c_DownloadFile"); + var dlFile = library.lookup>("c_DownloadFileDefaultLimit"); // ignore: non_constant_identifier_names final DownloadFile = dlFile.asFunction(); final u1 = profileOnion.toNativeUtf8(); @@ -546,12 +523,12 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names void CreateGroup(String profileOnion, String server, String groupName) { - var createGroup = library.lookup>("c_CreateGroup"); + var createGroup = library.lookup>("c_StartGroup"); // ignore: non_constant_identifier_names final CreateGroup = createGroup.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = server.toNativeUtf8(); - final u3 = groupName.toNativeUtf8(); + final u3 = server.toNativeUtf8(); + final u2 = groupName.toNativeUtf8(); CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length); malloc.free(u1); @@ -627,7 +604,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) { - var setMessageAttribute = library.lookup>("c_SetMessageAttribute"); + var setMessageAttribute = library.lookup>("c_UpdateMessageAttribute"); // ignore: non_constant_identifier_names final SetMessageAttribute = setMessageAttribute.asFunction(); final u1 = profile.toNativeUtf8(); @@ -762,7 +739,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names Future GetMessageByContentHash(String profile, int handle, String contentHash) async { - var getMessagesByContentHashC = library.lookup>("c_GetMessagesByContentHash"); + var getMessagesByContentHashC = library.lookup>("c_GetMessageByContentHash"); // ignore: non_constant_identifier_names final GetMessagesByContentHash = getMessagesByContentHashC.asFunction(); final utf8profile = profile.toNativeUtf8(); @@ -911,4 +888,19 @@ class CwtchFfi implements Cwtch { malloc.free(utf8profile); malloc.free(ut8filekey); } + + @override + void UpdateSettings(String json) { + var updateSettings = library.lookup>("c_UpdateSettings"); + // ignore: non_constant_identifier_names + final UpdateSettingsFn = updateSettings.asFunction(); + final u1 = json.toNativeUtf8(); + UpdateSettingsFn(u1, u1.length); + malloc.free(u1); + } + + @override + bool IsServersCompiled() { + return library.providesSymbol("c_LoadServers"); + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 91e4d607..e928ce30 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -358,4 +358,15 @@ class CwtchGomobile implements Cwtch { void StopSharing(String profile, String filekey) { cwtchPlatform.invokeMethod("StopSharing", {"ProfileOnion": profile, "filekey": filekey}); } + + @override + void UpdateSettings(String json) { + cwtchPlatform.invokeMethod("UpdateSettings", {"json": json}); + } + + @override + bool IsServersCompiled() { + // never for android builds... + return false; + } } diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart index 3c85a5de..ba3efac0 100644 --- a/lib/models/profilelist.dart +++ b/lib/models/profilelist.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:cwtch/config.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/widgets.dart'; @@ -9,6 +10,13 @@ class ProfileListState extends ChangeNotifier { List _profiles = []; int get num => _profiles.length; + @override + void dispose() { + EnvironmentConfig.debugLog("disposal of profile infostate called..."); + EnvironmentConfig.debugLog(StackTrace.current.toString()); + super.dispose(); + } + void add(String onion, String name, String picture, String defaultPicture, String contactsJson, String serverJson, bool online, bool autostart, bool encrypted) { var idx = _profiles.indexWhere((element) => element.onion == onion); if (idx == -1) { diff --git a/lib/third_party/base32/LICENSE b/lib/third_party/base32/LICENSE new file mode 100644 index 00000000..ff1740cf --- /dev/null +++ b/lib/third_party/base32/LICENSE @@ -0,0 +1,9 @@ +The original version of the base32 code in this library is from https://github.com/Daegalus/dart-base32 + +Copyright (c) 2012 Yulian Kuncheff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lib/third_party/base32/base32.dart b/lib/third_party/base32/base32.dart new file mode 100644 index 00000000..e62f0d32 --- /dev/null +++ b/lib/third_party/base32/base32.dart @@ -0,0 +1,196 @@ +import 'dart:typed_data'; +import 'package:cwtch/third_party/base32/encoding.dart'; + +// ignore: camel_case_types +class base32 { + /// Takes in a [byteList] converts it to a Uint8List so that I can run + /// bit operations on it, then outputs a [String] representation of the + /// base32. + static String encode(Uint8List bytesList, {Encoding encoding = Encoding.standardRFC4648}) { + var base32Chars = EncodingUtils.getChars(encoding); + var i = 0; + var count = (bytesList.length ~/ 5) * 5; + var base32str = ''; + while (i < count) { + var v1 = bytesList[i++]; + var v2 = bytesList[i++]; + var v3 = bytesList[i++]; + var v4 = bytesList[i++]; + var v5 = bytesList[i++]; + + base32str += base32Chars[v1 >> 3] + + base32Chars[(v1 << 2 | v2 >> 6) & 31] + + base32Chars[(v2 >> 1) & 31] + + base32Chars[(v2 << 4 | v3 >> 4) & 31] + + base32Chars[(v3 << 1 | v4 >> 7) & 31] + + base32Chars[(v4 >> 2) & 31] + + base32Chars[(v4 << 3 | v5 >> 5) & 31] + + base32Chars[v5 & 31]; + } + + var remain = bytesList.length - count; + if (remain == 1) { + var v1 = bytesList[i]; + base32str += base32Chars[v1 >> 3] + base32Chars[(v1 << 2) & 31]; + if (EncodingUtils.getPadded(encoding)) { + base32str += '======'; + } + } else if (remain == 2) { + var v1 = bytesList[i++]; + var v2 = bytesList[i]; + base32str += base32Chars[v1 >> 3] + base32Chars[(v1 << 2 | v2 >> 6) & 31] + base32Chars[(v2 >> 1) & 31] + base32Chars[(v2 << 4) & 31]; + if (EncodingUtils.getPadded(encoding)) { + base32str += '===='; + } + } else if (remain == 3) { + var v1 = bytesList[i++]; + var v2 = bytesList[i++]; + var v3 = bytesList[i]; + base32str += base32Chars[v1 >> 3] + base32Chars[(v1 << 2 | v2 >> 6) & 31] + base32Chars[(v2 >> 1) & 31] + base32Chars[(v2 << 4 | v3 >> 4) & 31] + base32Chars[(v3 << 1) & 31]; + if (EncodingUtils.getPadded(encoding)) { + base32str += '==='; + } + } else if (remain == 4) { + var v1 = bytesList[i++]; + var v2 = bytesList[i++]; + var v3 = bytesList[i++]; + var v4 = bytesList[i]; + base32str += base32Chars[v1 >> 3] + + base32Chars[(v1 << 2 | v2 >> 6) & 31] + + base32Chars[(v2 >> 1) & 31] + + base32Chars[(v2 << 4 | v3 >> 4) & 31] + + base32Chars[(v3 << 1 | v4 >> 7) & 31] + + base32Chars[(v4 >> 2) & 31] + + base32Chars[(v4 << 3) & 31]; + if (EncodingUtils.getPadded(encoding)) { + base32str += '='; + } + } + return base32str; + } + + static Uint8List _hexDecode(final String input) => Uint8List.fromList([ + for (int i = 0; i < input.length; i += 2) int.parse(input.substring(i, i + 2), radix: 16), + ]); + + static String _hexEncode(final Uint8List input) => [for (int i = 0; i < input.length; i++) input[i].toRadixString(16).padLeft(2, '0')].join(); + + /// Takes in a [hex] string, converts the string to a byte list + /// and runs a normal encode() on it. Returning a [String] representation + /// of the base32. + static String encodeHexString(String b32hex, {Encoding encoding = Encoding.standardRFC4648}) { + return encode(_hexDecode(b32hex), encoding: encoding); + } + + /// Takes in a [utf8string], converts the string to a byte list + /// and runs a normal encode() on it. Returning a [String] representation + /// of the base32. + static String encodeString(String utf8string, {Encoding encoding = Encoding.standardRFC4648}) { + return encode(Uint8List.fromList(utf8string.codeUnits), encoding: encoding); + } + + /// Takes in a [base32] string and decodes it back to a [String] in hex format. + static String decodeAsHexString(String base32, {Encoding encoding = Encoding.standardRFC4648}) { + return _hexEncode(decode(base32, encoding: encoding)); + } + + /// Takes in a [base32] string and decodes it back to a [String]. + static String decodeAsString(String base32, {Encoding encoding = Encoding.standardRFC4648}) { + return decode(base32, encoding: encoding).toList().map((charCode) => String.fromCharCode(charCode)).join(); + } + + /// Takes in a [base32] string and decodes it back to a [Uint8List] that can be + /// converted to a hex string using hexEncode + static Uint8List decode(String base32, {Encoding encoding = Encoding.standardRFC4648}) { + if (base32.isEmpty) { + return Uint8List(0); + } + + base32 = _pad(base32, encoding: encoding); + + if (!_isValid(base32, encoding: encoding)) { + throw FormatException('Invalid Base32 characters'); + } + + if (encoding == Encoding.crockford) { + base32 = base32.replaceAll('-', ''); + } // Handle crockford dashes. + + var base32Decode = EncodingUtils.getDecodeMap(encoding); + var length = base32.indexOf('='); + if (length == -1) { + length = base32.length; + } + + var i = 0; + var count = length >> 3 << 3; + var bytes = []; + while (i < count) { + var v1 = base32Decode[base32[i++]] ?? 0; + var v2 = base32Decode[base32[i++]] ?? 0; + var v3 = base32Decode[base32[i++]] ?? 0; + var v4 = base32Decode[base32[i++]] ?? 0; + var v5 = base32Decode[base32[i++]] ?? 0; + var v6 = base32Decode[base32[i++]] ?? 0; + var v7 = base32Decode[base32[i++]] ?? 0; + var v8 = base32Decode[base32[i++]] ?? 0; + bytes.add((v1 << 3 | v2 >> 2) & 255); + bytes.add((v2 << 6 | v3 << 1 | v4 >> 4) & 255); + bytes.add((v4 << 4 | v5 >> 1) & 255); + bytes.add((v5 << 7 | v6 << 2 | v7 >> 3) & 255); + bytes.add((v7 << 5 | v8) & 255); + } + + var remain = length - count; + if (remain == 2) { + var v1 = base32Decode[base32[i++]] ?? 0; + var v2 = base32Decode[base32[i++]] ?? 0; + bytes.add((v1 << 3 | v2 >> 2) & 255); + } else if (remain == 4) { + var v1 = base32Decode[base32[i++]] ?? 0; + var v2 = base32Decode[base32[i++]] ?? 0; + var v3 = base32Decode[base32[i++]] ?? 0; + var v4 = base32Decode[base32[i++]] ?? 0; + bytes.add((v1 << 3 | v2 >> 2) & 255); + bytes.add((v2 << 6 | v3 << 1 | v4 >> 4) & 255); + } else if (remain == 5) { + var v1 = base32Decode[base32[i++]] ?? 0; + var v2 = base32Decode[base32[i++]] ?? 0; + var v3 = base32Decode[base32[i++]] ?? 0; + var v4 = base32Decode[base32[i++]] ?? 0; + var v5 = base32Decode[base32[i++]] ?? 0; + bytes.add((v1 << 3 | v2 >> 2) & 255); + bytes.add((v2 << 6 | v3 << 1 | v4 >> 4) & 255); + bytes.add((v4 << 4 | v5 >> 1) & 255); + } else if (remain == 7) { + var v1 = base32Decode[base32[i++]] ?? 0; + var v2 = base32Decode[base32[i++]] ?? 0; + var v3 = base32Decode[base32[i++]] ?? 0; + var v4 = base32Decode[base32[i++]] ?? 0; + var v5 = base32Decode[base32[i++]] ?? 0; + var v6 = base32Decode[base32[i++]] ?? 0; + var v7 = base32Decode[base32[i++]] ?? 0; + bytes.add((v1 << 3 | v2 >> 2) & 255); + bytes.add((v2 << 6 | v3 << 1 | v4 >> 4) & 255); + bytes.add((v4 << 4 | v5 >> 1) & 255); + bytes.add((v5 << 7 | v6 << 2 | v7 >> 3) & 255); + } + return Uint8List.fromList(bytes); + } + + static bool _isValid(String b32str, {Encoding encoding = Encoding.standardRFC4648}) { + var regex = EncodingUtils.getRegex(encoding); + if (b32str.length % 2 != 0 || !regex.hasMatch(b32str)) { + return false; + } + return true; + } + + static String _pad(String base32, {Encoding encoding = Encoding.standardRFC4648}) { + if (EncodingUtils.getPadded(encoding)) { + int neededPadding = (8 - base32.length % 8) % 8; + return base32.padRight(base32.length + neededPadding, '='); + } + return base32; + } +} diff --git a/lib/third_party/base32/encoding.dart b/lib/third_party/base32/encoding.dart new file mode 100644 index 00000000..ed083e50 --- /dev/null +++ b/lib/third_party/base32/encoding.dart @@ -0,0 +1,52 @@ +class EncodingUtils { + static String getChars(Encoding encoding) { + return _encodeMap[encoding]!; + } + + static Map getDecodeMap(Encoding encoding) { + var map = _decodeMap[encoding]; + if (map != null) { + return map; + } else { + var chars = _encodeMap[encoding]!; + // ignore: omit_local_variable_types + Map map = {}; + for (var i = 0; i < 32; i++) { + map[chars[i]] = i; + } + _decodeMap[encoding] = map; + return map; + } + } + + static RegExp getRegex(Encoding encoding) { + return _regexMap[encoding]!; + } + + static bool getPadded(Encoding encoding) { + return _padded[encoding]!; + } + + static final _regexMap = { + Encoding.standardRFC4648: RegExp(r'^[A-Z2-7=]+$'), + Encoding.nonStandardRFC4648Lower: RegExp(r'^[a-z2-7=]+$'), + Encoding.base32Hex: RegExp(r'^[0-9A-V=]+$'), + Encoding.crockford: RegExp(r'^[0123456789ABCDEFGHJKMNPQRSTVWXYZ-]+$'), + Encoding.zbase32: RegExp(r'^[ybndrfg8ejkmcpqxot1uwisza345h769]+$'), + Encoding.geohash: RegExp(r'^[0123456789bcdefghjkmnpqrstuvwxyz=]+$') + }; + static final _encodeMap = { + Encoding.standardRFC4648: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', + Encoding.nonStandardRFC4648Lower: 'abcdefghijklmnopqrstuvwxyz234567', + Encoding.base32Hex: '0123456789ABCDEFGHIJKLMNOPQRSTUV', + Encoding.crockford: '0123456789ABCDEFGHJKMNPQRSTVWXYZ', + Encoding.zbase32: 'ybndrfg8ejkmcpqxot1uwisza345h769', + Encoding.geohash: '0123456789bcdefghjkmnpqrstuvwxyz' + }; + + static final Map> _decodeMap = {}; + + static final _padded = {Encoding.standardRFC4648: true, Encoding.nonStandardRFC4648Lower: true, Encoding.base32Hex: true, Encoding.crockford: false, Encoding.zbase32: false, Encoding.geohash: true}; +} + +enum Encoding { standardRFC4648, base32Hex, crockford, zbase32, geohash, nonStandardRFC4648Lower } diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index fdf51c6d..4fbfc04c 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -307,12 +307,13 @@ class _ContactsViewState extends State { } void _pushServers() { - var profile = Provider.of(context); + var profileInfoState = Provider.of(context, listen: false); + Navigator.of(context).push( PageRouteBuilder( pageBuilder: (bcontext, a1, a2) { return MultiProvider( - providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider.value(value: profileInfoState), Provider.value(value: Provider.of(context))], child: ProfileServersView(), ); }, diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 97e08748..ad9c4103 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -377,22 +377,27 @@ class _GlobalSettingsViewState extends State { Visibility( visible: !Platform.isAndroid && !Platform.isIOS, child: SwitchListTile( - title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor)), - subtitle: Text(AppLocalizations.of(context)!.settingServersDescription), - value: settings.isExperimentEnabled(ServerManagementExperiment), - onChanged: (bool value) { - Provider.of(context, listen: false).clear(); - if (value) { - settings.enableExperiment(ServerManagementExperiment); - } else { - settings.disableExperiment(ServerManagementExperiment); - } - // Save Settings... - saveSettings(context); - }, + title: Text(AppLocalizations.of(context)!.settingServers), + subtitle: Provider.of(context, listen: false).cwtch.IsServersCompiled() + ? Text(AppLocalizations.of(context)!.settingServersDescription) + : Text("This version of Cwtch has been compiled without support for the server hosting experiment."), + value: Provider.of(context, listen: false).cwtch.IsServersCompiled() && settings.isExperimentEnabled(ServerManagementExperiment), + onChanged: Provider.of(context, listen: false).cwtch.IsServersCompiled() + ? (bool value) { + Provider.of(context, listen: false).clear(); + if (value) { + settings.enableExperiment(ServerManagementExperiment); + } else { + settings.disableExperiment(ServerManagementExperiment); + } + // Save Settings... + saveSettings(context); + } + : null, activeTrackColor: settings.theme.defaultButtonColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor, - secondary: Icon(CwtchIcons.dns_24px, color: settings.current().mainTextColor), + inactiveThumbColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(CwtchIcons.dns_24px), )), SwitchListTile( title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor)), @@ -694,10 +699,5 @@ String getThemeName(context, String theme) { /// Send an UpdateGlobalSettings to the Event Bus saveSettings(context) { var settings = Provider.of(context, listen: false); - final updateSettingsEvent = { - "EventType": "UpdateGlobalSettings", - "Data": {"Data": jsonEncode(settings.asJson())}, - }; - final updateSettingsEventJson = jsonEncode(updateSettingsEvent); - Provider.of(context, listen: false).cwtch.SendAppEvent(updateSettingsEventJson); + Provider.of(context, listen: false).cwtch.UpdateSettings(jsonEncode(settings.asJson())); } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 868d1f70..5fbc5357 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -299,10 +299,6 @@ class _MessageViewState extends State { static const GroupMessageLengthMax = 1600; void _sendMessage([String? ignoredParam]) { - // Trim message - final messageWithoutNewLine = ctrlrCompose.value.text.trimRight(); - ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine, selection: TextSelection.fromPosition(TextPosition(offset: messageWithoutNewLine.length))); - // Do this after we trim to preserve enter-behaviour... bool isOffline = Provider.of(context, listen: false).isOnline() == false; bool performingAntiSpam = Provider.of(context, listen: false).antispamTickets == 0; @@ -311,6 +307,10 @@ class _MessageViewState extends State { return; } + // Trim message + final messageWithoutNewLine = ctrlrCompose.value.text.trimRight(); + ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine, selection: TextSelection.fromPosition(TextPosition(offset: messageWithoutNewLine.length))); + // peers and groups currently have different length constraints (servers can store less)... var actualMessageLength = ctrlrCompose.value.text.length; var lengthOk = (isGroup && actualMessageLength < GroupMessageLengthMax) || actualMessageLength <= P2PMessageLengthMax; @@ -368,6 +368,11 @@ class _MessageViewState extends State { return; } + // At this point we have decided to send the text to the backend, failure is still possible + // but it will show as an error-ed message, as such the draft can be purged. + Provider.of(context, listen: false).messageDraft = null; + ctrlrCompose.clear(); + var profileOnion = Provider.of(context, listen: false).profileOnion; var identifier = Provider.of(context, listen: false).identifier; var profile = Provider.of(context, listen: false); @@ -388,11 +393,8 @@ class _MessageViewState extends State { ); } - Provider.of(context, listen: false).messageDraft = null; - ctrlrCompose.clear(); - focusNode.requestFocus(); - Provider.of(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String()); + focusNode.requestFocus(); } Widget _buildPreviewBox() { diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index 3b33cecd..93806440 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -106,7 +106,10 @@ class _ProfileMgrViewState extends State { )); // Servers - if (Provider.of(context).isExperimentEnabled(ServerManagementExperiment) && !Platform.isAndroid && !Platform.isIOS) { + if (Provider.of(context, listen: false).cwtch.IsServersCompiled() && + Provider.of(context).isExperimentEnabled(ServerManagementExperiment) && + !Platform.isAndroid && + !Platform.isIOS) { actions.add( IconButton(icon: Icon(CwtchIcons.dns_black_24dp), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers)); } @@ -150,7 +153,7 @@ class _ProfileMgrViewState extends State { settings: RouteSettings(name: "servers"), pageBuilder: (bcontext, a1, a2) { return MultiProvider( - providers: [Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider.value(value: globalServersList), Provider.value(value: Provider.of(context))], child: ServersView(), ); }, @@ -226,7 +229,7 @@ class _ProfileMgrViewState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))), ), child: Text( - key:Key("addNewProfileActual"), + key: Key("addNewProfileActual"), AppLocalizations.of(context)!.addProfileTitle, semanticsLabel: AppLocalizations.of(context)!.addProfileTitle, style: TextStyle(fontWeight: FontWeight.bold), diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart index 22e0a6f2..9afbe13d 100644 --- a/lib/views/remoteserverview.dart +++ b/lib/views/remoteserverview.dart @@ -37,10 +37,6 @@ class _RemoteServerViewState extends State { @override void initState() { super.initState(); - var serverInfoState = Provider.of(context, listen: false); - if (serverInfoState.description.isNotEmpty) { - ctrlrDesc.text = serverInfoState.description; - } } @override @@ -50,6 +46,11 @@ class _RemoteServerViewState extends State { @override Widget build(BuildContext context) { + var serverInfoState = Provider.of(context, listen: false); + if (serverInfoState.description.isNotEmpty) { + ctrlrDesc.text = serverInfoState.description; + } + return Consumer3(builder: (context, profile, serverInfoState, settings, child) { return Scaffold( appBar: AppBar(title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion)), diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index a0fa87af..6bd6475d 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -6,6 +6,7 @@ import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/profile.dart'; +import 'package:cwtch/third_party/base32/base32.dart'; import 'package:cwtch/views/contactsview.dart'; import 'package:cwtch/widgets/staticmessagebubble.dart'; import 'package:flutter/material.dart'; @@ -180,6 +181,8 @@ class MessageRowState extends State with SingleTickerProviderStateMi String imagePath = Provider.of(context).senderImage!; if (sender != null) { imagePath = Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath; + } else { + imagePath = RandomProfileImage(Provider.of(context).senderHandle); } Widget wdgPortrait = GestureDetector( onTap: !isGroup @@ -388,6 +391,8 @@ void modalShowReplies( var sender = profile.contactList.findContact(e.getMetadata().senderHandle); if (sender != null) { imagePath = showImage ? sender.imagePath : sender.defaultImagePath; + } else { + imagePath = RandomProfileImage(e.getMetadata().senderHandle); } if (fromMe) { @@ -453,3 +458,61 @@ void modalShowReplies( }); }); } + +// temporary until we do real picture selection +String RandomProfileImage(String onion) { + var choices = [ + "001-centaur", + "002-kraken", + "003-dinosaur", + "004-tree-1", + "005-hand", + "006-echidna", + "007-robot", + "008-mushroom", + "009-harpy", + "010-phoenix", + "011-dragon-1", + "012-devil", + "013-troll", + "014-alien", + "015-minotaur", + "016-madre-monte", + "017-satyr", + "018-karakasakozou", + "019-pirate", + "020-werewolf", + "021-scarecrow", + "022-valkyrie", + "023-curupira", + "024-loch-ness-monster", + "025-tree", + "026-cerberus", + "027-gryphon", + "028-mermaid", + "029-vampire", + "030-goblin", + "031-yeti", + "032-leprechaun", + "033-medusa", + "034-chimera", + "035-elf", + "036-hydra", + "037-cyclops", + "038-pegasus", + "039-narwhal", + "040-woodcutter", + "041-zombie", + "042-dragon", + "043-frankenstein", + "044-witch", + "045-fairy", + "046-genie", + "047-pinocchio", + "048-ghost", + "049-wizard", + "050-unicorn" + ]; + var encoding = base32.decode(onion.toUpperCase()); + return "assets/profiles/" + choices[encoding[33] % choices.length] + ".png"; +} diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart index 26258004..ae77dae9 100644 --- a/lib/widgets/remoteserverrow.dart +++ b/lib/widgets/remoteserverrow.dart @@ -81,7 +81,7 @@ class _RemoteServerRowState extends State { settings: RouteSettings(name: "remoteserverview"), pageBuilder: (bcontext, a1, a2) { return MultiProvider( - providers: [Provider.value(value: profile), ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: server), Provider.value(value: Provider.of(context))], child: RemoteServerView(), ); }, diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index f63910bf..30574535 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -91,16 +91,12 @@ class _ServerRowState extends State { } void _pushEditServer(ServerInfoState server) { - Provider.of(context).reset(); + Provider.of(context, listen: false).reset(); Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name: "serveraddedit"), builder: (BuildContext context) { return MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => server, - ) - ], + providers: [ChangeNotifierProvider.value(value: server)], child: AddEditServerView(), ); },