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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
| grep -E "error:|warning: .*\.swift|Test Case .*(passed|failed)|Test .*(passed|failed) after|Executed [0-9]+ tests?|\*\* TEST"

# A run that executes no tests also ends in TEST SUCCEEDED. Read the counts from the result bundle.
# 209 = 208 unit tests (the live-server tests count while skipped without TGARCHIVE_DEMO_URL) plus the
# 219 = 218 unit tests (the live-server tests count while skipped without TGARCHIVE_DEMO_URL) plus the
# launch UI test; raise it when a stage adds tests.
- name: Gate on the test count
run: |
Expand All @@ -59,7 +59,7 @@ jobs:
s = json.load(sys.stdin)
total, failed = s.get("totalTestCount", 0), s.get("failedTests", 0)
print(f"totalTestCount={total} failedTests={failed}")
sys.exit(0 if total >= 209 and failed == 0 else 1)
sys.exit(0 if total >= 219 and failed == 0 else 1)
'

- name: Build the Release configuration
Expand Down
170 changes: 170 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Release to TestFlight

# Push a tag vX.Y.Z that matches MARKETING_VERSION in project.yml, on a commit of main whose CI passed.
# Each upload needs a CURRENT_PROJECT_VERSION higher than the last one App Store Connect received.
on:
push:
tags: ["v*"]

permissions:
contents: read
actions: read

concurrency:
group: release
cancel-in-progress: false

jobs:
testflight:
name: Archive and upload
runs-on: macos-latest
timeout-minutes: 60
env:
TEAM_ID: 624WUVM8B4
BUNDLE_ID: io.github.geiserx.tgarchive
APP_PROFILE_NAME: TG Archive App Store
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Check the tag is on main and CI passed there
env:
GH_TOKEN: ${{ github.token }}
run: |
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on main"
exit 1
fi
conclusion=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ci.yml --branch main \
--commit "$GITHUB_SHA" --json conclusion --jq '.[0].conclusion // "none"')
if [ "$conclusion" != success ]; then
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "::error::CI on main for $GITHUB_SHA is '$conclusion', not success"
exit 1
fi

