commit 7e90bf6369ae79d9e0473439e8ab3c56ae6b3442 Author: jack Date: Wed Jul 2 19:18:35 2025 +0200 Initial commit: Bluetooth mesh chat app with end-to-end encryption diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a506f99b --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +## macOS +.DS_Store + +## SPM +.swiftpm +.build/ + +## CocoaPods +Pods/ + +## Carthage +Carthage/Build/ + +## fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +## Code Injection +iOSInjectionProject/ + +## Xcode project +*.xcodeproj/project.xcworkspace/ +*.xcodeproj/xcshareddata/ + +## Python +__pycache__/ +*.py[cod] +*$py.class + +## Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..646bb341 --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "bitchat", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + .executable( + name: "bitchat", + targets: ["bitchat"] + ), + ], + targets: [ + .executableTarget( + name: "bitchat", + path: "bitchat", + resources: [ + .process("Info.plist") + ] + ), + ] +) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..bc8d3135 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# bitchat + +A secure, end-to-end encrypted Bluetooth mesh chat application with an IRC-style interface. + +## Features + +- End-to-end encryption using Curve25519 and AES-GCM +- Bluetooth mesh networking with automatic peer discovery +- Message relay capability (TTL-based flooding) +- IRC-style terminal interface +- Persistent nickname storage +- Universal app (iOS and macOS) +- No internet connection required + +## Setup + +### Option 1: Using XcodeGen (Recommended) + +1. Install XcodeGen if you haven't already: + ```bash + brew install xcodegen + ``` + +2. Generate the Xcode project: + ```bash + cd bitchat + xcodegen generate + ``` + +3. Open the generated project: + ```bash + open bitchat.xcodeproj + ``` + +### Option 2: Using Swift Package Manager + +1. Open the project in Xcode: + ```bash + cd bitchat + open Package.swift + ``` + +2. Select your target device and run + +### Option 3: Manual Xcode Project + +1. Open Xcode and create a new iOS/macOS App +2. Copy all Swift files from the `bitchat` directory into your project +3. Update Info.plist with Bluetooth permissions +4. Set deployment target to iOS 16.0 / macOS 13.0 + +## Usage + +1. Launch the app on multiple devices +2. Choose or modify your nickname +3. The app will automatically discover nearby peers +4. Start chatting! Messages are relayed through the mesh network + +## Security + +- All messages are end-to-end encrypted +- Public key exchange happens automatically on connection +- Messages are signed to prevent tampering +- TTL prevents infinite message loops + +## Protocol + +The bitchat protocol uses JSON-encoded packets with the following structure: +- Packet versioning for future compatibility +- Message types: handshake, message, ack, relay, announce, keyExchange +- TTL-based flooding for mesh relay +- Signature verification for authenticity + +## Building for Production + +1. Set your development team in project settings +2. Configure code signing +3. Archive and distribute through App Store or TestFlight + +## Android Compatibility + +The protocol is designed to be platform-agnostic. An Android client can be built using: +- Bluetooth LE APIs +- Same packet structure and encryption +- Compatible service/characteristic UUIDs \ No newline at end of file diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1ff73c49 --- /dev/null +++ b/bitchat.xcodeproj/project.pbxproj @@ -0,0 +1,483 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; + 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; + 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; + 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; + 739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; }; + 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; }; + 7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; }; + 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; + 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; + 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; }; + BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; }; + D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; }; + D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; }; + DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = ""; }; + 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = ""; }; + 7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = ""; }; + E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = ""; }; + EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 18198ED912AAF495D8AF7763 = { + isa = PBXGroup; + children = ( + 2F82C5FC8433F4064F079D1F /* bitchat */, + 9F37F9F2C353B58AC809E93B /* Products */, + ); + sourceTree = ""; + }; + 2F82C5FC8433F4064F079D1F /* bitchat */ = { + isa = PBXGroup; + children = ( + 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */, + EF625BB3AD919322C01A46B2 /* BitchatApp.swift */, + EA706D8E5097785414646A8E /* Info.plist */, + ADD53BCDA233C02E53458926 /* Protocols */, + D98A3186D7E4C72E35BDF7FE /* Services */, + 45BB7D87CAE42A8C0447D909 /* ViewModels */, + A55126E93155456CAA8D6656 /* Views */, + ); + path = bitchat; + sourceTree = ""; + }; + 45BB7D87CAE42A8C0447D909 /* ViewModels */ = { + isa = PBXGroup; + children = ( + E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 9F37F9F2C353B58AC809E93B /* Products */ = { + isa = PBXGroup; + children = ( + 997D512074C64904D75DDD40 /* bitchat.app */, + 7EEBDA723E1CFD88758DA4AC /* bitchat.app */, + ); + name = Products; + sourceTree = ""; + }; + A55126E93155456CAA8D6656 /* Views */ = { + isa = PBXGroup; + children = ( + A08E03AA0C63E97C91749AEC /* ContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; + ADD53BCDA233C02E53458926 /* Protocols */ = { + isa = PBXGroup; + children = ( + 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + D98A3186D7E4C72E35BDF7FE /* Services */ = { + isa = PBXGroup; + children = ( + D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */, + 6DC1563390A15C042D059CF9 /* EncryptionService.swift */, + ); + path = Services; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0576A29205865664C0937536 /* bitchat_macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DA5644925338B8189B035657 /* Build configuration list for PBXNativeTarget "bitchat_macOS" */; + buildPhases = ( + 137ABE739BF20ACDDF8CC605 /* Sources */, + 0214973A876129753D39EB47 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = bitchat_macOS; + packageProductDependencies = ( + ); + productName = bitchat_macOS; + productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */; + productType = "com.apple.product-type.application"; + }; + AF077EA0474EDEDE2C72716C /* bitchat_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */; + buildPhases = ( + 4E49E34F00154C051AE90FED /* Sources */, + CD6E8F32BC38357473954F97 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = bitchat_iOS; + packageProductDependencies = ( + ); + productName = bitchat_iOS; + productReference = 997D512074C64904D75DDD40 /* bitchat.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 475D96681D0EA0AE57A4E06E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 0576A29205865664C0937536 = { + DevelopmentTeam = ""; + ProvisioningStyle = Automatic; + }; + AF077EA0474EDEDE2C72716C = { + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 18198ED912AAF495D8AF7763; + minimizedProjectReferenceProxies = 1; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AF077EA0474EDEDE2C72716C /* bitchat_iOS */, + 0576A29205865664C0937536 /* bitchat_macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0214973A876129753D39EB47 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CD6E8F32BC38357473954F97 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 137ABE739BF20ACDDF8CC605 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */, + 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */, + 7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */, + D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */, + 92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */, + 739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4E49E34F00154C051AE90FED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */, + 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */, + D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */, + 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */, + 1D9674FA5F998503831DC281 /* ContentView.swift in Sources */, + DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 702E7395723CADA4B830F4A9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = bitchat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = net.bitchat.app; + PRODUCT_NAME = bitchat; + SDKROOT = iphoneos; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B36671AEACCBF92BE10852E9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = L3N5LHJD5Y; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = bitchat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = net.bitchat.app; + PRODUCT_NAME = bitchat; + SDKROOT = iphoneos; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BB044400A0F06B93F22D0D55 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = bitchat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app; + PRODUCT_NAME = bitchat; + SDKROOT = macosx; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + BF0D85727BCB6E346962F419 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + CC79F65842D42034ACEE79B7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = bitchat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app; + PRODUCT_NAME = bitchat; + SDKROOT = macosx; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D8C5BF109BB2630752185FA0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D8C5BF109BB2630752185FA0 /* Debug */, + BF0D85727BCB6E346962F419 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 702E7395723CADA4B830F4A9 /* Debug */, + B36671AEACCBF92BE10852E9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + DA5644925338B8189B035657 /* Build configuration list for PBXNativeTarget "bitchat_macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CC79F65842D42034ACEE79B7 /* Debug */, + BB044400A0F06B93F22D0D55 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 475D96681D0EA0AE57A4E06E /* Project object */; +} diff --git a/bitchat/Assets.xcassets/AccentColor.colorset/Contents.json b/bitchat/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..98d4069c --- /dev/null +++ b/bitchat/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "1.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/Contents.json b/bitchat/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..fdf4027a --- /dev/null +++ b/bitchat/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,176 @@ +{ + "images" : [ + { + "filename" : "icon_20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon_20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon_29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon_29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon_40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon_40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon_60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon_60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "icon_20x20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "icon_20x20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon_29x29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon_29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon_40x40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon_40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon_76x76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon_76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon_83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "icon_1024x1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png new file mode 100644 index 00000000..9bf9b82a Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 00000000..f85ced2c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..6516c53c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 00000000..fa6a8cf0 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..84b3ea2c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20.png new file mode 100644 index 00000000..86923c1c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png new file mode 100644 index 00000000..9c2fb54a Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png new file mode 100644 index 00000000..b8b5455e Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 00000000..6516c53c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..5c73d4e6 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29.png new file mode 100644 index 00000000..2e5f31ad Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png new file mode 100644 index 00000000..c301074b Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png new file mode 100644 index 00000000..0e9a9c14 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 00000000..84b3ea2c Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..58e93947 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40.png new file mode 100644 index 00000000..9c2fb54a Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png new file mode 100644 index 00000000..ad43fe36 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png new file mode 100644 index 00000000..fe8d9352 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 00000000..5c73d4e6 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..9bf9b82a Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png new file mode 100644 index 00000000..fe8d9352 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png new file mode 100644 index 00000000..6e66f93f Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76.png new file mode 100644 index 00000000..fdc2d7a9 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png new file mode 100644 index 00000000..ab85ec26 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png new file mode 100644 index 00000000..901bfd7a Binary files /dev/null and b/bitchat/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png differ diff --git a/bitchat/Assets.xcassets/Contents.json b/bitchat/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/bitchat/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift new file mode 100644 index 00000000..084fce76 --- /dev/null +++ b/bitchat/BitchatApp.swift @@ -0,0 +1,17 @@ +import SwiftUI + +@main +struct BitchatApp: App { + @StateObject private var chatViewModel = ChatViewModel() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(chatViewModel) + } + #if os(macOS) + .windowStyle(.hiddenTitleBar) + .windowResizability(.contentSize) + #endif + } +} \ No newline at end of file diff --git a/bitchat/Info.plist b/bitchat/Info.plist new file mode 100644 index 00000000..e2b8900c --- /dev/null +++ b/bitchat/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + bitchat + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSBluetoothAlwaysUsageDescription + bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. + NSBluetoothPeripheralUsageDescription + bitchat uses Bluetooth to discover and connect with other bitchat users nearby. + UILaunchScreen + + UIColorName + Black + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift new file mode 100644 index 00000000..7489094b --- /dev/null +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -0,0 +1,66 @@ +import Foundation +import CryptoKit + +enum MessageType: UInt8 { + case handshake = 0x01 + case message = 0x02 + case ack = 0x03 + case relay = 0x04 + case announce = 0x05 + case keyExchange = 0x06 +} + +struct BitchatPacket: Codable { + let version: UInt8 + let type: UInt8 + let senderID: Data + let recipientID: Data? + let timestamp: UInt64 + let payload: Data + let signature: Data? + var ttl: UInt8 + + init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8) { + self.version = 1 + self.type = type + self.senderID = senderID + self.recipientID = recipientID + self.timestamp = timestamp + self.payload = payload + self.signature = signature + self.ttl = ttl + } + + var data: Data? { + try? JSONEncoder().encode(self) + } + + static func from(_ data: Data) -> BitchatPacket? { + try? JSONDecoder().decode(BitchatPacket.self, from: data) + } +} + +struct BitchatMessage: Codable { + let id: String + let sender: String + let content: String + let timestamp: Date + let isRelay: Bool + let originalSender: String? + + init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil) { + self.id = UUID().uuidString + self.sender = sender + self.content = content + self.timestamp = timestamp + self.isRelay = isRelay + self.originalSender = originalSender + } +} + +protocol BitchatDelegate: AnyObject { + func didReceiveMessage(_ message: BitchatMessage) + func didConnectToPeer(_ peerID: String) + func didDisconnectFromPeer(_ peerID: String) + func didUpdatePeerList(_ peers: [String]) +} \ No newline at end of file diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift new file mode 100644 index 00000000..f098a4f4 --- /dev/null +++ b/bitchat/Services/BluetoothMeshService.swift @@ -0,0 +1,528 @@ +import Foundation +import CoreBluetooth +import Combine + +class BluetoothMeshService: NSObject { + static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") + static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") + + private var centralManager: CBCentralManager! + private var peripheralManager: CBPeripheralManager! + private var discoveredPeripherals: [CBPeripheral] = [] + private var connectedPeripherals: [String: CBPeripheral] = [:] + private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:] + private var characteristic: CBMutableCharacteristic! + private var subscribedCentrals: [CBCentral] = [] + private var peerNicknames: [String: String] = [:] + private var activePeers: Set = [] // Track all active peers + + weak var delegate: BitchatDelegate? + private let encryptionService = EncryptionService() + private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent) + private var processedMessages = Set() + private let maxTTL: UInt8 = 5 + + let myPeerID: String + + override init() { + self.myPeerID = UUID().uuidString.prefix(8).lowercased() + super.init() + + centralManager = CBCentralManager(delegate: self, queue: nil) + peripheralManager = CBPeripheralManager(delegate: self, queue: nil) + } + + func startServices() { + // Start both central and peripheral services + if centralManager.state == .poweredOn { + startScanning() + } + if peripheralManager.state == .poweredOn { + setupPeripheral() + startAdvertising() + } + } + + func startAdvertising() { + guard peripheralManager.state == .poweredOn else { + return + } + + let advertisementData: [String: Any] = [ + CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID], + CBAdvertisementDataLocalNameKey: "bitchat-\(myPeerID)" + ] + peripheralManager.startAdvertising(advertisementData) + } + + func startScanning() { + guard centralManager.state == .poweredOn else { + return + } + centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) + } + + private func setupPeripheral() { + let characteristic = CBMutableCharacteristic( + type: BluetoothMeshService.characteristicUUID, + properties: [.read, .write, .writeWithoutResponse, .notify], + value: nil, + permissions: [.readable, .writeable] + ) + + let service = CBMutableService(type: BluetoothMeshService.serviceUUID, primary: true) + service.characteristics = [characteristic] + + peripheralManager.add(service) + self.characteristic = characteristic + } + + func sendMessage(_ content: String, to recipientID: String? = nil) { + messageQueue.async { [weak self] in + guard let self = self else { return } + + let nickname = self.delegate as? ChatViewModel + let senderNick = nickname?.nickname ?? self.myPeerID + + let message = BitchatMessage( + sender: senderNick, + content: content, + timestamp: Date(), + isRelay: false, + originalSender: nil + ) + + if let messageData = try? JSONEncoder().encode(message) { + let packet = BitchatPacket( + type: MessageType.message.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: recipientID?.data(using: .utf8), + timestamp: UInt64(Date().timeIntervalSince1970), + payload: messageData, + signature: try? self.encryptionService.sign(messageData), + ttl: self.maxTTL + ) + + self.broadcastPacket(packet) + } + } + } + + + func getPeerNicknames() -> [String: String] { + return peerNicknames + } + + private func getAllConnectedPeerIDs() -> [String] { + return Array(activePeers) + } + + private func broadcastPacket(_ packet: BitchatPacket) { + guard let data = packet.data else { return } + + // Send to connected peripherals (as central) + for (_, peripheral) in connectedPeripherals { + if let characteristic = peripheralCharacteristics[peripheral] { + // Use withResponse for larger data for reliability + let writeType: CBCharacteristicWriteType = data.count > 50000 ? .withResponse : .withoutResponse + peripheral.writeValue(data, for: characteristic, type: writeType) + } + } + + // Send to subscribed centrals (as peripheral) + if characteristic != nil && !subscribedCentrals.isEmpty { + peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals) + } + } + + private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) { + guard packet.ttl > 0 else { return } + + let messageID = "\(packet.timestamp)-\(String(data: packet.senderID, encoding: .utf8) ?? "")" + guard !processedMessages.contains(messageID) else { return } + processedMessages.insert(messageID) + + if processedMessages.count > 1000 { + processedMessages.removeAll() + } + + switch MessageType(rawValue: packet.type) { + case .message: + if let message = try? JSONDecoder().decode(BitchatMessage.self, from: packet.payload) { + // Ignore our own messages + if let senderID = String(data: packet.senderID, encoding: .utf8), senderID == myPeerID { + return + } + + // Store nickname mapping + if let senderID = String(data: packet.senderID, encoding: .utf8) { + peerNicknames[senderID] = message.sender + } + + DispatchQueue.main.async { + self.delegate?.didReceiveMessage(message) + } + + var relayPacket = packet + relayPacket.ttl -= 1 + if relayPacket.ttl > 0 { + self.broadcastPacket(relayPacket) + } + } + + case .keyExchange: + if let publicKeyData = try? JSONDecoder().decode(Data.self, from: packet.payload) { + try? encryptionService.addPeerPublicKey(peerID, publicKeyData: publicKeyData) + + // Track this peer temporarily but don't announce until we get their name + if peerID != "unknown" && peerID != myPeerID && !peerNicknames.keys.contains(peerID) { + // Just track them, don't announce yet + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + + // Send announce with our nickname immediately + DispatchQueue.main.async { [weak self] in + if let self = self, let vm = self.delegate as? ChatViewModel { + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: peerID.data(using: .utf8), + timestamp: UInt64(Date().timeIntervalSince1970), + payload: vm.nickname.data(using: .utf8)!, + signature: nil, + ttl: 1 + ) + self.broadcastPacket(announcePacket) + } + } + } + + case .announce: + if let nickname = String(data: packet.payload, encoding: .utf8) { + // Store the nickname + peerNicknames[peerID] = nickname + + // Add to active peers if not already there + if peerID != "unknown" && peerID != myPeerID && !activePeers.contains(peerID) { + activePeers.insert(peerID) + DispatchQueue.main.async { + self.delegate?.didConnectToPeer(nickname) + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } else { + // Just update the peer list to refresh nicknames + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + } + + default: + break + } + } +} + +extension BluetoothMeshService: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + if central.state == .poweredOn { + startScanning() + } + } + + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + // Connect to any device we discover - we'll filter by service later + if !discoveredPeripherals.contains(peripheral) { + discoveredPeripherals.append(peripheral) + peripheral.delegate = self + central.connect(peripheral, options: nil) + } + } + + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + peripheral.delegate = self + peripheral.discoverServices([BluetoothMeshService.serviceUUID]) + + // Extract peer ID from advertisement or generate one + var peerID = peripheral.identifier.uuidString.prefix(8).lowercased() + if let name = peripheral.name, name.hasPrefix("bitchat-") { + peerID = String(name.dropFirst(8)) + } + + connectedPeripherals[String(peerID)] = peripheral + + // Update peer list to show we're connecting + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + + func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key { + connectedPeripherals.removeValue(forKey: peerID) + peripheralCharacteristics.removeValue(forKey: peripheral) + + // Remove from active peers + activePeers.remove(peerID) + + // Only show disconnect if we have a resolved nickname + if let nickname = peerNicknames[peerID], nickname != peerID { + DispatchQueue.main.async { + self.delegate?.didDisconnectFromPeer(nickname) + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } else { + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + } + + // Remove from discovered list to allow reconnection + discoveredPeripherals.removeAll { $0 == peripheral } + + // Continue scanning for reconnection + if centralManager.state == .poweredOn { + // Always restart scan to ensure we can reconnect + centralManager.stopScan() + centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) + } + } +} + +extension BluetoothMeshService: CBPeripheralDelegate { + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + guard let services = peripheral.services else { return } + + for service in services { + peripheral.discoverCharacteristics([BluetoothMeshService.characteristicUUID], for: service) + } + } + + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + guard let characteristics = service.characteristics else { return } + + for characteristic in characteristics { + if characteristic.uuid == BluetoothMeshService.characteristicUUID { + peripheral.setNotifyValue(true, for: characteristic) + peripheralCharacteristics[peripheral] = characteristic + + // Wait a moment for subscription to complete before sending data + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self = self else { return } + + let publicKeyData = self.encryptionService.publicKey.rawRepresentation + let packet = BitchatPacket( + type: MessageType.keyExchange.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970), + payload: try! JSONEncoder().encode(publicKeyData), + signature: nil, + ttl: 1 + ) + + if let data = packet.data { + peripheral.writeValue(data, for: characteristic, type: .withResponse) + } + + // Also send announce packet immediately + if let vm = self.delegate as? ChatViewModel { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + guard let self = self else { return } + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970), + payload: vm.nickname.data(using: .utf8)!, + signature: nil, + ttl: 1 + ) + if let data = announcePacket.data { + peripheral.writeValue(data, for: characteristic, type: .withResponse) + } + } + } + } + } + } + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + guard let data = characteristic.value, + let packet = BitchatPacket.from(data) else { + return + } + + let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown" + handleReceivedPacket(packet, from: peerID) + } + + func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + // Handle write completion if needed + } + + func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { + peripheral.discoverServices([BluetoothMeshService.serviceUUID]) + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { + // Handle notification state update if needed + } +} + +extension BluetoothMeshService: CBPeripheralManagerDelegate { + func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { + switch peripheral.state { + case .poweredOn: + setupPeripheral() + startAdvertising() + default: + break + } + } + + func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { + // Handle service addition if needed + } + + func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { + for request in requests { + if let data = request.value, + let packet = BitchatPacket.from(data) { + // Try to identify peer from packet + let peerID = String(data: packet.senderID, encoding: .utf8) ?? "unknown" + + // Store the central for updates + if !subscribedCentrals.contains(request.central) { + subscribedCentrals.append(request.central) + } + + // Track this peer as connected + if peerID != "unknown" && peerID != myPeerID { + // Send key exchange back if we haven't already + if packet.type == MessageType.keyExchange.rawValue { + let publicKeyData = self.encryptionService.publicKey.rawRepresentation + let responsePacket = BitchatPacket( + type: MessageType.keyExchange.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: peerID.data(using: .utf8), + timestamp: UInt64(Date().timeIntervalSince1970), + payload: try! JSONEncoder().encode(publicKeyData), + signature: nil, + ttl: 1 + ) + if let data = responsePacket.data { + peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central]) + } + + // Also send announce immediately after key exchange + if let vm = self.delegate as? ChatViewModel { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self = self else { return } + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: peerID.data(using: .utf8), + timestamp: UInt64(Date().timeIntervalSince1970), + payload: vm.nickname.data(using: .utf8)!, + signature: nil, + ttl: 1 + ) + if let data = announcePacket.data { + peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central]) + } + } + } + } + + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + + handleReceivedPacket(packet, from: peerID) + peripheral.respond(to: request, withResult: .success) + } + } + } + + func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { + if !subscribedCentrals.contains(central) { + subscribedCentrals.append(central) + + // Send our public key to the newly connected central + let publicKeyData = encryptionService.publicKey.rawRepresentation + let keyPacket = BitchatPacket( + type: MessageType.keyExchange.rawValue, + senderID: myPeerID.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970), + payload: try! JSONEncoder().encode(publicKeyData), + signature: nil, + ttl: 1 + ) + + if let data = keyPacket.data { + peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central]) + + // Also send announce after key exchange + if let vm = delegate as? ChatViewModel { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + guard let self = self else { return } + let announcePacket = BitchatPacket( + type: MessageType.announce.rawValue, + senderID: self.myPeerID.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970), + payload: vm.nickname.data(using: .utf8)!, + signature: nil, + ttl: 1 + ) + if let data = announcePacket.data { + peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central]) + } + } + } + } + + // Update peer list to show we're connected (even without peer ID yet) + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + } + + func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { + subscribedCentrals.removeAll { $0 == central } + + // If no more centrals are subscribed, clear all central-connected peers + if subscribedCentrals.isEmpty { + // Find and remove peers that were connected as centrals only + let peersToRemove = activePeers.filter { peerID in + !connectedPeripherals.keys.contains(peerID) + } + + for peerID in peersToRemove { + activePeers.remove(peerID) + if let nickname = peerNicknames[peerID] { + DispatchQueue.main.async { + self.delegate?.didDisconnectFromPeer(nickname) + } + } + } + + DispatchQueue.main.async { + self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs()) + } + } + + // Ensure advertising continues for reconnection + if peripheralManager.state == .poweredOn && !peripheralManager.isAdvertising { + startAdvertising() + } + } +} \ No newline at end of file diff --git a/bitchat/Services/EncryptionService.swift b/bitchat/Services/EncryptionService.swift new file mode 100644 index 00000000..c6e680dc --- /dev/null +++ b/bitchat/Services/EncryptionService.swift @@ -0,0 +1,67 @@ +import Foundation +import CryptoKit + +class EncryptionService { + private var privateKey: Curve25519.KeyAgreement.PrivateKey + public let publicKey: Curve25519.KeyAgreement.PublicKey + private var peerPublicKeys: [String: Curve25519.KeyAgreement.PublicKey] = [:] + private var sharedSecrets: [String: SymmetricKey] = [:] + + init() { + self.privateKey = Curve25519.KeyAgreement.PrivateKey() + self.publicKey = privateKey.publicKey + } + + func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws { + let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: publicKeyData) + peerPublicKeys[peerID] = publicKey + + let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey( + using: SHA256.self, + salt: "bitchat-v1".data(using: .utf8)!, + sharedInfo: Data(), + outputByteCount: 32 + ) + sharedSecrets[peerID] = symmetricKey + } + + func encrypt(_ data: Data, for peerID: String) throws -> Data { + guard let symmetricKey = sharedSecrets[peerID] else { + throw EncryptionError.noSharedSecret + } + + let sealedBox = try AES.GCM.seal(data, using: symmetricKey) + return sealedBox.combined ?? Data() + } + + func decrypt(_ data: Data, from peerID: String) throws -> Data { + guard let symmetricKey = sharedSecrets[peerID] else { + throw EncryptionError.noSharedSecret + } + + let sealedBox = try AES.GCM.SealedBox(combined: data) + return try AES.GCM.open(sealedBox, using: symmetricKey) + } + + func sign(_ data: Data) throws -> Data { + let signingKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey.rawRepresentation) + return try signingKey.signature(for: data) + } + + func verify(_ signature: Data, for data: Data, from peerID: String) throws -> Bool { + guard let peerPublicKey = peerPublicKeys[peerID] else { + return false + } + + let verifyingKey = try Curve25519.Signing.PublicKey(rawRepresentation: peerPublicKey.rawRepresentation) + return verifyingKey.isValidSignature(signature, for: data) + } +} + +enum EncryptionError: Error { + case noSharedSecret + case invalidPublicKey + case encryptionFailed + case decryptionFailed +} \ No newline at end of file diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift new file mode 100644 index 00000000..5633eee9 --- /dev/null +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -0,0 +1,160 @@ +import Foundation +import SwiftUI +import Combine +#if os(iOS) +import UIKit +#endif + +class ChatViewModel: ObservableObject { + @Published var messages: [BitchatMessage] = [] + @Published var connectedPeers: [String] = [] + @Published var nickname: String = "" { + didSet { + nicknameSaveTimer?.invalidate() + nicknameSaveTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in + self.saveNickname() + } + } + } + @Published var isConnected = false + + let meshService = BluetoothMeshService() + private let userDefaults = UserDefaults.standard + private let nicknameKey = "bitchat.nickname" + private var nicknameSaveTimer: Timer? + + init() { + loadNickname() + meshService.delegate = self + + // Start mesh service immediately + meshService.startServices() + } + + private func loadNickname() { + if let savedNickname = userDefaults.string(forKey: nicknameKey) { + nickname = savedNickname + } else { + nickname = "user\(Int.random(in: 1000...9999))" + saveNickname() + } + } + + func saveNickname() { + userDefaults.set(nickname, forKey: nicknameKey) + userDefaults.synchronize() // Force immediate save + } + + func sendMessage(_ content: String) { + guard !content.isEmpty else { return } + + // Add message to local display + let message = BitchatMessage( + sender: nickname, + content: content, + timestamp: Date(), + isRelay: false, + originalSender: nil + ) + messages.append(message) + + // Send via mesh + meshService.sendMessage(content) + } + + + func formatTimestamp(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } + + func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString { + var result = AttributedString() + + let isDark = colorScheme == .dark + let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0) + let secondaryColor = primaryColor.opacity(0.7) + + let timestamp = AttributedString("[\(formatTimestamp(message.timestamp))] ") + var timestampStyle = AttributeContainer() + timestampStyle.foregroundColor = secondaryColor + timestampStyle.font = .system(size: 12, design: .monospaced) + result.append(timestamp.mergingAttributes(timestampStyle)) + + if message.sender == "system" { + let content = AttributedString("* \(message.content) *") + var contentStyle = AttributeContainer() + contentStyle.foregroundColor = secondaryColor + contentStyle.font = .system(size: 12, design: .monospaced).italic() + result.append(content.mergingAttributes(contentStyle)) + } else { + let sender = AttributedString("<\(message.sender)> ") + var senderStyle = AttributeContainer() + senderStyle.foregroundColor = message.sender == nickname ? primaryColor : primaryColor.opacity(0.9) + senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced) + result.append(sender.mergingAttributes(senderStyle)) + + let content = AttributedString(message.content) + var contentStyle = AttributeContainer() + contentStyle.font = .system(size: 14, design: .monospaced) + contentStyle.foregroundColor = isDark ? Color.white : Color.black + result.append(content.mergingAttributes(contentStyle)) + + if message.isRelay, let originalSender = message.originalSender { + let relay = AttributedString(" (via \(originalSender))") + var relayStyle = AttributeContainer() + relayStyle.foregroundColor = secondaryColor + relayStyle.font = .system(size: 11, design: .monospaced) + result.append(relay.mergingAttributes(relayStyle)) + } + } + + return result + } +} + +extension ChatViewModel: BitchatDelegate { + func didReceiveMessage(_ message: BitchatMessage) { + messages.append(message) + + #if os(iOS) + // Haptic feedback for new messages + let impactFeedback = UIImpactFeedbackGenerator(style: .light) + impactFeedback.impactOccurred() + #endif + } + + func didConnectToPeer(_ peerID: String) { + isConnected = true + let systemMessage = BitchatMessage( + sender: "system", + content: "\(peerID) has joined", + timestamp: Date(), + isRelay: false, + originalSender: nil + ) + messages.append(systemMessage) + } + + func didDisconnectFromPeer(_ peerID: String) { + let systemMessage = BitchatMessage( + sender: "system", + content: "\(peerID) has left", + timestamp: Date(), + isRelay: false, + originalSender: nil + ) + messages.append(systemMessage) + } + + func didUpdatePeerList(_ peers: [String]) { + connectedPeers = peers + isConnected = !peers.isEmpty + + // If we just disconnected from all peers, ensure UI updates + if peers.isEmpty && isConnected { + isConnected = false + } + } +} \ No newline at end of file diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift new file mode 100644 index 00000000..9b21a8ae --- /dev/null +++ b/bitchat/Views/ContentView.swift @@ -0,0 +1,177 @@ +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var viewModel: ChatViewModel + @State private var messageText = "" + @FocusState private var isTextFieldFocused: Bool + @Environment(\.colorScheme) var colorScheme + @State private var showPeerList = false + + private var backgroundColor: Color { + colorScheme == .dark ? Color.black : Color.white + } + + private var textColor: Color { + colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) + } + + private var secondaryTextColor: Color { + colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8) + } + + var body: some View { + VStack(spacing: 0) { + headerView + Divider() + messagesView + Divider() + inputView + } + .background(backgroundColor) + .foregroundColor(textColor) + #if os(macOS) + .frame(minWidth: 600, minHeight: 400) + #endif + } + + private var headerView: some View { + HStack { + Text("bitchat") + .font(.system(size: 18, weight: .medium, design: .monospaced)) + .foregroundColor(textColor) + + Spacer() + + Menu { + if viewModel.connectedPeers.isEmpty { + Text("No peers connected") + .font(.system(size: 12, design: .monospaced)) + } else { + let peerNicknames = viewModel.meshService.getPeerNicknames() + ForEach(viewModel.connectedPeers, id: \.self) { peerID in + if let displayName = peerNicknames[peerID], displayName != peerID { + // Only show if we have a real nickname + Label(displayName, systemImage: "person.fill") + .font(.system(size: 12, design: .monospaced)) + } + } + } + } label: { + HStack(spacing: 4) { + Circle() + .fill(viewModel.isConnected ? textColor : Color.red) + .frame(width: 8, height: 8) + + HStack(spacing: 0) { + Text(viewModel.isConnected ? "\(viewModel.connectedPeers.count) \(viewModel.connectedPeers.count == 1 ? "peer" : "peers")" : "Scanning...") + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(secondaryTextColor) + Text(Image(systemName: "chevron.down")) + .font(.system(size: 10)) + .foregroundColor(secondaryTextColor) + .baselineOffset(-1) + } + } + .contentShape(Rectangle()) // Make entire area tappable + } + .buttonStyle(.plain) + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + + Spacer() + + HStack(spacing: 4) { + Text("nick:") + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(secondaryTextColor) + + TextField("nickname", text: $viewModel.nickname) + .textFieldStyle(.plain) + .font(.system(size: 12, design: .monospaced)) + .frame(maxWidth: 100) + .foregroundColor(textColor) + .onChange(of: viewModel.nickname) { _ in + viewModel.saveNickname() + } + .onSubmit { + viewModel.saveNickname() + } + } + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(backgroundColor.opacity(0.95)) + } + + private var messagesView: some View { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 2) { + ForEach(Array(viewModel.messages.enumerated()), id: \.offset) { index, message in + VStack(alignment: .leading, spacing: 4) { + Text(viewModel.formatMessage(message, colorScheme: colorScheme)) + .font(.system(size: 14, design: .monospaced)) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + + } + .padding(.horizontal, 12) + .padding(.vertical, 2) + .id(index) + } + } + .padding(.vertical, 8) + } + .background(backgroundColor) + .onChange(of: viewModel.messages.count) { _ in + withAnimation { + proxy.scrollTo(viewModel.messages.count - 1, anchor: .bottom) + } + } + } + } + + private var inputView: some View { + HStack(spacing: 4) { + Text("[\(viewModel.formatTimestamp(Date()))]") + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(secondaryTextColor) + .lineLimit(1) + .fixedSize() + .padding(.leading, 12) + + Text("<\(viewModel.nickname)>") + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundColor(textColor) + .lineLimit(1) + .fixedSize() + + TextField("", text: $messageText) + .textFieldStyle(.plain) + .font(.system(size: 14, design: .monospaced)) + .foregroundColor(textColor) + .focused($isTextFieldFocused) + .onSubmit { + sendMessage() + } + + Button(action: sendMessage) { + Image(systemName: "arrow.right.circle.fill") + .font(.system(size: 16)) + .foregroundColor(textColor) + } + .buttonStyle(.plain) + .padding(.trailing, 12) + } + .padding(.vertical, 10) + .background(backgroundColor.opacity(0.95)) + .onAppear { + isTextFieldFocused = true + } + } + + private func sendMessage() { + viewModel.sendMessage(messageText) + messageText = "" + } +} \ No newline at end of file diff --git a/project.yml b/project.yml new file mode 100644 index 00000000..672340a0 --- /dev/null +++ b/project.yml @@ -0,0 +1,46 @@ +name: bitchat +options: + bundleIdPrefix: com.bitchat + deploymentTarget: + iOS: 16.0 + macOS: 13.0 + createIntermediateGroups: true + +settings: + MARKETING_VERSION: 1.0.0 + CURRENT_PROJECT_VERSION: 1 + DEVELOPMENT_TEAM: "" + +targets: + bitchat: + type: application + platform: [iOS, macOS] + sources: + - bitchat + info: + path: bitchat/Info.plist + properties: + CFBundleDisplayName: bitchat + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) + NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. + NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby. + UILaunchScreen: + UIColorName: Black + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + settings: + PRODUCT_BUNDLE_IDENTIFIER: com.bitchat.app + INFOPLIST_FILE: bitchat/Info.plist + ENABLE_PREVIEWS: YES + SWIFT_VERSION: 5.0 + IPHONEOS_DEPLOYMENT_TARGET: 16.0 + MACOSX_DEPLOYMENT_TARGET: 13.0 + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: YES + CODE_SIGN_STYLE: Automatic + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 00000000..72b636a2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +echo "bitchat Setup Script" +echo "===================" + +# Check if XcodeGen is installed +if command -v xcodegen &> /dev/null; then + echo "✓ XcodeGen found" + echo "Generating Xcode project..." + xcodegen generate + echo "✓ Project generated successfully" + echo "" + echo "To open the project, run:" + echo " open bitchat.xcodeproj" +else + echo "⚠️ XcodeGen not found" + echo "" + echo "You have several options:" + echo "1. Install XcodeGen:" + echo " brew install xcodegen" + echo "" + echo "2. Open with Swift Package Manager:" + echo " open Package.swift" + echo "" + echo "3. Create a new Xcode project manually and add the source files" +fi + +echo "" +echo "Project Structure:" +echo "- bitchat/ Main source files" +echo " - BitchatApp.swift App entry point" +echo " - Views/ SwiftUI views" +echo " - ViewModels/ View models" +echo " - Services/ Bluetooth and encryption" +echo " - Protocols/ Protocol definitions" +echo "" +echo "Remember to:" +echo "1. Enable Bluetooth in device settings" +echo "2. Run on physical devices (Bluetooth doesn't work in simulator)" +echo "3. Test with multiple devices for mesh functionality" \ No newline at end of file