Compare commits
239 Commits
4dbc8c93d1
...
v4.11.168
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8b57ba217 | ||
| 8d8809d0c7 | |||
|
|
55a0f4f566 | ||
| f488f32d88 | |||
|
|
9154929de9 | ||
| c2cc0d5ed5 | |||
|
|
aa38348fdd | ||
| 02ab544a37 | |||
|
|
3707a6d38a | ||
| fe1f4abf2d | |||
| 147389724e | |||
|
|
d4d4c73c2b | ||
| 73fbc1c77a | |||
|
|
3dc732521f | ||
| b76aa3fe4a | |||
|
|
c0d1e87b8d | ||
| 04d4026e78 | |||
|
|
cfbc59d6a9 | ||
| dfe42fa942 | |||
|
|
6a8397425e | ||
| 641ec26142 | |||
|
|
1eaf246806 | ||
| 0e97e4d3b6 | |||
|
|
1636a8d0c6 | ||
| ac236750aa | |||
|
|
7e747e6c36 | ||
| 1b2cae5b12 | |||
|
|
76921c4e79 | ||
| 688efff7c9 | |||
|
|
9bc377b62a | ||
| 836ccce7a3 | |||
|
|
34c8e6e38c | ||
| e69bda3aa6 | |||
|
|
5d55e5d826 | ||
| b5902b9fc3 | |||
|
|
f60cf0bd3c | ||
| 83dec0ef54 | |||
|
|
40a76f02cd | ||
| 3def3cc456 | |||
|
|
30bd67d43a | ||
| ffeccee680 | |||
|
|
facbeee024 | ||
| 09102c7903 | |||
|
|
fa33ec7ac8 | ||
| c44f5cf14a | |||
|
|
27793d7c4f | ||
| 32b2902ef7 | |||
|
|
5cdb318854 | ||
| a28f05bb88 | |||
|
|
cecfe73b2e | ||
| b458c7918c | |||
|
|
d8660f4822 | ||
| dad83773a6 | |||
|
|
d2d35de46b | ||
| c752ec0b38 | |||
|
|
28752eb56e | ||
| 4f243a0fcf | |||
|
|
9bce616f26 | ||
| f16ab73c9a | |||
|
|
c29a15e073 | ||
| f5fe61e3eb | |||
|
|
a1a752b0fb | ||
| 7769250875 | |||
|
|
b3af9f3185 | ||
| 7906326790 | |||
|
|
3b4babb9d7 | ||
| 4b284ec747 | |||
| e1b4cfb9c8 | |||
|
|
c0266903ec | ||
| 81964d6f14 | |||
|
|
47e025cf55 | ||
| b400070ad0 | |||
| 529694ce66 | |||
|
|
791c3ab4df | ||
| de8d9db795 | |||
|
|
ffaf9ff66a | ||
| f0891ba4cc | |||
| 627b117119 | |||
|
|
b6c6659a5a | ||
| d7a73afebc | |||
| e469d51ba4 | |||
|
|
6ba03277e5 | ||
| fdb2dca794 | |||
|
|
8797bb5a22 | ||
| 4d154fd4f3 | |||
| 42e8fb71ef | |||
|
|
ff2c284984 | ||
| 75be84e472 | |||
| a3c91b306a | |||
|
|
20edc293bb | ||
| 6803eac8fb | |||
| fa5592f1e4 | |||
|
|
78601be6f4 | ||
| c940bdc78c | |||
|
|
9471fe2f5d | ||
| 57f7fe7086 | |||
| a2364f2622 | |||
|
|
df2220581d | ||
| 198eb0fe04 | |||
| fff114abef | |||
|
|
7013f1c22d | ||
| 1dbc7d6cc4 | |||
| f93f109d4a | |||
|
|
f707aa98f5 | ||
| 8a0be09d5d | |||
| a1b86f00ba | |||
|
|
c9f569a2b2 | ||
| 7a736b3adf | |||
| b6ecd16148 | |||
|
|
30d6b8dc74 | ||
| b6bcd8d05b | |||
|
|
0b5fe5f04c | ||
| c629c07bf9 | |||
|
|
fcd7d3e7ea | ||
| db994a5d43 | |||
| fea9ebd501 | |||
|
|
9238d80adb | ||
| a28d3c26b5 | |||
|
|
161030927f | ||
| c431c1d547 | |||
| 739c863601 | |||
|
|
5b6c213101 | ||
| 5acf981bd4 | |||
|
|
14b27717a6 | ||
| bd3e1d5b9c | |||
| a668544c6e | |||
|
|
dd8f4eb47d | ||
| 54cb11e55b | |||
| dec48f6c1d | |||
|
|
8f776f4f7c | ||
| c6aff43467 | |||
| 9f95ef1ecd | |||
|
|
aafda0d437 | ||
| f97a8d4929 | |||
|
|
a8c742376f | ||
| 938ab53d4b | |||
|
|
62352412d3 | ||
| fd33b3f6b7 | |||
| 3c0b4cf108 | |||
|
|
4eb48726ae | ||
| ba17993001 | |||
| cf39f0acd9 | |||
|
|
bce2cbec94 | ||
| f401b5ea3d | |||
| 4a5e3a1691 | |||
|
|
dc50f74971 | ||
| a198eb6896 | |||
| dc9b12dd26 | |||
|
|
6ea0e7a4fd | ||
| 51b13e0fcd | |||
|
|
1d7a48c57e | ||
| 0e34ca2063 | |||
|
|
6248d5843a | ||
| 5af79606c2 | |||
| 3cc31d8e05 | |||
|
|
0f7dd5f55c | ||
| de4f6ebbc9 | |||
|
|
fd25143489 | ||
| 5265674a58 | |||
|
|
39376e0003 | ||
| cf1f7fcec8 | |||
|
|
153e55705a | ||
| 77cdd66027 | |||
| 80580391cb | |||
|
|
6f0988f873 | ||
| 99358803d2 | |||
| 6a347d977f | |||
|
|
c03a4d3f41 | ||
| 50818f1d76 | |||
|
|
0f8c9b83f4 | ||
| 76cae46005 | |||
| ca22449d54 | |||
|
|
dda1041268 | ||
| b4f8f13213 | |||
| 9ad9971a03 | |||
| c38dc7bbf6 | |||
|
|
aaed71e37b | ||
| ed85051480 | |||
| d0524c2bc7 | |||
|
|
8a0f8f5422 | ||
| 8a95d9e67d | |||
| b8a126fd69 | |||
|
|
6b18e5c8ee | ||
| 27c2ce021d | |||
|
|
de6620245c | ||
| 21e895efff | |||
| e9487c3043 | |||
|
|
686749ac55 | ||
| dd790d3336 | |||
|
|
c8ceeccc8c | ||
| a592766e0a | |||
| 7acf8ea877 | |||
|
|
2a87133d14 | ||
| c960bc5410 | |||
| fde412fafa | |||
|
|
476d0c71fc | ||
| df6fa0fa42 | |||
|
|
246521b7b3 | ||
| 8ccc3ecb78 | |||
|
|
19ce4b43ca | ||
| 8ea4c58a5b | |||
| 465fa6b57b | |||
|
|
12f0d7f8d3 | ||
| f11078bb5c | |||
| 4e6835c741 | |||
|
|
4584367041 | ||
| bf71bd9726 | |||
| 8cfc96688e | |||
|
|
eedd040203 | ||
| 8396834b08 | |||
| cb136002b9 | |||
|
|
9a6dc126db | ||
| c9352327eb | |||
| 9985b0931c | |||
|
|
5faf0056d7 | ||
| cf6bedf58a | |||
| 03ecdbb54f | |||
|
|
1222651d17 | ||
| 1f34f726bf | |||
| 5b1bd1cffa | |||
| 050352fd2b | |||
|
|
422b680b18 | ||
| 6510522b6b | |||
| 48a189ed6a | |||
|
|
a72758d9ef | ||
| 7ee178f6e9 | |||
| 50885a9da4 | |||
|
|
5d39692e41 | ||
| de87a55582 | |||
| a3c2510830 | |||
|
|
6b9a57e243 | ||
| 4fa0ce0d4a | |||
| e90b8d43cb | |||
|
|
379a957a40 | ||
| fc18b0b902 | |||
| a4429fe9c2 | |||
|
|
fc4229cea7 | ||
| 69fcfd12f3 | |||
| 843b00166e |
@@ -8,13 +8,15 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
|
# Large AppImage/exe uploads time out on Gitea's actions artifact pipeline.
|
||||||
|
# Build jobs publish assets straight to Gitea/GitHub releases via the REST API.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
increment-version:
|
increment-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.bump.outputs.VERSION }}
|
version: ${{ steps.finalize.outputs.VERSION }}
|
||||||
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
|
should_build: ${{ steps.decide.outputs.SHOULD_BUILD }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -22,9 +24,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Decide version behavior
|
||||||
|
id: decide
|
||||||
|
run: |
|
||||||
|
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||||
|
COMMIT_MSG="${{ github.event.head_commit.message || '' }}"
|
||||||
|
|
||||||
|
SHOULD_BUMP=false
|
||||||
|
SHOULD_BUILD=true
|
||||||
|
|
||||||
|
if [ "${{ github.ref }}" = "refs/heads/main" ] && ! echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||||
|
SHOULD_BUMP=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||||
|
SHOULD_BUILD=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CURRENT_VERSION=$CURRENT" >> $GITHUB_OUTPUT
|
||||||
|
echo "SHOULD_BUMP=$SHOULD_BUMP" >> $GITHUB_OUTPUT
|
||||||
|
echo "SHOULD_BUILD=$SHOULD_BUILD" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "Current version: $CURRENT"
|
||||||
|
echo "Should bump: $SHOULD_BUMP"
|
||||||
|
echo "Should build: $SHOULD_BUILD"
|
||||||
|
|
||||||
- name: Bump patch version
|
- name: Bump patch version
|
||||||
id: bump
|
id: bump
|
||||||
|
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
# Get current version from package.json
|
# Get current version from package.json
|
||||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||||
@@ -45,20 +73,136 @@ jobs:
|
|||||||
grep '"version"' package.json | head -1
|
grep '"version"' package.json | head -1
|
||||||
|
|
||||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Commit and push version bump
|
- name: Commit and push version bump
|
||||||
|
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
git config user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
git config user.email "actions@github.com"
|
git config user.email "actions@github.com"
|
||||||
git add package.json
|
git add package.json
|
||||||
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
||||||
git push
|
for attempt in 1 2 3; do
|
||||||
|
git fetch origin main
|
||||||
|
git rebase origin/main || {
|
||||||
|
git rebase --abort || true
|
||||||
|
echo "Failed to rebase version bump onto latest main"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if git push origin HEAD:main; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Push rejected on attempt ${attempt}, retrying against latest main"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Failed to push version bump after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Finalize version output
|
||||||
|
id: finalize
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ steps.bump.outputs.VERSION }}" ]; then
|
||||||
|
echo "VERSION=${{ steps.bump.outputs.VERSION }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "VERSION=${{ steps.decide.outputs.CURRENT_VERSION }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
prepare-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: increment-version
|
||||||
|
if: always() && needs.increment-version.outputs.should_build == 'true'
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.VERSION }}
|
||||||
|
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Pull latest (after version bump)
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
run: git pull origin main
|
||||||
|
|
||||||
|
- name: Resolve version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
|
||||||
|
VERSION="${{ needs.increment-version.outputs.version }}"
|
||||||
|
else
|
||||||
|
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||||
|
fi
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "TAG_NAME=v$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Create and push tag
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||||
|
TAG_NAME="v${VERSION}"
|
||||||
|
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||||
|
echo "Tag $TAG_NAME already exists"
|
||||||
|
else
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
|
||||||
|
git push origin "$TAG_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Ensure Gitea release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||||
|
TAG_NAME="v${VERSION}"
|
||||||
|
API_BASE="${{ github.server_url }}/api/v1"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
|
||||||
|
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Creating Gitea release ${TAG_NAME}..."
|
||||||
|
RELEASE_ID=$(curl -fsS -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Gitea release id: ${RELEASE_ID}"
|
||||||
|
|
||||||
|
- name: Ensure GitHub release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||||
|
TAG_NAME="v${VERSION}"
|
||||||
|
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||||
|
|
||||||
|
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Creating GitHub release ${TAG_NAME}..."
|
||||||
|
RELEASE_ID=$(curl -fsS -X POST \
|
||||||
|
-H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "GitHub release id: ${RELEASE_ID}"
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
needs: increment-version
|
needs: [increment-version, prepare-release]
|
||||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
if: always() && needs.prepare-release.result == 'success'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -70,7 +214,32 @@ jobs:
|
|||||||
- name: Checkout submodules manually
|
- name: Checkout submodules manually
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
# Use the same host/scheme the runner already used for origin (not public HTTPS).
|
||||||
|
$origin = (git remote get-url origin).Trim()
|
||||||
|
$origin = $origin.TrimEnd('/')
|
||||||
|
if ($origin.EndsWith('.git')) { $origin = $origin.Substring(0, $origin.Length - 4) }
|
||||||
|
$base = $origin.Substring(0, $origin.LastIndexOf('/'))
|
||||||
|
$cinnyUrl = "$base/cinny.git"
|
||||||
|
Write-Host "Rewriting submodule URL to $cinnyUrl"
|
||||||
|
git config submodule.cinny.url $cinnyUrl
|
||||||
|
git submodule sync --recursive
|
||||||
|
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "Submodules checked out at pinned commits."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||||
|
git submodule deinit -f --all
|
||||||
|
git config submodule.cinny.url $cinnyUrl
|
||||||
|
git submodule sync --recursive
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
|
||||||
|
Write-Host "Resolved submodule commit:"
|
||||||
|
git -C cinny rev-parse HEAD
|
||||||
|
|
||||||
- name: Pull latest (after version bump)
|
- name: Pull latest (after version bump)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
@@ -83,11 +252,11 @@ jobs:
|
|||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Install dependencies (root)
|
- name: Install dependencies (root)
|
||||||
run: npm install --prefer-offline
|
run: npm ci --prefer-offline
|
||||||
|
|
||||||
- name: Install dependencies (cinny)
|
- name: Install dependencies (cinny)
|
||||||
working-directory: ./cinny
|
working-directory: ./cinny
|
||||||
run: npm install --prefer-offline
|
run: npm ci --prefer-offline
|
||||||
|
|
||||||
- name: Clean previous builds
|
- name: Clean previous builds
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -96,54 +265,141 @@ jobs:
|
|||||||
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
||||||
|
|
||||||
- name: Build Electron app
|
- name: Build Electron app
|
||||||
run: npm run build:win
|
run: npm run build:local -- --win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
|
||||||
- name: Upload Squirrel Setup (x64)
|
- name: Upload Windows assets to releases
|
||||||
uses: actions/upload-artifact@v3
|
shell: powershell
|
||||||
with:
|
env:
|
||||||
name: Paarrot-Setup-x64.exe
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
path: dist-electron/squirrel-windows/Paarrot-*-win-x64.exe
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$TAG_NAME = "${{ needs.prepare-release.outputs.tag }}"
|
||||||
|
$API_BASE = "${{ github.server_url }}/api/v1"
|
||||||
|
$REPO = "${{ github.repository }}"
|
||||||
|
$GITHUB_REPO = "Paarrot/Paarrot-Desktop"
|
||||||
|
|
||||||
|
function Invoke-JsonCurl([string[]]$CurlArgs) {
|
||||||
|
$raw = & curl.exe @CurlArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "curl failed: $($CurlArgs -join ' ')" }
|
||||||
|
return ($raw | ConvertFrom-Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
$giteaRelease = Invoke-JsonCurl @(
|
||||||
|
"-fsS", "-H", "Authorization: token $env:GITEA_TOKEN",
|
||||||
|
"$API_BASE/repos/$REPO/releases/tags/$TAG_NAME"
|
||||||
|
)
|
||||||
|
$ghRelease = Invoke-JsonCurl @(
|
||||||
|
"-fsS", "-H", "Authorization: token $env:GH_TOKEN",
|
||||||
|
"https://api.github.com/repos/$GITHUB_REPO/releases/tags/$TAG_NAME"
|
||||||
|
)
|
||||||
|
# GitHub returns upload_url like .../assets{?name,label}
|
||||||
|
$ghUploadUrl = [string]$ghRelease.upload_url
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ghUploadUrl)) {
|
||||||
|
throw "GitHub release $($ghRelease.id) has no upload_url"
|
||||||
|
}
|
||||||
|
$ghUploadUrl = $ghUploadUrl -replace '\{\?[^}]*\}', ''
|
||||||
|
Write-Host "GitHub upload base: $ghUploadUrl"
|
||||||
|
|
||||||
|
function Upload-One([string]$File) {
|
||||||
|
$filename = [System.IO.Path]::GetFileName($File)
|
||||||
|
$sizeMb = [math]::Round((Get-Item $File).Length / 1MB, 1)
|
||||||
|
Write-Host "Uploading $filename (${sizeMb} MB)..."
|
||||||
|
|
||||||
|
$giteaAssets = Invoke-JsonCurl @(
|
||||||
|
"-fsS", "-H", "Authorization: token $env:GITEA_TOKEN",
|
||||||
|
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets"
|
||||||
|
)
|
||||||
|
foreach ($asset in @($giteaAssets)) {
|
||||||
|
if ($asset.name -eq $filename) {
|
||||||
|
& curl.exe -fsS -X DELETE -H "Authorization: token $env:GITEA_TOKEN" `
|
||||||
|
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets/$($asset.id)" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& curl.exe -fsS --max-time 0 --retry 5 --retry-delay 10 `
|
||||||
|
-X POST `
|
||||||
|
-H "Authorization: token $env:GITEA_TOKEN" `
|
||||||
|
-H "Content-Type: application/octet-stream" `
|
||||||
|
--data-binary "@$File" `
|
||||||
|
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets?name=$filename" | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Gitea upload failed for $filename" }
|
||||||
|
Write-Host " Gitea: ok"
|
||||||
|
|
||||||
|
$ghAssets = Invoke-JsonCurl @(
|
||||||
|
"-fsS", "-H", "Authorization: token $env:GH_TOKEN",
|
||||||
|
"https://api.github.com/repos/$GITHUB_REPO/releases/$($ghRelease.id)/assets"
|
||||||
|
)
|
||||||
|
foreach ($asset in @($ghAssets)) {
|
||||||
|
if ($asset.name -eq $filename) {
|
||||||
|
& curl.exe -fsS -X DELETE -H "Authorization: token $env:GH_TOKEN" `
|
||||||
|
"https://api.github.com/repos/$GITHUB_REPO/releases/assets/$($asset.id)" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Brace the variable — `$url?name=` is parsed as PowerShell's null-conditional.
|
||||||
|
$ghAssetUrl = "${ghUploadUrl}?name=$([uri]::EscapeDataString($filename))"
|
||||||
|
Write-Host " GitHub URL: $ghAssetUrl"
|
||||||
|
& curl.exe -fsS --max-time 0 --retry 5 --retry-delay 10 `
|
||||||
|
-X POST `
|
||||||
|
-H "Authorization: token $env:GH_TOKEN" `
|
||||||
|
-H "Content-Type: application/octet-stream" `
|
||||||
|
--data-binary "@$File" `
|
||||||
|
$ghAssetUrl | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "GitHub upload failed for $filename" }
|
||||||
|
Write-Host " GitHub: ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-ChildItem -Path dist-electron -File |
|
||||||
|
Where-Object { $_.Name -like 'Paarrot-*-win-x64.exe' -or $_.Name -eq 'latest.yml' } |
|
||||||
|
ForEach-Object { Upload-One $_.FullName }
|
||||||
|
|
||||||
- name: List build output
|
- name: List build output
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
|
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
|
||||||
|
|
||||||
- name: Copy RELEASES file for upload
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
if (Test-Path "dist-electron/squirrel-windows/RELEASES") {
|
|
||||||
Copy-Item "dist-electron/squirrel-windows/RELEASES" -Destination "dist-electron/RELEASES"
|
|
||||||
} else {
|
|
||||||
Write-Error "RELEASES file not found for x64"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Upload Squirrel RELEASES
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: RELEASES
|
|
||||||
path: dist-electron/RELEASES
|
|
||||||
|
|
||||||
- name: Upload Squirrel nupkg (x64)
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: Paarrot-x64.nupkg
|
|
||||||
path: dist-electron/squirrel-windows/*.nupkg
|
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: increment-version
|
needs: [increment-version, prepare-release]
|
||||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
if: always() && needs.prepare-release.result == 'success'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: false
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Checkout submodules manually
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Use the same host/scheme the runner already used for origin (not public HTTPS).
|
||||||
|
ORIGIN_URL=$(git remote get-url origin)
|
||||||
|
ORIGIN_URL=${ORIGIN_URL%/}
|
||||||
|
ORIGIN_URL=${ORIGIN_URL%.git}
|
||||||
|
BASE_URL=${ORIGIN_URL%/*}
|
||||||
|
CINNY_URL="${BASE_URL}/cinny.git"
|
||||||
|
echo "Rewriting submodule URL to ${CINNY_URL}"
|
||||||
|
git config submodule.cinny.url "${CINNY_URL}"
|
||||||
|
git submodule sync --recursive
|
||||||
|
|
||||||
|
if git submodule update --init --recursive; then
|
||||||
|
echo "Submodules checked out at pinned commits."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||||
|
git submodule deinit -f --all || true
|
||||||
|
rm -rf cinny .git/modules/cinny
|
||||||
|
git config submodule.cinny.url "${CINNY_URL}"
|
||||||
|
git submodule sync --recursive
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
|
||||||
|
echo "Resolved submodule commit:"
|
||||||
|
git -C cinny rev-parse HEAD
|
||||||
|
|
||||||
- name: Pull latest (after version bump)
|
- name: Pull latest (after version bump)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
@@ -166,162 +422,130 @@ jobs:
|
|||||||
rm -rf cinny/dist dist-electron
|
rm -rf cinny/dist dist-electron
|
||||||
|
|
||||||
- name: Build Electron app
|
- name: Build Electron app
|
||||||
run: npm run build:linux
|
run: npm run build:local -- --linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
|
||||||
- name: Upload AppImage
|
- name: Upload Linux assets to releases
|
||||||
uses: actions/upload-artifact@v3
|
env:
|
||||||
with:
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
name: Paarrot-Linux-x64.AppImage
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
path: dist-electron/Paarrot-*.AppImage
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ needs.prepare-release.outputs.version }}"
|
||||||
|
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||||
|
API_BASE="${{ github.server_url }}/api/v1"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||||
|
|
||||||
create-release:
|
GITEA_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
|
||||||
|
GH_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
|
||||||
|
GH_UPLOAD_URL=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||||
|
|
||||||
|
upload_one() {
|
||||||
|
local FILE="$1"
|
||||||
|
local FILENAME
|
||||||
|
FILENAME=$(basename "$FILE")
|
||||||
|
echo "Uploading ${FILENAME} ($(du -h "$FILE" | cut -f1))..."
|
||||||
|
|
||||||
|
OLD_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets" \
|
||||||
|
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
|
||||||
|
if [ -n "${OLD_ID:-}" ]; then
|
||||||
|
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets/${OLD_ID}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"${FILE}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets?name=${FILENAME}" \
|
||||||
|
>/dev/null
|
||||||
|
echo " Gitea: ok"
|
||||||
|
|
||||||
|
OLD_GH=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}/assets" \
|
||||||
|
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
|
||||||
|
if [ -n "${OLD_GH:-}" ]; then
|
||||||
|
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/assets/${OLD_GH}" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"${FILE}" \
|
||||||
|
"${GH_UPLOAD_URL}?name=${FILENAME}" \
|
||||||
|
>/dev/null
|
||||||
|
echo " GitHub: ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for FILE in dist-electron/Paarrot-*.AppImage dist-electron/latest-linux.yml; do
|
||||||
|
upload_one "$FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
finalize-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [increment-version, build-windows, build-linux]
|
needs: [increment-version, prepare-release, build-windows, build-linux]
|
||||||
if: always() && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
|
if: always() && needs.prepare-release.result == 'success' && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Cleanup old Gitea releases
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Pull latest (after version bump)
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: git pull origin main
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
# Use version from increment-version job if available, otherwise read from file
|
|
||||||
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
|
|
||||||
VERSION="${{ needs.increment-version.outputs.version }}"
|
|
||||||
else
|
|
||||||
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
|
||||||
fi
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
|
|
||||||
- name: Check if tag exists
|
|
||||||
id: tag_check
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
|
||||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo "Tag v$VERSION already exists"
|
|
||||||
echo "exists=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Tag v$VERSION does not exist"
|
|
||||||
echo "exists=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create and push tag
|
|
||||||
if: steps.tag_check.outputs.exists == 'false'
|
|
||||||
run: |
|
|
||||||
git config user.name "GitHub Actions"
|
|
||||||
git config user.email "actions@github.com"
|
|
||||||
git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}"
|
|
||||||
git push origin "v${{ steps.version.outputs.VERSION }}"
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Prepare release files
|
|
||||||
run: |
|
|
||||||
mkdir -p release-files
|
|
||||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \;
|
|
||||||
ls -la release-files/
|
|
||||||
|
|
||||||
- name: Create or Update Release
|
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
set -euo pipefail
|
||||||
TAG_NAME="v${VERSION}"
|
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||||
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
API_BASE="${{ github.server_url }}/api/v1"
|
||||||
REPO="litruv/cinny-desktop"
|
REPO="${{ github.repository }}"
|
||||||
|
|
||||||
# Check if release exists
|
echo "Cleaning up old Gitea releases (keeping ${TAG_NAME})..."
|
||||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
"${API_BASE}/repos/${REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
for OLD_ID in $OLD_RELEASES; do
|
||||||
echo "Creating new release for ${TAG_NAME}..."
|
[ -n "$OLD_ID" ] || continue
|
||||||
RELEASE_ID=$(curl -s -X POST \
|
OLD_TAG=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||||
-H "Content-Type: application/json" \
|
echo "Deleting Gitea release ${OLD_ID} (${OLD_TAG})..."
|
||||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
|
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" >/dev/null || true
|
||||||
echo "Created release with ID: ${RELEASE_ID}"
|
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
|
||||||
else
|
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
echo "Release exists with ID: ${RELEASE_ID}"
|
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}" >/dev/null || true
|
||||||
# Delete existing assets
|
fi
|
||||||
echo "Deleting existing assets..."
|
|
||||||
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
|
|
||||||
for ASSET_ID in $ASSETS; do
|
|
||||||
echo "Deleting asset ${ASSET_ID}..."
|
|
||||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload new assets
|
|
||||||
echo "Uploading assets..."
|
|
||||||
for FILE in release-files/*; do
|
|
||||||
FILENAME=$(basename "$FILE")
|
|
||||||
echo "Uploading ${FILENAME}..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary "@${FILE}" \
|
|
||||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
|
||||||
echo ""
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Release complete!"
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Cleanup old GitHub releases
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
set -euo pipefail
|
||||||
TAG_NAME="v${VERSION}"
|
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||||
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||||
|
|
||||||
# Check if release exists on GitHub
|
echo "Cleaning up old GitHub releases (keeping ${TAG_NAME})..."
|
||||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
for OLD_ID in $OLD_RELEASES; do
|
||||||
echo "Creating new GitHub release for ${TAG_NAME}..."
|
[ -n "$OLD_ID" ] || continue
|
||||||
RELEASE_ID=$(curl -s -X POST \
|
OLD_TAG=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||||
-H "Authorization: token ${GH_TOKEN}" \
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||||
-H "Content-Type: application/json" \
|
echo "Deleting GitHub release ${OLD_ID} (${OLD_TAG})..."
|
||||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||||
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.id')
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" >/dev/null || true
|
||||||
echo "Created GitHub release with ID: ${RELEASE_ID}"
|
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
|
||||||
else
|
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||||
echo "GitHub release exists with ID: ${RELEASE_ID}"
|
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}" >/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Upload assets to GitHub
|
|
||||||
echo "Uploading assets to GitHub..."
|
|
||||||
UPLOAD_URL=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
|
||||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
|
||||||
|
|
||||||
for FILE in release-files/*; do
|
|
||||||
FILENAME=$(basename "$FILE")
|
|
||||||
echo "Uploading ${FILENAME} to GitHub..."
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Authorization: token ${GH_TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary "@${FILE}" \
|
|
||||||
"${UPLOAD_URL}?name=${FILENAME}"
|
|
||||||
echo ""
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "GitHub release complete!"
|
echo "Release finalize complete for ${TAG_NAME}"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules
|
|||||||
devAssets
|
devAssets
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dist-electron/
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "cinny"]
|
[submodule "cinny"]
|
||||||
path = cinny
|
path = cinny
|
||||||
url = http://synbox.ruv.wtf:8418/litruv/cinny.git
|
url = ../cinny.git
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
# Paarrot API Quick Start
|
|
||||||
|
|
||||||
A local HTTP API server for controlling Paarrot from external devices like Stream Deck, scripts, and automation tools.
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
The API server starts automatically when you run Paarrot. It listens on `http://127.0.0.1:33384`.
|
|
||||||
|
|
||||||
### Test the API
|
|
||||||
|
|
||||||
**Option 1: Using Postman**
|
|
||||||
|
|
||||||
Import the Postman collection for easy testing:
|
|
||||||
1. Open Postman
|
|
||||||
2. Click **Import** → **File**
|
|
||||||
3. Select `paarrot-api.postman_collection.json`
|
|
||||||
4. All endpoints will be ready to use!
|
|
||||||
|
|
||||||
The collection is automatically updated on git push.
|
|
||||||
|
|
||||||
**Option 2: Using the test script**
|
|
||||||
```bash
|
|
||||||
node test-api.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 3: Using curl**
|
|
||||||
```bash
|
|
||||||
./test-api.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 4: Manual curl commands**
|
|
||||||
```bash
|
|
||||||
# Health check
|
|
||||||
curl http://127.0.0.1:33384/health
|
|
||||||
|
|
||||||
# Toggle mute
|
|
||||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
|
||||||
|
|
||||||
# Send message
|
|
||||||
curl -X POST http://127.0.0.1:33384/message/current \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message": "Hello from API!"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Quick Reference
|
|
||||||
|
|
||||||
### Common Endpoints
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/health` | GET | Check if API is running |
|
|
||||||
| `/status` | GET | Get app status (mute, deafen, etc.) |
|
|
||||||
| `/mute/toggle` | POST | Toggle microphone mute |
|
|
||||||
| `/deafen/toggle` | POST | Toggle deafen |
|
|
||||||
| `/channels` | GET | Get list of rooms |
|
|
||||||
| `/channel` | POST | Switch to a room |
|
|
||||||
| `/message/current` | POST | Send message to current room |
|
|
||||||
|
|
||||||
## 🎮 Stream Deck Integration
|
|
||||||
|
|
||||||
1. Install the "API Ninja" or "HTTP Request" plugin
|
|
||||||
2. Create buttons with these settings:
|
|
||||||
|
|
||||||
**Mute Toggle Button:**
|
|
||||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Quick Message Button:**
|
|
||||||
- URL: `http://127.0.0.1:33384/message/current`
|
|
||||||
- Method: POST
|
|
||||||
- Body: `{"message": "BRB!"}`
|
|
||||||
|
|
||||||
## 🔧 Integration with Paarrot
|
|
||||||
|
|
||||||
To enable full functionality, you need to implement the action handlers in the Cinny frontend:
|
|
||||||
|
|
||||||
1. Import the API handler in your app initialization:
|
|
||||||
```typescript
|
|
||||||
import { initPaarrotAPI } from './app/paarrot-api';
|
|
||||||
|
|
||||||
// After Matrix client is initialized
|
|
||||||
initPaarrotAPI(matrixClient);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Implement the TODO items in `cinny/src/app/paarrot-api.ts`:
|
|
||||||
- Mute/unmute logic (WebRTC audio)
|
|
||||||
- Deafen logic (WebRTC audio output)
|
|
||||||
- Navigation to rooms (router integration)
|
|
||||||
- Get current room from URL/state
|
|
||||||
|
|
||||||
## 📖 Full Documentation
|
|
||||||
|
|
||||||
See [API.md](API.md) for complete API documentation including:
|
|
||||||
- All available endpoints
|
|
||||||
- Request/response formats
|
|
||||||
- Error handling
|
|
||||||
- Code examples in multiple languages
|
|
||||||
- Detailed integration guide
|
|
||||||
|
|
||||||
## 🛠️ Configuration
|
|
||||||
|
|
||||||
Edit `electron/api-server.js` to customize:
|
|
||||||
- Port number (default: 33384)
|
|
||||||
- Timeout duration (default: 10s)
|
|
||||||
- CORS settings
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
**API not responding:**
|
|
||||||
1. Check if Paarrot is running
|
|
||||||
2. Look for "Paarrot API server listening" in console logs
|
|
||||||
3. Verify port 33384 is not in use: `lsof -i :33384`
|
|
||||||
|
|
||||||
**Actions not working:**
|
|
||||||
1. Check browser console for errors
|
|
||||||
2. Ensure `initPaarrotAPI()` is called in your app
|
|
||||||
3. Implement the TODO items in `paarrot-api.ts`
|
|
||||||
|
|
||||||
**Port in use:**
|
|
||||||
Edit `electron/api-server.js` and change the port number in the constructor.
|
|
||||||
|
|
||||||
## 📦 Files
|
|
||||||
|
|
||||||
- `electron/api-server.js` - API server implementation
|
|
||||||
- `cinny/src/app/paarrot-api.ts` - Client-side handler (needs implementation)
|
|
||||||
- `test-api.js` - Node.js test script
|
|
||||||
- `test-api.sh` - Bash test script
|
|
||||||
- `API.md` - Full documentation
|
|
||||||
|
|
||||||
## 🔐 Security
|
|
||||||
|
|
||||||
- API only listens on localhost (127.0.0.1)
|
|
||||||
- Not accessible from network
|
|
||||||
- No authentication required (local only)
|
|
||||||
- CORS enabled for all origins (safe since localhost only)
|
|
||||||
|
|
||||||
## 💡 Examples
|
|
||||||
|
|
||||||
**Python Script:**
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
def toggle_mute():
|
|
||||||
r = requests.post('http://127.0.0.1:33384/mute/toggle')
|
|
||||||
print(r.json())
|
|
||||||
|
|
||||||
toggle_mute()
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript:**
|
|
||||||
```javascript
|
|
||||||
async function sendQuickMessage(msg) {
|
|
||||||
const res = await fetch('http://127.0.0.1:33384/message/current', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message: msg })
|
|
||||||
});
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. Run Paarrot: `npm run dev`
|
|
||||||
2. Test the API: `node test-api.js`
|
|
||||||
3. Integrate the handler: Import and call `initPaarrotAPI()`
|
|
||||||
4. Implement the TODO items in `paarrot-api.ts`
|
|
||||||
5. Create Stream Deck buttons or automation scripts!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more details, see [API.md](API.md)
|
|
||||||
537
API.md
537
API.md
@@ -1,537 +0,0 @@
|
|||||||
# Paarrot API Server Documentation
|
|
||||||
|
|
||||||
The Paarrot API server provides a local HTTP API for controlling the Paarrot desktop app from external applications like Stream Deck, keyboard macros, scripts, and other automation tools.
|
|
||||||
|
|
||||||
## Server Details
|
|
||||||
|
|
||||||
- **Base URL**: `http://127.0.0.1:33384`
|
|
||||||
- **Protocol**: HTTP
|
|
||||||
- **Content-Type**: `application/json`
|
|
||||||
- **CORS**: Enabled for all origins
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Currently, no authentication is required. The API server only listens on localhost (127.0.0.1) for security.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
|
|
||||||
Check if the API server is running.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /health`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"app": "Paarrot API",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Status
|
|
||||||
|
|
||||||
Get the current status of the app (mute, deafen, current room, etc.).
|
|
||||||
|
|
||||||
**Endpoint**: `GET /status`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": false,
|
|
||||||
"deafened": false,
|
|
||||||
"currentRoom": "!abc123:matrix.org",
|
|
||||||
"connected": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Mute/Unmute
|
|
||||||
|
|
||||||
Set the mute status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /mute`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Toggle Mute
|
|
||||||
|
|
||||||
Toggle the current mute status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /mute/toggle`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Deafen/Undeafen
|
|
||||||
|
|
||||||
Set the deafen status (deafening also mutes).
|
|
||||||
|
|
||||||
**Endpoint**: `POST /deafen`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Toggle Deafen
|
|
||||||
|
|
||||||
Toggle the current deafen status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /deafen/toggle`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Change Channel/Room
|
|
||||||
|
|
||||||
Switch to a different Matrix room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /channel`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"roomName": "General"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Channels/Rooms
|
|
||||||
|
|
||||||
Get a list of available rooms/channels.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /channels`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"name": "General",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"roomId": "!def456:matrix.org",
|
|
||||||
"name": "Random",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"roomId": "!xyz789:example.com",
|
|
||||||
"name": "General",
|
|
||||||
"server": "example.com",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: The `server` field helps distinguish between rooms with the same name on different servers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Send Message
|
|
||||||
|
|
||||||
Send a message to a specific room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /message`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"message": "Hello from the API!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"eventId": "$abc123",
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Send Message to Current Room
|
|
||||||
|
|
||||||
Send a message to the currently active room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /message/current`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Hello from the API!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"eventId": "$abc123",
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Current Room
|
|
||||||
|
|
||||||
Get information about the currently active room.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /room/current`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"name": "General",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"avatar": "mxc://matrix.org/abc123",
|
|
||||||
"isDirect": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
All endpoints return error responses in the following format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Error message here"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Common HTTP status codes:
|
|
||||||
- `200 OK` - Request successful
|
|
||||||
- `400 Bad Request` - Missing or invalid parameters
|
|
||||||
- `404 Not Found` - Endpoint not found
|
|
||||||
- `500 Internal Server Error` - Server error
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Example Usage
|
|
||||||
|
|
||||||
### cURL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Toggle mute
|
|
||||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
|
||||||
|
|
||||||
# Send message to current room
|
|
||||||
curl -X POST http://127.0.0.1:33384/message/current \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message": "Hello!"}'
|
|
||||||
|
|
||||||
# Change channel
|
|
||||||
curl -X POST http://127.0.0.1:33384/channel \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"roomId": "!abc123:matrix.org"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript/Node.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// Toggle mute
|
|
||||||
async function toggleMute() {
|
|
||||||
const response = await fetch('http://127.0.0.1:33384/mute/toggle', {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Muted:', data.data.muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message
|
|
||||||
async function sendMessage(roomId, message) {
|
|
||||||
const response = await fetch('http://127.0.0.1:33384/message', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ roomId, message })
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Message sent:', data.data.eventId);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Toggle mute
|
|
||||||
def toggle_mute():
|
|
||||||
response = requests.post('http://127.0.0.1:33384/mute/toggle')
|
|
||||||
data = response.json()
|
|
||||||
print('Muted:', data['data']['muted'])
|
|
||||||
|
|
||||||
# Send message
|
|
||||||
def send_message(room_id, message):
|
|
||||||
response = requests.post('http://127.0.0.1:33384/message', json={
|
|
||||||
'roomId': room_id,
|
|
||||||
'message': message
|
|
||||||
})
|
|
||||||
data = response.json()
|
|
||||||
print('Message sent:', data['data']['eventId'])
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stream Deck Integration
|
|
||||||
|
|
||||||
To integrate with Stream Deck:
|
|
||||||
|
|
||||||
1. Install the "API Ninja" or "HTTP Request" plugin for Stream Deck
|
|
||||||
2. Create a button with the following configuration:
|
|
||||||
- **URL**: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- **Method**: POST
|
|
||||||
- **Headers**: `Content-Type: application/json`
|
|
||||||
|
|
||||||
Example configurations:
|
|
||||||
|
|
||||||
**Mute Toggle Button**:
|
|
||||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Deafen Toggle Button**:
|
|
||||||
- URL: `http://127.0.0.1:33384/deafen/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Send Quick Message**:
|
|
||||||
- URL: `http://127.0.0.1:33384/message/current`
|
|
||||||
- Method: POST
|
|
||||||
- Body: `{"message": "BRB!"}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Client-Side Implementation
|
|
||||||
|
|
||||||
To enable the API functionality in the Paarrot app, you need to implement the action handlers in your Matrix client code. The API server sends actions via IPC to the renderer process, which should handle them and respond.
|
|
||||||
|
|
||||||
### Example Handler (TypeScript/React)
|
|
||||||
|
|
||||||
Add this to your app initialization:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Listen for API actions from the Electron API server
|
|
||||||
if (window.electron?.api?.onAction) {
|
|
||||||
window.electron.api.onAction(async (action: {
|
|
||||||
action: string;
|
|
||||||
params: any;
|
|
||||||
responseChannel: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
switch (action.action) {
|
|
||||||
case 'toggle-mute':
|
|
||||||
// Toggle microphone mute
|
|
||||||
result = await toggleMicrophone();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'set-mute':
|
|
||||||
// Set microphone mute state
|
|
||||||
result = await setMicrophone(action.params.muted);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'toggle-deafen':
|
|
||||||
// Toggle deafen (mute speakers)
|
|
||||||
result = await toggleDeafen();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'set-deafen':
|
|
||||||
// Set deafen state
|
|
||||||
result = await setDeafen(action.params.deafened);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'change-channel':
|
|
||||||
// Navigate to a different room
|
|
||||||
result = await changeRoom(action.params.roomId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-channels':
|
|
||||||
// Get list of rooms
|
|
||||||
result = await getRoomList();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'send-message':
|
|
||||||
// Send message to specific room
|
|
||||||
result = await sendMessage(action.params.roomId, action.params.message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'send-message-current':
|
|
||||||
// Send message to current room
|
|
||||||
result = await sendMessageToCurrent(action.params.message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-current-room':
|
|
||||||
// Get current room info
|
|
||||||
result = await getCurrentRoomInfo();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-status':
|
|
||||||
// Get current app status
|
|
||||||
result = await getAppStatus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown action: ${action.action}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success response back
|
|
||||||
window.electron.api.sendResponse(action.responseChannel, {
|
|
||||||
success: true,
|
|
||||||
data: result
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Send error response back
|
|
||||||
window.electron.api.sendResponse(action.responseChannel, {
|
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The API server can be configured by modifying `electron/api-server.js`:
|
|
||||||
|
|
||||||
- **Default Port**: 33384 (can be changed in the constructor)
|
|
||||||
- **Bind Address**: 127.0.0.1 (localhost only for security)
|
|
||||||
- **Timeout**: 10 seconds for actions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
1. The API server only listens on localhost (127.0.0.1) and is not accessible from the network
|
|
||||||
2. No authentication is implemented - anyone with local access can control the app
|
|
||||||
3. Consider adding API key authentication if needed for your use case
|
|
||||||
4. CORS is enabled for all origins since it's localhost only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**API server not starting**:
|
|
||||||
- Check if port 33384 is already in use
|
|
||||||
- Check console logs for errors
|
|
||||||
- Verify Express is installed: `npm install express cors body-parser`
|
|
||||||
|
|
||||||
**Actions not working**:
|
|
||||||
- Ensure the client-side handler is implemented
|
|
||||||
- Check browser console for errors
|
|
||||||
- Verify the action names match between API server and client
|
|
||||||
|
|
||||||
**Timeout errors**:
|
|
||||||
- Actions must respond within 10 seconds
|
|
||||||
- Check if the Matrix client is initialized
|
|
||||||
- Verify the action handler is registered
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential features to add:
|
|
||||||
- WebSocket support for real-time events
|
|
||||||
- Authentication with API keys
|
|
||||||
- Voice channel controls (join/leave)
|
|
||||||
- User status updates
|
|
||||||
- Notification controls
|
|
||||||
- Screen sharing controls
|
|
||||||
51
README.md
51
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Paarrot
|
# Paarrot
|
||||||
|
|
||||||
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
||||||
|
|
||||||
@@ -22,10 +22,57 @@ To setup development locally run the following commands:
|
|||||||
* `cd cinny-desktop/cinny`
|
* `cd cinny-desktop/cinny`
|
||||||
* `npm ci`
|
* `npm ci`
|
||||||
* `cd ..`
|
* `cd ..`
|
||||||
* `npm ci`
|
* `npm ci`
|
||||||
|
|
||||||
To build the app locally, run:
|
To build the app locally, run:
|
||||||
* `npm run build`
|
* `npm run build`
|
||||||
|
|
||||||
To start local dev server, run:
|
To start local dev server, run:
|
||||||
* `npm run dev`
|
* `npm run dev`
|
||||||
|
|
||||||
|
## Plugin System
|
||||||
|
|
||||||
|
Paarrot includes a plugin system for extending and customising the app with JavaScript modules.
|
||||||
|
|
||||||
|
### Installing Plugins
|
||||||
|
|
||||||
|
1. Drop a plugin folder into your plugins directory:
|
||||||
|
- **Windows**: `%APPDATA%\paarrot\plugins\<plugin-name>\`
|
||||||
|
- **Linux**: `~/.config/Paarrot/plugins/<plugin-name>/`
|
||||||
|
- **macOS**: `~/Library/Application Support/Paarrot/plugins/<plugin-name>/`
|
||||||
|
2. Enable it in **Settings → Plugins → Installed**
|
||||||
|
|
||||||
|
### What Plugins Can Do
|
||||||
|
|
||||||
|
- Register custom slash commands
|
||||||
|
- Intercept and modify messages
|
||||||
|
- Inject buttons into 11 UI locations (nav lists, toolbars, headers, sidebar, menus)
|
||||||
|
- Register custom themes
|
||||||
|
- Hook into raw Matrix events
|
||||||
|
- Run background tasks
|
||||||
|
- Show system notifications
|
||||||
|
|
||||||
|
### Plugin Locations (UI Buttons)
|
||||||
|
|
||||||
|
Plugins can inject buttons as **nav list rows** or **icon buttons** across the app:
|
||||||
|
|
||||||
|
| Location | Style | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| `channel-list` | Nav row | Space channel list |
|
||||||
|
| `home-section` | Nav row | Home panel, above room list |
|
||||||
|
| `direct-messages` | Nav row | DMs panel, below "Create Chat" |
|
||||||
|
| `sidebar-actions` | Icon | Left sidebar — above Explore and above Search |
|
||||||
|
| `text-composer-toolbar` | Icon | Message composer toolbar |
|
||||||
|
| `composer-actions` | Icon | Beside the `+` attach button |
|
||||||
|
| `room-header` | Icon | Room header bar |
|
||||||
|
| `room-menu` | Icon | Room ⋮ dropdown |
|
||||||
|
| `message-actions` | Icon | Message hover bar |
|
||||||
|
| `user-menu` | Icon | Right-click on user avatar |
|
||||||
|
| `search-notification-section` | Icon | Notifications page header |
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](PLUGINS.md)
|
||||||
|
- [Full Plugin API Reference](docs/PLUGIN_API.md)
|
||||||
|
- [Button Registration API](docs/PLUGIN_BUTTON_API.md)
|
||||||
|
- [Example Plugins](plugins/)
|
||||||
|
|||||||
2
cinny
2
cinny
Submodule cinny updated: 9eb5e4fa32...99a294791e
19
config.json
19
config.json
@@ -9,30 +9,13 @@
|
|||||||
"xmr.se"
|
"xmr.se"
|
||||||
],
|
],
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"calling": {
|
|
||||||
"livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt"
|
|
||||||
},
|
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
"spaces": [
|
"spaces": [
|
||||||
"#cinny-space:matrix.org",
|
|
||||||
"#community:matrix.org",
|
|
||||||
"#space:envs.net",
|
|
||||||
"#science-space:matrix.org",
|
|
||||||
"#libregaming-games:tchncs.de",
|
|
||||||
"#mathematics-on:matrix.org"
|
|
||||||
],
|
],
|
||||||
"rooms": [
|
"rooms": [
|
||||||
"#cinny:matrix.org",
|
|
||||||
"#freesoftware:matrix.org",
|
|
||||||
"#pcapdroid:matrix.org",
|
|
||||||
"#gentoo:matrix.org",
|
|
||||||
"#PrivSec.dev:arcticfoxes.net",
|
|
||||||
"#disroot:aria-net.org"
|
|
||||||
],
|
],
|
||||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
"servers": []
|
||||||
},
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
|
|||||||
4
dev-app-update.yml
Normal file
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
provider: github
|
||||||
|
owner: Paarrot
|
||||||
|
repo: Paarrot-Desktop
|
||||||
|
updaterCacheDirName: paarrot-updater
|
||||||
BIN
dist-electron/win-unpacked/resources/app.asar
Normal file
BIN
dist-electron/win-unpacked/resources/app.asar
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
"cinny/dist/**/*",
|
"cinny/dist/**/*",
|
||||||
|
"dev-app-update.yml",
|
||||||
"!**/*.map",
|
"!**/*.map",
|
||||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
||||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"protocols": [
|
||||||
|
{
|
||||||
|
"name": "Paarrot URL",
|
||||||
|
"schemes": ["paarrot"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
@@ -50,6 +57,7 @@
|
|||||||
"Name": "Paarrot",
|
"Name": "Paarrot",
|
||||||
"GenericName": "Matrix Client",
|
"GenericName": "Matrix Client",
|
||||||
"Comment": "A Matrix client built with Cinny",
|
"Comment": "A Matrix client built with Cinny",
|
||||||
|
"MimeType": "x-scheme-handler/paarrot;",
|
||||||
"Categories": "Network;InstantMessaging;",
|
"Categories": "Network;InstantMessaging;",
|
||||||
"Keywords": "matrix;chat;messaging;",
|
"Keywords": "matrix;chat;messaging;",
|
||||||
"StartupWMClass": "paarrot"
|
"StartupWMClass": "paarrot"
|
||||||
@@ -77,30 +85,29 @@
|
|||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "squirrel",
|
"target": "nsis",
|
||||||
"arch": ["x64"]
|
"arch": ["x64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icons/icon.ico",
|
"icon": "icons/icon.ico",
|
||||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
||||||
},
|
},
|
||||||
"squirrelWindows": {
|
"nsis": {
|
||||||
"name": "Paarrot",
|
"oneClick": true,
|
||||||
"loadingGif": "icons/icon.png",
|
"perMachine": false,
|
||||||
"iconUrl": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/raw/branch/main/icons/icon.ico",
|
"allowToChangeInstallationDirectory": false,
|
||||||
"remoteReleases": false
|
"deleteAppDataOnUninstall": false,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "Paarrot",
|
||||||
|
"differentialPackage": false
|
||||||
},
|
},
|
||||||
|
"compression": "normal",
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
|
||||||
"provider": "generic",
|
|
||||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${version}",
|
|
||||||
"channel": "latest"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
"owner": "Paarrot",
|
"owner": "Paarrot",
|
||||||
"repo": "Paarrot-Desktop",
|
"repo": "Paarrot-Desktop",
|
||||||
"token": "${env.GH_TOKEN}",
|
|
||||||
"releaseType": "release"
|
"releaseType": "release"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
1426
electron/main.js
1426
electron/main.js
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,8 @@ const commandMap = {
|
|||||||
'check_for_updates': 'check-for-updates',
|
'check_for_updates': 'check-for-updates',
|
||||||
'download_update': 'download-update',
|
'download_update': 'download-update',
|
||||||
'install_update': 'install-update',
|
'install_update': 'install-update',
|
||||||
'get_desktop_sources': 'get-desktop-sources'
|
'get_desktop_sources': 'get-desktop-sources',
|
||||||
|
'get_protocol_status': 'protocol:get-status'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shared invoke function for legacy API compatibility
|
// Shared invoke function for legacy API compatibility
|
||||||
@@ -129,6 +130,7 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
updater: {
|
updater: {
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
||||||
|
isMock: () => ipcRenderer.invoke('updater-is-mock'),
|
||||||
onUpdateAvailable: (callback) => {
|
onUpdateAvailable: (callback) => {
|
||||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
@@ -138,6 +140,9 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
onUpdateDownloaded: (callback) => {
|
onUpdateDownloaded: (callback) => {
|
||||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
|
onUpdateNotAvailable: (callback) => {
|
||||||
|
ipcRenderer.on('update-not-available', (_, info) => callback(info));
|
||||||
|
},
|
||||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
},
|
},
|
||||||
@@ -167,6 +172,21 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
ipcRenderer.on('notification:navigate', handler);
|
ipcRenderer.on('notification:navigate', handler);
|
||||||
return () => ipcRenderer.removeListener('notification:navigate', handler);
|
return () => ipcRenderer.removeListener('notification:navigate', handler);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugin management
|
||||||
|
plugins: {
|
||||||
|
getPath: () => ipcRenderer.invoke('plugin:get-path'),
|
||||||
|
download: (pluginId, downloadUrl, name) => ipcRenderer.invoke('plugin:download', { pluginId, downloadUrl, name }),
|
||||||
|
list: () => ipcRenderer.invoke('plugin:list'),
|
||||||
|
uninstall: (pluginId) => ipcRenderer.invoke('plugin:uninstall', pluginId),
|
||||||
|
readPluginCode: (pluginId) => ipcRenderer.invoke('plugin:read-code', pluginId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Desktop protocol diagnostics
|
||||||
|
protocol: {
|
||||||
|
getStatus: () => ipcRenderer.invoke('protocol:get-status'),
|
||||||
|
repair: () => ipcRenderer.invoke('protocol:repair')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
161
example-plugin/index.js
Normal file
161
example-plugin/index.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// Example plugin for Paarrot - Demonstrates all plugin API features
|
||||||
|
// This plugin showcases commands, message interceptors, settings, logging, and more
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "Example Plugin",
|
||||||
|
version: "2.0.0",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin is loaded
|
||||||
|
* @param {PluginContext} ctx - The plugin context with access to all APIs
|
||||||
|
*/
|
||||||
|
onLoad: async (ctx) => {
|
||||||
|
ctx.log('🎉 Example plugin is loading...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Register commands with args
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "shrug",
|
||||||
|
description: "Send a shrug emoji",
|
||||||
|
run: () => {
|
||||||
|
ctx.log('Shrug command executed');
|
||||||
|
return "¯\\_(ツ)_/¯";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "echo",
|
||||||
|
args: ["text"],
|
||||||
|
description: "Echo back your text",
|
||||||
|
run: ({ text }) => {
|
||||||
|
ctx.log('Echo command:', text);
|
||||||
|
return text || "Nothing to echo!";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "wave",
|
||||||
|
description: "Wave at someone",
|
||||||
|
args: ["name"],
|
||||||
|
run: ({ name }) => {
|
||||||
|
return `👋 Hey ${name || 'everyone'}!`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Message interceptors
|
||||||
|
ctx.messages.onBeforeSend((msg) => {
|
||||||
|
// Auto-expand abbreviations
|
||||||
|
if (msg.content === "brb") {
|
||||||
|
msg.content = "be right back!";
|
||||||
|
ctx.log('Expanded brb to full phrase');
|
||||||
|
}
|
||||||
|
if (msg.content === "omw") {
|
||||||
|
msg.content = "on my way!";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.messages.onReceive((msg) => {
|
||||||
|
// Log all received messages (for debugging)
|
||||||
|
ctx.log(`Received message in ${msg.roomId}:`, msg.content.substring(0, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Define settings
|
||||||
|
ctx.settings.define({
|
||||||
|
autoShrug: {
|
||||||
|
type: "boolean",
|
||||||
|
label: "Auto Shrug",
|
||||||
|
description: "Automatically append ¯\\_(ツ)_/¯ to messages ending with '?'",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: "select",
|
||||||
|
label: "Plugin Theme",
|
||||||
|
description: "Choose your preferred theme",
|
||||||
|
options: [
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "purple", label: "Purple" }
|
||||||
|
],
|
||||||
|
default: "dark"
|
||||||
|
},
|
||||||
|
customPrefix: {
|
||||||
|
type: "string",
|
||||||
|
label: "Custom Prefix",
|
||||||
|
description: "Prefix for plugin commands",
|
||||||
|
default: "[Plugin]"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use settings
|
||||||
|
const autoShrug = ctx.settings.get('autoShrug');
|
||||||
|
if (autoShrug) {
|
||||||
|
ctx.log('Auto shrug is enabled!');
|
||||||
|
ctx.messages.onBeforeSend((msg) => {
|
||||||
|
if (msg.content.endsWith('?')) {
|
||||||
|
msg.content += ' ¯\\_(ツ)_/¯';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Listen to raw Matrix events
|
||||||
|
ctx.matrix.on('Room.timeline', (event) => {
|
||||||
|
if (event.getType() === 'm.room.message') {
|
||||||
|
const content = event.getContent();
|
||||||
|
if (content.body?.includes('hello')) {
|
||||||
|
ctx.log('Someone said hello!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Background tasks
|
||||||
|
ctx.timers.setInterval(() => {
|
||||||
|
ctx.log('Background task running... (every 30s)');
|
||||||
|
// You could check for notifications, clean up data, etc.
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// 6. Notifications
|
||||||
|
ctx.notify({
|
||||||
|
title: "Example Plugin",
|
||||||
|
body: "Plugin loaded successfully!",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Custom UI renderer (conceptual - would need UI integration)
|
||||||
|
ctx.ui.registerRenderer("custom-message", (msg) => {
|
||||||
|
ctx.log('Rendering custom message:', msg);
|
||||||
|
// Return custom React component
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. Export API for other plugins
|
||||||
|
this.exports = {
|
||||||
|
greet: (name) => {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
},
|
||||||
|
version: "2.0.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.log('✅ Example plugin loaded successfully!');
|
||||||
|
ctx.log('Registered commands: /shrug, /echo, /wave');
|
||||||
|
ctx.log('Settings:', {
|
||||||
|
autoShrug: ctx.settings.get('autoShrug'),
|
||||||
|
theme: ctx.settings.get('theme'),
|
||||||
|
prefix: ctx.settings.get('customPrefix')
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
ctx.error('Failed to load example plugin:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin is unloaded or disabled
|
||||||
|
*/
|
||||||
|
onUnload: async () => {
|
||||||
|
console.log('[ExamplePlugin] Plugin is unloading...');
|
||||||
|
console.log('[ExamplePlugin] Cleaned up successfully!');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exports for other plugins to use via ctx.require()
|
||||||
|
exports: null // Will be set during onLoad
|
||||||
|
};
|
||||||
9
example-plugin/plugin-metadata.json
Normal file
9
example-plugin/plugin-metadata.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Example Plugin",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "A comprehensive example plugin demonstrating all plugin API features including commands, interceptors, settings, and more",
|
||||||
|
"author": "Paarrot Team",
|
||||||
|
"homepage": "https://github.com/Paarrot/cinny-desktop",
|
||||||
|
"thumbnail": "https://raw.githubusercontent.com/Paarrot/cinny-desktop/main/icons/icon.png",
|
||||||
|
"tags": ["example", "demo", "utility", "commands", "interceptors"]
|
||||||
|
}
|
||||||
3922
package-lock.json
generated
3922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "paarrot",
|
"name": "paarrot",
|
||||||
"version": "4.11.72",
|
"version": "4.11.168",
|
||||||
"description": "Paarrot - A Matrix client based on Cinny",
|
"description": "Paarrot - A Matrix client based on Cinny",
|
||||||
"homepage": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop",
|
"homepage": "https://github.com/Paarrot/Paarrot-Desktop",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Paarrot/Paarrot-Desktop.git"
|
||||||
|
},
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
||||||
"dev:vite": "cd cinny && npm start",
|
"dev:vite": "cross-env BROWSER=none sh -c 'cd cinny && npm start'",
|
||||||
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
|
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
|
||||||
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
|
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
|
||||||
|
"build:local": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder",
|
||||||
"build:linux": "npm run build -- --linux",
|
"build:linux": "npm run build -- --linux",
|
||||||
"build:win": "npm run build -- --win",
|
"build:win": "npm run build -- --win",
|
||||||
"postman:generate": "node scripts/generate-postman-collection.js"
|
"postman:generate": "node scripts/generate-postman-collection.js"
|
||||||
@@ -23,24 +28,28 @@
|
|||||||
},
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "2.2.2",
|
"adm-zip": "^0.5.18",
|
||||||
|
"body-parser": "2.3.0",
|
||||||
"cors": "2.8.6",
|
"cors": "2.8.6",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-store": "^11.0.2",
|
||||||
"electron-store": "^8.2.0",
|
"electron-updater": "^6.8.9",
|
||||||
"electron-updater": "^6.3.9",
|
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"open": "11.0.0"
|
"open": "11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "6.0.0",
|
"@actions/github": "9.1.1",
|
||||||
"@electron/rebuild": "4.0.3",
|
"@electron/rebuild": "4.2.0",
|
||||||
"concurrently": "9.2.1",
|
"concurrently": "10.0.3",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"electron": "40.6.0",
|
"electron": "43.1.0",
|
||||||
"electron-builder": "26.8.1",
|
"electron-builder": "26.15.3",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"png2icons": "2.0.1",
|
"png2icons": "2.0.1",
|
||||||
"sharp": "0.34.5",
|
"sharp": "0.35.3",
|
||||||
"wait-on": "9.0.4"
|
"wait-on": "9.0.10"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"electron-winstaller@5.4.0": true,
|
||||||
|
"electron@43.1.0": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
scripts/migrate-icons.mjs
Normal file
104
scripts/migrate-icons.mjs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const srcRoot = path.resolve(__dirname, '../cinny/src');
|
||||||
|
const iconsModule = path.resolve(srcRoot, 'app/components/icons');
|
||||||
|
|
||||||
|
const ICON_SYMBOLS = new Set(['Icon', 'Icons', 'IconName', 'IconSrc']);
|
||||||
|
|
||||||
|
function walk(dir, files = []) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath, files);
|
||||||
|
} else if (/\.(tsx?)$/.test(entry.name)) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconsImportPath(filePath) {
|
||||||
|
const rel = path.relative(path.dirname(filePath), iconsModule).replace(/\\/g, '/');
|
||||||
|
return rel.startsWith('.') ? rel : `./${rel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitFoldsImport(importClause) {
|
||||||
|
const match = importClause.match(/^\{([^}]+)\}(?:\s+as\s+\w+)?$/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const specifiers = match[1]
|
||||||
|
.split(',')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => {
|
||||||
|
const [local, imported] = part.split(/\s+as\s+/).map((s) => s.trim());
|
||||||
|
return { local: local ?? imported, imported: imported ?? local };
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconSpecs = specifiers.filter((s) => ICON_SYMBOLS.has(s.imported));
|
||||||
|
const foldsSpecs = specifiers.filter((s) => !ICON_SYMBOLS.has(s.imported));
|
||||||
|
|
||||||
|
if (iconSpecs.length === 0) return null;
|
||||||
|
|
||||||
|
return { iconSpecs, foldsSpecs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateFile(filePath) {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const importRegex = /import\s+(\{[^}]+\})\s+from\s+['"]folds['"];?/g;
|
||||||
|
let changed = false;
|
||||||
|
const iconImports = new Map();
|
||||||
|
|
||||||
|
content = content.replace(importRegex, (fullMatch, importClause) => {
|
||||||
|
const split = splitFoldsImport(importClause);
|
||||||
|
if (!split) return fullMatch;
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
for (const spec of split.iconSpecs) {
|
||||||
|
iconImports.set(spec.local, spec.imported);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (split.foldsSpecs.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const foldsImport = split.foldsSpecs.map((s) => (s.local === s.imported ? s.local : `${s.imported} as ${s.local}`)).join(', ');
|
||||||
|
return `import { ${foldsImport} } from 'folds';`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changed) return false;
|
||||||
|
|
||||||
|
const iconsPath = getIconsImportPath(filePath);
|
||||||
|
const iconExportList = [...iconImports.entries()]
|
||||||
|
.map(([local, imported]) => (local === imported ? imported : `${imported} as ${local}`))
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const iconsImportLine = `import { ${iconExportList} } from '${iconsPath}';`;
|
||||||
|
|
||||||
|
const foldsImportMatch = content.match(/^import\s+\{[^}]+\}\s+from\s+['"]folds['"];?\s*$/m);
|
||||||
|
if (foldsImportMatch) {
|
||||||
|
const insertAt = content.indexOf(foldsImportMatch[0]) + foldsImportMatch[0].length;
|
||||||
|
content = `${content.slice(0, insertAt)}\n${iconsImportLine}${content.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
content = `${iconsImportLine}\n${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.replace(/\n{3,}/g, '\n\n');
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = walk(srcRoot);
|
||||||
|
let migrated = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.includes(`${path.sep}components${path.sep}icons${path.sep}`)) continue;
|
||||||
|
if (migrateFile(file)) {
|
||||||
|
migrated += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Migrated ${migrated} files.`);
|
||||||
Reference in New Issue
Block a user