- name: Check the tag matches project.yml
run: |
VERSION=$(awk '/^ *MARKETING_VERSION:/ {print $2; exit}' project.yml)
if [ "v$VERSION" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match MARKETING_VERSION $VERSION in project.yml"
exit 1
fi

- name: Check the signing secrets exist
env:
APPSTORE_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
APPSTORE_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
APPSTORE_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
DIST_CERTIFICATE_P12: ${{ secrets.DIST_CERTIFICATE_P12 }}
DIST_CERTIFICATE_PASSWORD: ${{ secrets.DIST_CERTIFICATE_PASSWORD }}
PROFILE_APP: ${{ secrets.PROFILE_APP }}
run: |
missing=0
for name in APPSTORE_ISSUER_ID APPSTORE_KEY_ID APPSTORE_PRIVATE_KEY DIST_CERTIFICATE_P12 \
DIST_CERTIFICATE_PASSWORD PROFILE_APP; do
if [ -z "${!name}" ]; then echo "::error::Secret $name is not set"; missing=1; fi
done
exit $missing

- name: Install the distribution certificate
env:
DIST_CERTIFICATE_P12: ${{ secrets.DIST_CERTIFICATE_P12 }}
DIST_CERTIFICATE_PASSWORD: ${{ secrets.DIST_CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -hex 24)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 3600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
printf '%s' "$DIST_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/dist.p12"
security import "$RUNNER_TEMP/dist.p12" -k "$KEYCHAIN" -P "$DIST_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
rm -f "$RUNNER_TEMP/dist.p12"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null
# Put the new keychain first and keep the existing ones searchable (one path per word).
existing=$(security list-keychains -d user | tr -d '"' | xargs)
# shellcheck disable=SC2086
security list-keychains -d user -s "$KEYCHAIN" $existing
security find-identity -v -p codesigning "$KEYCHAIN"

- name: Install the provisioning profile
env:
PROFILE_APP: ${{ secrets.PROFILE_APP }}
run: |
printf '%s' "$PROFILE_APP" | base64 --decode > "$RUNNER_TEMP/profile.mobileprovision"
security cms -D -i "$RUNNER_TEMP/profile.mobileprovision" > "$RUNNER_TEMP/profile.plist"
name=$(plutil -extract Name raw "$RUNNER_TEMP/profile.plist")
if [ "$name" != "$APP_PROFILE_NAME" ]; then
echo "::error::PROFILE_APP is the profile '$name', not '$APP_PROFILE_NAME'"
exit 1
fi
uuid=$(plutil -extract UUID raw "$RUNNER_TEMP/profile.plist")
for dir in "$HOME/Library/MobileDevice/Provisioning Profiles" \
"$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"; do
mkdir -p "$dir"
cp "$RUNNER_TEMP/profile.mobileprovision" "$dir/$uuid.mobileprovision"
done
rm -f "$RUNNER_TEMP/profile.mobileprovision" "$RUNNER_TEMP/profile.plist"

- name: Install XcodeGen and generate the project
run: |
brew install xcodegen
xcodegen generate

- name: Archive
run: |
set -o pipefail
xcodebuild archive \
-project TGArchive.xcodeproj -scheme TGArchive -configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$RUNNER_TEMP/TGArchive.xcarchive" \
| grep -E "error:|warning: .*\.swift|\*\* ARCHIVE"

- name: Export the IPA
run: |
cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
<?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>app-store-connect</string>
<key>destination</key><string>export</string>
<key>teamID</key><string>${TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
<key>signingCertificate</key><string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>${BUNDLE_ID}</key><string>${APP_PROFILE_NAME}</string>
</dict>
<key>uploadSymbols</key><true/>
</dict>
</plist>
PLIST
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/TGArchive.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/export"
ls -la "$RUNNER_TEMP/export"

- name: Upload to TestFlight
env:
APPSTORE_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
APPSTORE_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
APPSTORE_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
run: |
mkdir -p "$HOME/.appstoreconnect/private_keys"
KEY="$HOME/.appstoreconnect/private_keys/AuthKey_${APPSTORE_KEY_ID}.p8"
printf '%s\n' "$APPSTORE_PRIVATE_KEY" > "$KEY"
chmod 600 "$KEY"
xcrun altool --upload-app -t ios -f "$RUNNER_TEMP/export/TGArchive.ipa" \
--apiKey "$APPSTORE_KEY_ID" --apiIssuer "$APPSTORE_ISSUER_ID"

- name: Remove signing material
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/signing.keychain-db" 2>/dev/null || true
rm -rf "$HOME/.appstoreconnect/private_keys"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/"*.mobileprovision
rm -f "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/"*.mobileprovision
59 changes: 55 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,63 @@
<img src="docs/images/banner.svg" alt="TG-Archive-iOS" width="900"/>
</h1>

The read-only iOS client for a self-hosted [Telegram-Archive](https://github.com/GeiserX/Telegram-Archive) backup. Point it at your own viewer, sign in with a viewer account or a share link, and read your archived chats, search them and open their media on the phone.
TG Archive is the iOS client for [Telegram-Archive](https://github.com/GeiserX/Telegram-Archive), the open-source server you run yourself to back up your chats. Point it at your server, sign in with a viewer account or a share link, and read, search and open your archived chats natively on the iPhone.

TG Archive is the iOS app of the Telegram-Archive project and is independent of Telegram. It talks only to the Telegram-Archive server you run, through its documented HTTP API.
TG Archive is part of the open-source Telegram-Archive project and is independent of Telegram; the server you run uses the Telegram API.

Work in progress: nothing is on TestFlight yet.
Status: v0.1.0 is being prepared for TestFlight and the App Store.

## Features

- **Your server, nothing else.** The app talks only to the address you enter, through the server's [documented API](https://github.com/GeiserX/Telegram-Archive/blob/main/docs/reference/api.md). No analytics, ads or tracking ([privacy policy](PRIVACY.md)).
- **Three ways in.** A viewer account, a pasted share link, or nothing at all when the server runs in anonymous mode.
- **Chat list.** Previews, folders, archived chats, forum topics, server-side title search, pull to refresh.
- **Every message kind.** Formatted text, replies, forwards, reactions, polls, locations, venues, contacts, stickers, pinned messages, service rows, and marks for messages that were edited or deleted after the backup saved them.
- **Media.** Zoomable photos, videos and round videos through AVKit, voice notes and audio with their transcripts, documents in Quick Look, and the share sheet when the login allows downloads.
- **Search.** Every message the login can see, with a jump to the hit inside its chat.
- **One clean session.** The session cookie lives in the Keychain on this device. The app holds one server session and ends it when you sign out, so it never pushes out a browser session.
- **Native.** SwiftUI, Swift 6, iOS 18, iPhone, English and Spanish, no third-party packages, no web views.

## Quick start

1. Run a Telegram-Archive server with its viewer enabled. Its [documentation](https://github.com/GeiserX/Telegram-Archive) covers the install, viewer accounts and share links. A server outside your local network needs https.
2. Install TG Archive (TestFlight and App Store links will be here once it ships), or build it as below.
3. Enter your server's address, or paste a share link, and sign in.

### Build from source

You need a Mac with Xcode and [XcodeGen](https://github.com/yonaskolb/XcodeGen).

```bash
git clone https://github.com/GeiserX/TG-Archive-iOS.git
cd TG-Archive-iOS
brew install xcodegen
xcodegen generate # writes TGArchive.xcodeproj from project.yml
open TGArchive.xcodeproj
```

Run the tests on a simulator:

```bash
xcodebuild test -project TGArchive.xcodeproj -scheme TGArchive \
-destination "platform=iOS Simulator,id=$(scripts/pick-simulator.sh)" \
-parallel-testing-enabled NO
```

### Try it without your own archive

`scripts/demo-server.sh start` runs a demo Telegram-Archive viewer on `http://127.0.0.1:8000` with an invented archive, and prints its logins and share link. It needs `git` and `uv` (`brew install uv`). `stop`, `status` and `reset` do what they say.

## Documentation

- [Design](docs/DESIGN.md): the screens, the API routes each one uses, the session rules and the App Review plan.
- [Agent and contributor notes](AGENTS.md): how to build, test and land a change.
- [App Store text](docs/app-store/): the store name, subtitle, keywords and description in English and Spanish, and the App Review notes template.
- [Privacy policy](PRIVACY.md) and [security policy](SECURITY.md).
- [Telegram-Archive API reference](https://github.com/GeiserX/Telegram-Archive/blob/main/docs/reference/api.md).

Releases: merge to `main`, wait for CI on that commit, then push a tag `vX.Y.Z` that matches `MARKETING_VERSION` in `project.yml`. [`release.yml`](.github/workflows/release.yml) archives, signs and uploads the build to TestFlight.

## License

GPL-3.0-or-later with an App Store distribution permission; see [LICENSE](LICENSE) and [NOTICE](NOTICE).
GPL-3.0-or-later with an App Store distribution permission; see [LICENSE](LICENSE) and [NOTICE](NOTICE). Source: https://github.com/GeiserX/TG-Archive-iOS
Loading
Loading