Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
acf4add
Add Telegram settings model and test target
fred-lede Jul 21, 2026
f9c1ec3
Store Telegram credentials in Keychain
fred-lede Jul 21, 2026
6590db1
Add Telegram Bot API client
fred-lede Jul 21, 2026
37c3f73
Harden Telegram request handling
fred-lede Jul 21, 2026
1ae6d1c
Add one-shot camera capture
fred-lede Jul 21, 2026
d589690
Fix camera capture lifecycle
fred-lede Jul 21, 2026
25aacbd
Orchestrate Telegram event notifications
fred-lede Jul 21, 2026
748271c
Strengthen Telegram cancellation coverage
fred-lede Jul 21, 2026
501b709
Add Telegram notification menu
fred-lede Jul 21, 2026
0332798
Document and localize Telegram notifications
fred-lede Jul 21, 2026
7971b0b
Resolve Telegram security review findings
fred-lede Jul 21, 2026
c365e03
Retain in-flight camera delegates safely
fred-lede Jul 21, 2026
43fe273
Warm up camera before Telegram snapshots
fred-lede Jul 21, 2026
2833d4c
Test production camera warm-up wiring
fred-lede Jul 21, 2026
2655047
Add one-shot Mac location provider
fred-lede Jul 22, 2026
767fcb8
Cover Mac location failure boundaries
fred-lede Jul 22, 2026
38aa3a0
Add opt-in Telegram location setting
fred-lede Jul 22, 2026
f7aef44
Send Telegram native location messages
fred-lede Jul 22, 2026
c9cce35
Format Telegram photo location captions
fred-lede Jul 22, 2026
5b8b262
Attach Mac location to Telegram photos
fred-lede Jul 22, 2026
b68b92d
Latch first photo and location callbacks
fred-lede Jul 22, 2026
9671dfb
Wire localized Telegram photo locations
fred-lede Jul 22, 2026
c884498
Keep Telegram PR independent of Traditional Chinese
fred-lede Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 216 additions & 2 deletions BLEUnlock.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions BLEUnlock.xcodeproj/xcshareddata/xcschemes/BLEUnlock.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7E1000163000000000000001"
BuildableName = "BLEUnlockTests.xctest"
BlueprintName = "BLEUnlockTests"
ReferencedContainer = "container:BLEUnlock.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
44 changes: 41 additions & 3 deletions BLEUnlock/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
var unlockedAt = 0.0
var inScreensaver = false
var lastRSSI: Int? = nil
let telegramSettings = TelegramSettings(
secrets: KeychainStore(service: "jp.sone.BLEUnlock.telegram")
)
let macLocationProvider = CoreMacLocationProvider()
lazy var telegramService: TelegramNotificationHandling = TelegramNotificationService(
settings: telegramSettings,
sender: TelegramNotifier(transport: URLSessionTransport()),
camera: CameraCapture(),
location: macLocationProvider,
reporter: RateLimitedFailureReporter()
)
lazy var telegramMenuController = TelegramMenuController(
settings: telegramSettings,
service: telegramService,
dialogs: AppKitTelegramDialogPresenter(),
locationAuthorization: macLocationProvider
)
private let telegramEventQueue = DispatchQueue(
label: "jp.sone.BLEUnlock.telegram.events",
qos: .utility
)

func menuWillOpen(_ menu: NSMenu) {
if menu == deviceMenu {
Expand Down Expand Up @@ -177,6 +198,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
try? process.run()
}

func dispatchEvent(_ rawValue: String) {
runScript(rawValue)
guard let event = TelegramEvent(rawValue: rawValue) else { return }
let context = TelegramEventContext(
event: event,
hostName: Host.current().localizedName ?? "Mac",
timestamp: Date(),
rssi: lastRSSI
)
telegramEventQueue.async { [weak self] in
self?.telegramService.handle(context)
}
}

func pauseNowPlaying() {
guard prefs.bool(forKey: "pauseItunes") else { return }
MRMediaRemoteGetNowPlayingApplicationIsPlaying(
Expand Down Expand Up @@ -238,7 +273,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
pauseNowPlaying()
lockOrSaveScreen()
notifyUser(reason)
runScript(reason)
dispatchEvent(reason)
}
manualLock = false
}
Expand Down Expand Up @@ -302,7 +337,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
self.unlockedAt = Date().timeIntervalSince1970
self.fakeKeyStrokes(password)
self.playNowPlaying()
self.runScript("unlocked")
self.dispatchEvent("unlocked")
})
}

