Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .github/ExportOptions.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>5HR8E5CWR7</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
54 changes: 54 additions & 0 deletions .github/scripts/notarize-macos-zip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash
# Submit a zip of Jared.app for notarization and staple the app bundle.
set -euo pipefail

APP="${1:?Usage: notarize-macos-zip.sh path/to/Jared.app}"
PROFILE="${NOTARY_KEYCHAIN_PROFILE:-notarytool}"

if [ ! -d "$APP" ]; then
echo "Not a bundle: $APP" >&2
exit 1
fi

for var in APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!var:-}" ]; then
echo "Missing $var — skipping notarization." >&2
exit 0
fi
done

if ! xcrun notarytool history --keychain-profile "$PROFILE" &>/dev/null; then
echo "Storing notary credentials in keychain profile: $PROFILE"
xcrun notarytool store-credentials "$PROFILE" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD"
fi

submit_zip="$(mktemp -t jared-notarize).zip"
trap 'rm -f "$submit_zip"' EXIT

ditto -c -k --keepParent "$APP" "$submit_zip"

echo "Submitting $submit_zip for notarization..."
submit_output="$(mktemp)"
if ! xcrun notarytool submit "$submit_zip" --keychain-profile "$PROFILE" --wait 2>&1 | tee "$submit_output"; then
echo "notarytool submit failed." >&2
exit 1
fi

if grep -q 'status: Invalid' "$submit_output"; then
submission_id="$(sed -n 's/^[[:space:]]*id:[[:space:]]*//p' "$submit_output" | head -1)"
echo "Notarization rejected (Invalid)." >&2
if [ -n "$submission_id" ]; then
xcrun notarytool log "$submission_id" --keychain-profile "$PROFILE" || true
fi
exit 1
fi
rm -f "$submit_output"

echo "Stapling notarization ticket to $APP..."
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"

echo "Notarization complete: $APP"
60 changes: 60 additions & 0 deletions .github/scripts/release-macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/bash

set -euxo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"

BUILD_DIR="$ROOT/build"
ARCHIVE_PATH="$BUILD_DIR/Jared.xcarchive"
EXPORT_PATH="$BUILD_DIR/export"
EXPORT_OPTIONS="$ROOT/.github/ExportOptions.plist"

if [[ -n "${GITHUB_REF_NAME:-}" && "$GITHUB_REF_NAME" == v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' JaredUI/Info.plist)"
fi

ARTIFACT_FILENAME="Jared-${VERSION}.zip"

rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"

archive_args=(
-project Jared.xcodeproj
-scheme JaredUI
-configuration Release
-archivePath "$ARCHIVE_PATH"
archive
DEVELOPMENT_TEAM=5HR8E5CWR7
)

if [ -n "${MACOS_CODESIGN_IDENTITY:-}" ]; then
archive_args+=(
CODE_SIGN_IDENTITY="$MACOS_CODESIGN_IDENTITY"
CODE_SIGN_STYLE=Manual
)
else
archive_args+=(CODE_SIGN_STYLE=Automatic)
fi

xcodebuild "${archive_args[@]}"

xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS"

APP_PATH="$EXPORT_PATH/Jared.app"
if [ ! -d "$APP_PATH" ]; then
echo "Expected exported app at $APP_PATH" >&2
exit 1
fi

bash "$ROOT/.github/scripts/notarize-macos-zip.sh" "$APP_PATH"

ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ARTIFACT_FILENAME"

ls -la "$ARTIFACT_FILENAME"
du -hs "$APP_PATH" "$ARTIFACT_FILENAME"
13 changes: 13 additions & 0 deletions .github/scripts/test-macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -euxo pipefail

xcodebuild -project Jared.xcodeproj -scheme JaredFramework build \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO

xcodebuild -project Jared.xcodeproj -scheme JaredTests test \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO
13 changes: 5 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
name: JaredTests
if: vars.MACOS_CI_ENABLED == 'true'
runs-on:
- self-hosted
- macOS
Expand All @@ -30,11 +34,4 @@ jobs:
xcodebuild -version

- name: Run unit tests
run: |
xcodebuild \
-project Jared.xcodeproj \
-scheme JaredTests \
test \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO
run: bash ./.github/scripts/test-macos.sh
63 changes: 63 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: release

on:
push:
tags:
- 'v*'
workflow_dispatch:

permissions:
contents: write

jobs:
build-macos:
if: vars.MACOS_CI_ENABLED == 'true' || github.event_name == 'workflow_dispatch'
runs-on:
- self-hosted
- macOS

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Build release
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }}
run: bash ./.github/scripts/release-macos.sh

- name: Upload Jared.app zip
uses: actions/upload-artifact@v7
with:
name: jared-macos
path: Jared-*.zip
if-no-files-found: error