Expand Down Expand Up @@ -344,7 +379,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
print("onUnlock")
if Date().timeIntervalSince1970 >= self.unlockedAt + 10 {
if self.ble.unlockRSSI != self.ble.UNLOCK_DISABLED {
self.runScript("intruded")
self.dispatchEvent("intruded")
}
self.playNowPlaying()
}
Expand Down Expand Up @@ -642,6 +677,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenuItemVa
if prefs.bool(forKey: "sleepDisplay") {
item.state = .on
}

item = mainMenu.addItem(withTitle: t("telegram"), action: nil, keyEquivalent: "")
item.submenu = telegramMenuController.menu

mainMenu.addItem(withTitle: t("set_password"), action: #selector(askPassword), keyEquivalent: "")

Expand Down
2 changes: 2 additions & 0 deletions BLEUnlock/BLEUnlock.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions BLEUnlock/Base.lproj/InfoPlist.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"NSCameraUsageDescription" = "BLEUnlock uses the system default camera to photograph a manual unlock and upload the alert to Telegram.";
"NSLocationUsageDescription" = "BLEUnlock gets this Mac's location at capture time and attaches the coordinates and map to the Telegram security notification.";
48 changes: 48 additions & 0 deletions BLEUnlock/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,51 @@
"use_screensaver_to_lock" = "Use Screensaver to Lock";
"wake_on_proximity" = "Wake on Proximity";
"wake_without_unlocking" = "Wake without Unlocking";

"telegram" = "Telegram Notifications";
"telegram_enable" = "Enable Telegram";
"telegram_configure" = "Configure…";
"telegram_test" = "Send Test Notification";
"telegram_events" = "Events";
"telegram_event_away" = "Device is away";
"telegram_event_lost" = "Signal is lost";
"telegram_event_unlocked" = "Unlocked by BLEUnlock";
"telegram_event_intruded" = "Unlocked manually";
"telegram_take_photo" = "Attach a photo for manual unlock";
"telegram_attach_mac_location" = "Attach Mac Location";
"telegram_status_not_configured" = "Not configured";
"telegram_status_enabled" = "Enabled";
"telegram_status_disabled" = "Disabled";
"telegram_bot_token" = "Bot token";
"telegram_chat_id" = "Chat ID";
"telegram_save" = "Save";
"telegram_setup_help" = "Create a bot with @BotFather and copy its token. Send the bot a message, then open getUpdates in the Telegram Bot API and copy the numeric Chat ID from the response.";
"telegram_test_success" = "Test notification sent.";
"telegram_test_failed" = "Could not send the test notification.";
"telegram_camera_privacy" = "BLEUnlock uses the system default camera and uploads one photo to Telegram. The temporary photo is deleted after the attempt; if capture fails, BLEUnlock sends text instead.";
"telegram_error_not_configured" = "Configure a bot token and Chat ID first.";
"telegram_camera_error_denied" = "Camera access was denied.";
"telegram_camera_error_restricted" = "Camera access is restricted.";
"telegram_camera_error_no_camera" = "No camera is available.";
"telegram_camera_error_setup_failed" = "The camera could not be configured.";
"telegram_camera_error_capture_failed" = "The camera could not capture a photo.";
"telegram_camera_error_timeout" = "The camera capture timed out.";
"telegram_camera_error_file_write_failed" = "The captured photo could not be saved.";
"telegram_error_invalid_request" = "The Telegram request could not be created.";
"telegram_error_unreadable_photo" = "The captured photo could not be read.";
"telegram_error_transport" = "Telegram could not be reached.";
"telegram_error_http_status" = "Telegram returned HTTP status %d.";
"telegram_error_rejected" = "Telegram rejected the request.";
"telegram_error_invalid_response" = "Telegram returned an invalid response.";
"telegram_error_settings_unavailable" = "Telegram settings could not be read.";
"telegram_error_file_cleanup" = "The captured photo could not be deleted.";
"telegram_error_keychain_status" = "Keychain operation failed (%d).";
"telegram_failure_notification_subtitle" = "Telegram notification failed";
"telegram_message_time" = "Time";
"telegram_message_rssi" = "RSSI";
"telegram_message_coordinates" = "GPS Coordinates";
"telegram_message_accuracy" = "Location Accuracy";
"telegram_message_map" = "Map";
"telegram_location_unavailable" = "The location at capture time is unavailable.";
"telegram_location_error" = "The Mac location could not be obtained.";
"telegram_location_send_error" = "The photo was sent, but the Telegram map location could not be sent.";
Loading