release:
name: Create release and upload artifacts
runs-on: ubuntu-latest
needs: [build-macos]
if: needs.build-macos.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Download macOS artifact
uses: actions/download-artifact@v7
with:
name: jared-macos

- name: Display structure of downloaded files
run: ls -la

- name: Release
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: false
fail_on_unmatched_files: false
files: Jared-*.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
xcodebuild -project Jared.xcodeproj -scheme JaredTests test ONLY_ACTIVE_ARCH=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
```

Or run the CI script (builds `JaredFramework` first):
```
bash ./.github/scripts/test-macos.sh
```

**GitHub Actions** (`.github/workflows/test.yml`): macOS unit tests run on a **self-hosted** runner only when the repository variable `MACOS_CI_ENABLED` is `true` (Settings → Secrets and variables → Actions → Variables). The runner must have labels `self-hosted` and `macOS`. When the variable is unset or not `true`, the job is skipped — flip it on when your Mac runner is online.

**Release** (`.github/workflows/release.yml`): triggers on `v*` tags or **workflow_dispatch**. Builds on the same self-hosted Mac when `MACOS_CI_ENABLED` is `true` (always runs on manual dispatch). Script: `bash ./.github/scripts/release-macos.sh` (archive → export → optional notarize → zip). Uploads a draft GitHub release with `Jared-<version>.zip`.

Optional repository **secrets** for signing/notarization: `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`, `MACOS_CODESIGN_IDENTITY`. Notarization uses keychain profile `notarytool` (or creates it from those secrets). Without Apple secrets, export still runs if Developer ID is in the runner keychain; notarization is skipped.

**"No such module 'JaredFramework'" errors**: This is normal when Xcode hasn't built the framework target yet. Build the `JaredFramework` scheme first, then build `Jared`/`JaredTests`. These LSP errors are stale and not real compile failures.

## Architecture Overview
Expand Down
4 changes: 4 additions & 0 deletions Jared.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
B32DE51C24D552D300AB3A71 /* MockPluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32DE51B24D552D300AB3A71 /* MockPluginManager.swift */; };
B332787A1CB2CB1300BBDD3A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B33278791CB2CB1300BBDD3A /* AppDelegate.swift */; };
EC000001000000000000AA0B /* MenuBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC000001000000000000AA0C /* MenuBarManager.swift */; };
EC000001000000000000AD01 /* SystemSleepPreventer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC000001000000000000AD02 /* SystemSleepPreventer.swift */; };
B332787C1CB2CB1300BBDD3A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B332787B1CB2CB1300BBDD3A /* ViewController.swift */; };
B332787E1CB2CB1300BBDD3A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B332787D1CB2CB1300BBDD3A /* Assets.xcassets */; };
B33278811CB2CB1300BBDD3A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B332787F1CB2CB1300BBDD3A /* Main.storyboard */; };
Expand Down Expand Up @@ -197,6 +198,7 @@
B33278771CB2CB1300BBDD3A /* Jared.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jared.app; sourceTree = BUILT_PRODUCTS_DIR; };
B33278791CB2CB1300BBDD3A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
EC000001000000000000AA0C /* MenuBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarManager.swift; sourceTree = "<group>"; };
EC000001000000000000AD02 /* SystemSleepPreventer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSleepPreventer.swift; sourceTree = "<group>"; };
B332787B1CB2CB1300BBDD3A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
B332787D1CB2CB1300BBDD3A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B33278801CB2CB1300BBDD3A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
Expand Down Expand Up @@ -362,6 +364,7 @@
children = (
B33278791CB2CB1300BBDD3A /* AppDelegate.swift */,
EC000001000000000000AA0C /* MenuBarManager.swift */,
EC000001000000000000AD02 /* SystemSleepPreventer.swift */,
B375A65821EB14470073F09F /* AboutViewController.swift */,
EC000001000000000000AC02 /* LLMSettingsViewController.swift */,
EC000001000000000000AC04 /* WebhookManagementWindowController.swift */,
Expand Down Expand Up @@ -800,6 +803,7 @@
B3B844CB24DA5D1700DC162A /* DiskAccessDelegate.swift in Sources */,
B332787A1CB2CB1300BBDD3A /* AppDelegate.swift in Sources */,
EC000001000000000000AA0B /* MenuBarManager.swift in Sources */,
EC000001000000000000AD01 /* SystemSleepPreventer.swift in Sources */,
B3BF17C4244EBF4900CC44C5 /* InternalModule.swift in Sources */,
B3BF17C2244EBE9A00CC44C5 /* Router.swift in Sources */,
B375A65921EB14470073F09F /* AboutViewController.swift in Sources */,
Expand Down
40 changes: 30 additions & 10 deletions Jared/DatabaseHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DatabaseHandler {
message.expressive_send_style_id,
message.associated_message_type,
message.associated_message_guid, message.guid, destination_caller_id,
message.attributedBody
message.handle_id, message.attributedBody
FROM message LEFT JOIN handle
ON message.handle_id = handle.ROWID
WHERE message.ROWID > ? ORDER BY message.ROWID ASC
Expand All @@ -55,6 +55,8 @@ class DatabaseHandler {
private var walSource: DispatchSourceFileSystemObject?
private var walContinuation: AsyncStream<Void>.Continuation?
private var backgroundTask: Task<Void, Never>?
/// Inbound rows deferred once while Messages backfills `handle_id` / handle join.
private var deferredIncomingRowIDs = Set<String>()

/// Triggers an immediate poll, bypassing the refresh interval. Used in tests.
func triggerImmediateQuery() {
Expand Down Expand Up @@ -205,8 +207,10 @@ class DatabaseHandler {
var People = [Person]()
var groupName: String?
var chatGUID: String?
var foundRow = false

while sqlite3_step(statement) == SQLITE_ROW {
foundRow = true
guard let idcString = sqlite3_column_text(statement, 0) else {
break
}
Expand All @@ -219,6 +223,10 @@ class DatabaseHandler {
People.append(Person(givenName: givenName, handle: handle, isMe: false))
}

guard foundRow else {
return nil
}

return Group(name: groupName, handle: chatGUID ?? chatHandle, participants: People)
}

Expand Down Expand Up @@ -298,7 +306,7 @@ class DatabaseHandler {
while sqlite3_step(statement) == SQLITE_ROW {
var senderHandleOptional = unwrapStringColumn(for: statement, at: 0)
let rawText = unwrapStringColumn(for: statement, at: 1)
let textOptional = (rawText?.isEmpty == false ? rawText : nil) ?? extractAttributedBodyText(for: statement, at: 13)
let textOptional = (rawText?.isEmpty == false ? rawText : nil) ?? extractAttributedBodyText(for: statement, at: 14)
let rowID = unwrapStringColumn(for: statement, at: 2)
let roomName = unwrapStringColumn(for: statement, at: 3)
let isFromMe = sqlite3_column_int(statement, 4) == 1
Expand All @@ -310,17 +318,25 @@ class DatabaseHandler {
let associatedMessageGUID = unwrapStringColumn(for: statement, at: 10)
let guid = unwrapStringColumn(for: statement, at: 11)
let destinationCallerId = unwrapStringColumn(for: statement, at: 12)

querySinceID = rowID;
let messageHandleId = sqlite3_column_int(statement, 13)

guard !isFromMe else { continue }

if senderHandleOptional == nil {
let raw = destinationCallerId ?? destinationOptional
senderHandleOptional = raw?.hasPrefix("mailto:") == true ? String(raw!.dropFirst(7)) : raw
if !isFromMe, senderHandleOptional == nil {
if messageHandleId > 0 {
// handle_id points at a missing row — never substitute destination_caller_id.
querySinceID = rowID
continue
}
// handle_id still 0: Messages may not have linked the sender yet. Defer one poll
// so we don't treat a remote message as self-chat (destination_caller_id as sender).
if let id = rowID, !deferredIncomingRowIDs.contains(id) {
deferredIncomingRowIDs.insert(id)
break
}
senderHandleOptional = destinationCallerId ?? destinationOptional
}

guard let senderHandle = senderHandleOptional, let text = textOptional, let destination = destinationOptional else {
querySinceID = rowID
continue
}

Expand All @@ -344,6 +360,10 @@ class DatabaseHandler {
sendStyle: sendStyle, associatedMessageType: Int(associatedMessageType), associatedMessageGUID: associatedMessageGUID)

router?.route(message: message)
querySinceID = rowID
if let rowID {
deferredIncomingRowIDs.remove(rowID)
}
}

if sqlite3_finalize(statement) != SQLITE_OK {
Expand Down
1 change: 1 addition & 0 deletions Jared/JaredConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct JaredConstants {
static let sendMessageAccess = "SendMessageAccess"
static let fullDiskAccess = "FullDiskAccess"
static let webhookCount = "WebhookCount"
static let preventSystemSleep = "PreventSystemSleep"
static let fullDiskAcccessUrl = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
static let contactsAccessUrl = "x-apple.systempreferences:com.apple.preference.security?Privacy_Contacts"
static let automationAccessUrl = "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"
Expand Down
4 changes: 4 additions & 0 deletions Jared/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class Router : RouterDelegate {
func route(message myMessage: Message) {
messageDelegates.forEach { delegate in delegate.didProcess(message: myMessage) }

if let sender = myMessage.sender as? Person, sender.isMe {
return
}

guard myMessage.body is TextBody || myMessage.action != nil else { return }

let defaults = UserDefaults.standard
Expand Down
Loading