Compare commits

55 Commits

Author SHA1 Message Date
GitHub Actions
afc4ba1087 chore: bump version to 4.11.191 [skip ci] 2026-08-24 14:03:10 +00:00
f022155aa1 Update responsive image attachments
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 16s
Build / build-windows (push) Successful in 7m8s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 6s
Advance Cinny to the responsive message image fix.
2026-08-25 00:02:49 +10:00
GitHub Actions
f4ec39f229 chore: bump version to 4.11.190 [skip ci] 2026-08-24 13:58:49 +00:00
848283da42 Add TikTok player embed
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Has been cancelled
Build / finalize-release (push) Has been cancelled
Build / build-windows (push) Has been cancelled
Use TikTok's supported iframe player and safely invoke yt-dlp for YouTube streams.
2026-08-24 23:58:35 +10:00
GitHub Actions
2cd9409e3d chore: bump version to 4.11.189 [skip ci] 2026-08-24 10:07:31 +00:00
b85774e911 Bump cinny for Capacitor HTTP.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m37s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 4s
Include the native HTTP configuration required for mobile collectible catalog downloads.
2026-08-24 20:07:17 +10:00
GitHub Actions
73d31a2cb4 chore: bump version to 4.11.188 [skip ci] 2026-08-24 09:53:09 +00:00
f9bc1c7c2f Bump cinny for mobile collectibles support.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m44s
Build / build-linux (push) Successful in 3m6s
Build / finalize-release (push) Successful in 4s
Use the mobile catalog and CDN fallback for profile cosmetics outside Electron.
2026-08-24 19:52:54 +10:00
GitHub Actions
85995d0744 chore: bump version to 4.11.187 [skip ci] 2026-08-24 09:30:06 +00:00
9d7ae1d63b Use published Discord collectibles catalog.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m54s
Build / build-linux (push) Successful in 3m5s
Build / finalize-release (push) Successful in 4s
Fetch and persist the public release catalog locally while retiring client-side Discord token handling.
2026-08-24 19:29:53 +10:00
GitHub Actions
ce7b2104ad chore: bump version to 4.11.186 [skip ci] 2026-08-24 08:49:07 +00:00
60d65a29ff Bump cinny for configurable sidebar nameplate background.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m49s
Build / build-linux (push) Failing after 3m18s
Build / finalize-release (push) Successful in 4s
Include immediate collectible rendering updates and sidebar opacity controls.
2026-08-24 18:48:53 +10:00
GitHub Actions
ce29b24c28 chore: bump version to 4.11.185 [skip ci] 2026-08-23 14:55:15 +00:00
608cbaf682 Add Discord collectibles IPC and bump cinny for profile overlays.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m55s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 4s
Wire electron-side catalog fetch and asset download, exposing collectibles APIs to the renderer for live Discord shop browsing and Matrix profile uploads.
2026-08-24 00:55:01 +10:00
GitHub Actions
8ca7be8b42 chore: bump version to 4.11.184 [skip ci] 2026-08-23 06:55:17 +00:00
3c55fe65a3 Bump cinny: What's New dialog fixes and readable dev class names.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 7m2s
Build / build-linux (push) Successful in 3m4s
Build / finalize-release (push) Successful in 4s
2026-08-23 16:55:04 +10:00
GitHub Actions
f99b3c85d4 chore: bump version to 4.11.183 [skip ci] 2026-08-23 06:18:56 +00:00
fc4f087c68 Bump cinny for Android About/updates fixes and overlay user color crash guard.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m40s
Build / build-linux (push) Successful in 3m3s
Build / finalize-release (push) Successful in 4s
2026-08-23 16:18:43 +10:00
GitHub Actions
6d347a6f6b chore: bump version to 4.11.182 [skip ci] 2026-08-22 22:38:44 +00:00
316a5514e6 Bump cinny for Android View updates dialog fix.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m44s
Build / build-linux (push) Successful in 3m2s
Build / finalize-release (push) Successful in 4s
2026-08-23 08:38:29 +10:00
GitHub Actions
8392e3f7f5 chore: bump version to 4.11.181 [skip ci] 2026-08-22 22:26:52 +00:00
61b3cbc6d8 Point desktop build at cinny-local update manifest script for mobile CI.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m46s
Build / build-linux (push) Successful in 3m1s
Build / finalize-release (push) Successful in 7s
2026-08-23 08:26:33 +10:00
GitHub Actions
9befaa14a5 chore: bump version to 4.11.180 [skip ci] 2026-08-22 22:23:43 +00:00
ea45c4551b Bump cinny for What's New auto-show on first launch after update.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / finalize-release (push) Has been cancelled
Build / build-windows (push) Has been cancelled
Build / build-linux (push) Has been cancelled
2026-08-23 08:23:23 +10:00
GitHub Actions
146927fc10 chore: bump version to 4.11.179 [skip ci] 2026-08-22 22:20:20 +00:00
8edbc1b19a Bump cinny for Android updates dialog fix and add release notes manifest workflow.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Has started running
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
2026-08-23 08:20:07 +10:00
GitHub Actions
9700c645a3 chore: bump version to 4.11.178 [skip ci] 2026-08-22 22:07:17 +00:00
2828ae425c Add native media save dialog and bump cinny submodule.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m51s
Build / build-linux (push) Successful in 3m11s
Build / finalize-release (push) Successful in 5s
Wire Electron/Tauri save-file IPC for desktop downloads and update the cinny submodule with username colors, release notes, and account profile layout fixes.
2026-08-23 08:07:02 +10:00
GitHub Actions
5e06256973 chore: bump version to 4.11.177 [skip ci] 2026-08-09 08:35:32 +00:00
28c7dedf65 Bump cinny for fullscreen sick emoji confetti spew.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 7m35s
Build / build-linux (push) Successful in 3m13s
Build / finalize-release (push) Successful in 4s
2026-08-09 18:35:17 +10:00
GitHub Actions
ca10121796 chore: bump version to 4.11.176 [skip ci] 2026-08-09 05:59:10 +00:00
1ad1fc7b70 Bump cinny for shadowbox image viewer.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 7m31s
Build / build-linux (push) Successful in 3m16s
Build / finalize-release (push) Successful in 6s
2026-08-09 15:58:52 +10:00
GitHub Actions
f16b84a9f1 chore: bump version to 4.11.175 [skip ci] 2026-08-09 05:50:38 +00:00
e917193b0d Harden Linux clipboard image paste and bump cinny.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 7m28s
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Require a real image/* clipboard format before treating paste as an image, and pick up firework confetti plus URL-paste fixes from cinny.
2026-08-09 15:50:23 +10:00
GitHub Actions
fc5d498673 chore: bump version to 4.11.174 [skip ci] 2026-08-09 03:17:02 +00:00
c01bd9a43e Bump cinny for bot-driven room app UI.
All checks were successful
Build / increment-version (push) Successful in 14s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 8m3s
Build / build-linux (push) Successful in 3m22s
Build / finalize-release (push) Successful in 4s
2026-08-09 13:16:48 +10:00
GitHub Actions
3f4b1b3fc4 chore: bump version to 4.11.173 [skip ci] 2026-08-03 05:22:25 +00:00
24b59a454a Ensure wine binary exists for Windows NSIS cross-builds.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m58s
Build / build-windows (push) Successful in 5m46s
Build / finalize-release (push) Successful in 4s
2026-08-03 15:22:12 +10:00
GitHub Actions
fac9a49d56 chore: bump version to 4.11.172 [skip ci] 2026-08-03 05:16:00 +00:00
83993efc83 Run Windows Electron builds on Linux with Wine.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 3m2s
Build / build-windows (push) Failing after 4m57s
Build / finalize-release (push) Successful in 4s
2026-08-03 15:15:46 +10:00
GitHub Actions
44a9230df3 chore: bump version to 4.11.171 [skip ci] 2026-08-03 05:08:38 +00:00
658ff1215c Bump cinny: local API message and unread endpoints by scope.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m55s
Build / build-windows (push) Successful in 3m54s
Build / finalize-release (push) Successful in 5s
2026-08-03 15:08:25 +10:00
GitHub Actions
829e9b06cb chore: bump version to 4.11.170 [skip ci] 2026-07-29 10:41:07 +00:00
e64279db74 Bump cinny: emoji confetti bursts and component playground.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 3m13s
Build / build-windows (push) Successful in 6m11s
Build / finalize-release (push) Successful in 4s
Wire the playground npm script through the desktop wrapper.
2026-07-29 20:40:53 +10:00
GitHub Actions
85d95faf11 chore: bump version to 4.11.169 [skip ci] 2026-07-25 10:09:49 +00:00
61de658029 Bump cinny: fix avatar upload for JPEG/WebP/GIF without profile color.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 16s
Build / build-linux (push) Successful in 3m21s
Build / build-windows (push) Successful in 5m45s
Build / finalize-release (push) Successful in 4s
2026-07-25 20:09:35 +10:00
GitHub Actions
c8b57ba217 chore: bump version to 4.11.168 [skip ci] 2026-07-24 06:52:49 +00:00
8d8809d0c7 Bump cinny: open DMs while space parent maps catch up.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m44s
Build / build-windows (push) Successful in 3m45s
Build / finalize-release (push) Successful in 4s
2026-07-24 16:52:35 +10:00
GitHub Actions
55a0f4f566 chore: bump version to 4.11.167 [skip ci] 2026-07-24 06:24:58 +00:00
f488f32d88 Fix PowerShell mangling GitHub upload URLs with ?name=.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 2m45s
Build / build-windows (push) Successful in 3m43s
Build / finalize-release (push) Successful in 4s
2026-07-24 16:24:44 +10:00
GitHub Actions
9154929de9 chore: bump version to 4.11.166 [skip ci] 2026-07-24 06:20:05 +00:00
c2cc0d5ed5 Fix PowerShell origin URL trimming for submodule checkout.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 2m44s
Build / build-windows (push) Failing after 3m22s
Build / finalize-release (push) Successful in 8s
2026-07-24 16:19:51 +10:00
GitHub Actions
aa38348fdd chore: bump version to 4.11.165 [skip ci] 2026-07-24 06:17:38 +00:00
02ab544a37 Fix CI Windows upload shell and submodule clone host.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Failing after 11s
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Use PowerShell + curl.exe on the Windows runner (no WSL), and clone cinny via a relative/origin-derived URL so act runners do not hairpin through public HTTPS.
2026-07-24 16:17:23 +10:00
GitHub Actions
3707a6d38a chore: bump version to 4.11.164 [skip ci] 2026-07-24 06:09:58 +00:00
14 changed files with 915 additions and 49 deletions

View File

@@ -200,7 +200,7 @@ jobs:
echo "GitHub release id: ${RELEASE_ID}"
build-windows:
runs-on: windows
runs-on: ubuntu-latest
needs: [increment-version, prepare-release]
if: always() && needs.prepare-release.result == 'success'
@@ -212,25 +212,35 @@ jobs:
ref: ${{ github.ref_name }}
- name: Checkout submodules manually
shell: powershell
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) {
Write-Host "Submodules checked out at pinned commits."
if git submodule update --init --recursive; then
echo "Submodules checked out at pinned commits."
exit 0
}
fi
Write-Warning "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
git submodule deinit -f --all
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
Write-Host "Resolved submodule commit:"
echo "Resolved submodule commit:"
git -C cinny rev-parse HEAD
- name: Pull latest (after version bump)
if: github.ref == 'refs/heads/main'
shell: powershell
run: git pull origin main
- name: Setup Node.js
@@ -238,6 +248,27 @@ jobs:
with:
node-version: '20'
- name: Install Wine (NSIS cross-build)
run: |
set -euo pipefail
# electron-builder spawns `wine` by name; wine64 alone is not enough.
sudo dpkg --add-architecture i386
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
wine \
wine64 \
wine32 \
|| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wine64
if ! command -v wine >/dev/null 2>&1; then
WINE64_BIN=$(command -v wine64)
echo "No wine binary; linking wine64 -> /usr/local/bin/wine (${WINE64_BIN})"
sudo ln -sf "${WINE64_BIN}" /usr/local/bin/wine
fi
command -v wine
wine --version
- name: Install dependencies (root)
run: npm ci --prefer-offline
@@ -246,24 +277,21 @@ jobs:
run: npm ci --prefer-offline
- name: Clean previous builds
shell: powershell
run: |
if (Test-Path cinny/dist) { Remove-Item -Recurse -Force cinny/dist }
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
rm -rf cinny/dist dist-electron
- name: Build Electron app
run: npm run build:local -- --win
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Upload Windows assets to releases
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GHTOKEN }}
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 }}"
@@ -282,7 +310,6 @@ jobs:
FILENAME=$(basename "$FILE")
echo "Uploading ${FILENAME} ($(du -h "$FILE" | cut -f1))..."
# Replace existing asset on Gitea if re-running
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)
@@ -324,9 +351,8 @@ jobs:
done
- name: List build output
shell: powershell
run: |
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
find dist-electron -type f -printf '%p\n' | sort
build-linux:
runs-on: ubuntu-latest
@@ -343,6 +369,14 @@ jobs:
- 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
@@ -351,7 +385,10 @@ jobs:
fi
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
git submodule deinit -f --all
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:"

35
.github/workflows/update-manifest.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Update release notes manifest
on:
push:
paths:
- 'cinny/public/update/*.md'
- 'cinny/scripts/generate-update-manifest.mjs'
permissions:
contents: write
jobs:
manifest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate manifest.json
run: node cinny/scripts/generate-update-manifest.mjs
- name: Commit manifest if changed
run: |
if git diff --quiet -- cinny/public/update/manifest.json; then
echo "manifest.json unchanged"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add cinny/public/update/manifest.json
git commit -m "chore: regenerate update manifest.json"
git push

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "cinny"]
path = cinny
url = https://git.ruv.wtf/litruv/cinny.git
url = ../cinny.git

2
cinny

Submodule cinny updated: f62200ecd1...47979718eb

View File

@@ -153,6 +153,72 @@ class PaarrotAPIServer {
}
});
// Recent messages across group channels (non-DMs)
this.app.get('/messages/groups', async (req, res) => {
try {
const result = await this.executeAction('get-messages-groups', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Recent messages across DMs
this.app.get('/messages/dms', async (req, res) => {
try {
const result = await this.executeAction('get-messages-dms', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Recent messages across groups + DMs
this.app.get('/messages/combined', async (req, res) => {
try {
const result = await this.executeAction('get-messages-combined', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across group channels (non-DMs)
this.app.get('/unreads/groups', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-groups');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across DMs
this.app.get('/unreads/dms', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-dms');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across groups + DMs
this.app.get('/unreads/combined', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-combined');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 404 handler
this.app.use((req, res) => {
res.status(404).json({
@@ -169,7 +235,13 @@ class PaarrotAPIServer {
'GET /channels',
'POST /message',
'POST /message/current',
'GET /room/current'
'GET /room/current',
'GET /messages/groups',
'GET /messages/dms',
'GET /messages/combined',
'GET /unreads/groups',
'GET /unreads/dms',
'GET /unreads/combined'
]
});
});
@@ -237,6 +309,12 @@ class PaarrotAPIServer {
console.log(' POST /message');
console.log(' POST /message/current');
console.log(' GET /room/current');
console.log(' GET /messages/groups');
console.log(' GET /messages/dms');
console.log(' GET /messages/combined');
console.log(' GET /unreads/groups');
console.log(' GET /unreads/dms');
console.log(' GET /unreads/combined');
resolve(port);
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {

View File

@@ -0,0 +1,381 @@
/**
* Discord shop collectibles: fetch a published catalog and download selected assets from Discord CDN.
*/
const ITEM_TYPE_FOLDER = {
0: 'avatar-decorations',
1: 'profile-effects',
2: 'nameplates',
};
const ITEM_TYPE_KEY = {
0: 'avatar_decoration',
1: 'profile_effect',
2: 'nameplate',
};
const CDN_BASE = 'https://cdn.discordapp.com';
const CDN_HOST_RE = /(^|\.)(discordapp\.com|discordapp\.net|discord\.com)$/i;
const DEFAULT_USER_AGENT =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) discord/1.0 Chrome/120 Electron/28 Safari/537.36';
const PUBLISHED_CATALOG_URLS = [
'https://github.com/litruv/discord-collectibles/releases/download/latest/profileeffects.json',
'https://github.com/litruv/discord-collectibles/releases/download/latest/nameplate.json',
'https://github.com/litruv/discord-collectibles/releases/download/latest/avatardecorations.json',
];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function isCdnUrl(value) {
if (typeof value !== 'string' || !value.startsWith('http')) return false;
try {
return CDN_HOST_RE.test(new URL(value).hostname);
} catch {
return false;
}
}
function basenameFromUrl(url) {
try {
const { pathname } = new URL(url);
const last = decodeURIComponent(pathname.split('/').filter(Boolean).pop() || '');
return last || 'asset';
} catch {
return 'asset';
}
}
function guessMimeType(filename) {
const lower = String(filename).toLowerCase();
if (lower.endsWith('.png')) return 'image/png';
if (lower.endsWith('.webp')) return 'image/webp';
if (lower.endsWith('.webm')) return 'video/webm';
if (lower.endsWith('.gif')) return 'image/gif';
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
return 'application/octet-stream';
}
function deepCollectUrls(node, acc = new Set()) {
if (node == null) return acc;
if (typeof node === 'string') {
if (isCdnUrl(node)) acc.add(node);
return acc;
}
if (Array.isArray(node)) {
for (const v of node) deepCollectUrls(v, acc);
return acc;
}
if (typeof node === 'object') {
for (const v of Object.values(node)) deepCollectUrls(v, acc);
}
return acc;
}
function urlsForItem(item) {
const out = [];
const seen = new Set();
function push(url, filename, role) {
if (!url || seen.has(url)) return;
seen.add(url);
out.push({ url, filename, role: role || filename });
}
for (const url of deepCollectUrls(item)) {
let role = basenameFromUrl(url);
if (item.thumbnailPreviewSrc === url) role = 'thumbnail';
else if (item.reducedMotionSrc === url) role = 'reduced_motion';
else if (Array.isArray(item.effects)) {
const effectIndex = item.effects.findIndex((effect) => effect?.src === url);
if (effectIndex >= 0) role = `effect_${effectIndex}`;
}
push(url, basenameFromUrl(url), role);
}
if (item.type === 0 && item.asset) {
const base = `${CDN_BASE}/avatar-decoration-presets/${item.asset}.png`;
push(`${base}?passthrough=true`, 'animated.png', 'animated');
push(`${base}?passthrough=false`, 'static.png', 'static');
}
if (item.type === 2 && item.asset) {
const assetPath = String(item.asset).replace(/^\/+/, '');
push(`${CDN_BASE}/assets/collectibles/${assetPath}static.png`, 'static.png', 'static');
push(`${CDN_BASE}/assets/collectibles/${assetPath}asset.webm`, 'asset.webm', 'animated');
}
return out;
}
function decimalRgbToCss(color) {
const value = Number(color) >>> 0;
const r = (value >> 16) & 0xff;
const g = (value >> 8) & 0xff;
const b = value & 0xff;
return `rgb(${r}, ${g}, ${b})`;
}
function gradientFromColors(colors) {
if (!Array.isArray(colors) || colors.length === 0) return undefined;
if (colors.length === 1) return decimalRgbToCss(colors[0]);
return `linear-gradient(135deg, ${decimalRgbToCss(colors[0])}, ${decimalRgbToCss(colors[1])})`;
}
function previewAspectRatioForItem(item) {
if (item.type === 0) return 1;
if (item.type === 2) return 448 / 84;
const firstEffect = item.effects?.[0];
if (firstEffect?.width && firstEffect?.height) {
return firstEffect.width / firstEffect.height;
}
return 450 / 880;
}
function formatPaletteName(palette) {
if (!palette) return undefined;
return String(palette)
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
const NAMEPLATE_PALETTE_GRADIENTS = {
crimson: 'linear-gradient(135deg, #5c0a1c, #dc143c)',
berry: 'linear-gradient(135deg, #4a1030, #c42d78)',
sky: 'linear-gradient(135deg, #0a2a5c, #3b8eed)',
teal: 'linear-gradient(135deg, #0a3d3d, #2dd4bf)',
forest: 'linear-gradient(135deg, #0a2e1a, #22c55e)',
bubble_gum: 'linear-gradient(135deg, #4a1038, #f472b6)',
violet: 'linear-gradient(135deg, #2d1050, #8b5cf6)',
cobalt: 'linear-gradient(135deg, #0a1448, #3b5bdb)',
clover: 'linear-gradient(135deg, #0a3d20, #4ade80)',
lemon: 'linear-gradient(135deg, #4a3d0a, #fbbf24)',
white: 'linear-gradient(135deg, #888888, #f0f0f0)',
black: 'linear-gradient(135deg, #1a1a1a, #404040)',
};
function nameplatePaletteGradient(palette) {
if (!palette) return undefined;
return NAMEPLATE_PALETTE_GRADIENTS[palette];
}
function thumbnailForItem(item) {
if (item.thumbnailPreviewSrc && isCdnUrl(item.thumbnailPreviewSrc)) {
return item.thumbnailPreviewSrc;
}
if (item.type === 0 && item.asset) {
return `${CDN_BASE}/avatar-decoration-presets/${item.asset}.png?passthrough=false`;
}
if (item.type === 2 && item.asset) {
const assetPath = String(item.asset).replace(/^\/+/, '');
return `${CDN_BASE}/assets/collectibles/${assetPath}static.png`;
}
const assets = urlsForItem(item);
return assets[0]?.url;
}
function* walkProduct(product, category) {
yield { product, category };
for (const bundled of product.bundled_products ?? []) {
yield* walkProduct(bundled, category);
}
for (const variant of product.variants ?? []) {
yield* walkProduct(variant, category);
}
}
function* walkProducts(categories) {
for (const category of categories ?? []) {
for (const product of category.products ?? []) {
yield* walkProduct(product, category);
}
}
}
function extractCollectibleItems(categories) {
const items = [];
const seenItems = new Set();
for (const { product, category } of walkProducts(categories)) {
const categoryName = category?.name || 'Unknown';
for (const item of product.items ?? []) {
if (![0, 1, 2].includes(item.type)) continue;
const skuId = String(item.sku_id ?? item.id ?? product.sku_id ?? 'unknown');
const itemKey = `${item.type}:${skuId}:${item.id ?? ''}`;
if (seenItems.has(itemKey)) continue;
seenItems.add(itemKey);
const assets = urlsForItem(item);
if (assets.length === 0) continue;
const productName = product.name || item.title || item.label || skuId;
const typeKey = ITEM_TYPE_KEY[item.type];
const entry = {
id: `${typeKey}:${skuId}`,
skuId,
name: productName,
type: typeKey,
category: categoryName,
label: item.label ?? item.title ?? productName,
thumbnailUrl: thumbnailForItem(item),
previewAspectRatio: previewAspectRatioForItem(item),
previewColors: product.styles?.background_colors,
palette: item.type === 2 ? item.palette : undefined,
paletteLabel: item.type === 2 ? formatPaletteName(item.palette) : undefined,
previewGradient: item.type === 2 ? nameplatePaletteGradient(item.palette) : undefined,
assets: assets.map((a) => ({
role: a.role,
url: a.url,
filename: a.filename,
mimeType: guessMimeType(a.filename),
})),
};
if (item.type === 1) {
entry.effect = {
animationType: item.animationType,
thumbnailPreviewSrc: item.thumbnailPreviewSrc,
reducedMotionSrc: item.reducedMotionSrc,
effects: item.effects,
};
}
items.push(entry);
}
}
return items;
}
function isPublishedCatalog(document) {
return (
document &&
document.schema_version === 1 &&
typeof document.type === 'string' &&
Array.isArray(document.items) &&
document.items.every(
(item) =>
item &&
typeof item.id === 'string' &&
typeof item.skuId === 'string' &&
typeof item.type === 'string' &&
Array.isArray(item.assets)
)
);
}
async function fetchPublishedCatalog() {
const documents = await Promise.all(
PUBLISHED_CATALOG_URLS.map(async (url) => {
const response = await fetch(url, { headers: { Accept: 'application/json' } });
if (!response.ok) {
throw new Error(`Published collectibles catalog request failed with HTTP ${response.status}.`);
}
const document = await response.json();
if (!isPublishedCatalog(document)) {
throw new Error('Published collectibles catalog has an unsupported schema.');
}
return document;
})
);
const items = documents.flatMap((document) => document.items);
if (items.length === 0) {
throw new Error('Published collectibles catalog is empty.');
}
return items;
}
async function downloadAsset(url, { retries = 4 } = {}) {
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(url, {
headers: { 'User-Agent': DEFAULT_USER_AGENT, Accept: '*/*' },
});
} catch (err) {
if (attempt >= retries) throw err;
await sleep(500 * 2 ** attempt);
continue;
}
if (res.status === 429) {
const retryAfter = Number(res.headers.get('retry-after')) || 1;
await sleep((retryAfter + 0.5) * 1000);
continue;
}
if (!res.ok) {
if (attempt >= retries) {
throw new Error(`Failed to download asset: ${res.status} ${res.statusText}`);
}
await sleep(500 * 2 ** attempt);
continue;
}
const buf = Buffer.from(await res.arrayBuffer());
return buf;
}
}
function createDiscordCollectiblesService(store) {
let catalogCache = store.get('discordCollectiblesCatalog') || null;
if (
!catalogCache ||
!Array.isArray(catalogCache.items) ||
typeof catalogCache.fetchedAt !== 'string'
) {
catalogCache = null;
}
let catalogCacheAt = catalogCache?.fetchedAt ? Date.parse(catalogCache.fetchedAt) : 0;
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
store.delete('discordCollectiblesToken');
async function getCatalog({ force = false } = {}) {
const now = Date.now();
if (!force && catalogCache && now - catalogCacheAt < CATALOG_TTL_MS) {
return { success: true, data: catalogCache };
}
try {
const items = await fetchPublishedCatalog();
catalogCache = { items, fetchedAt: new Date().toISOString() };
catalogCacheAt = now;
store.set('discordCollectiblesCatalog', catalogCache);
return { success: true, data: catalogCache };
} catch (err) {
if (catalogCache) {
return {
success: true,
data: { ...catalogCache, stale: true },
};
}
return { success: false, error: err instanceof Error ? err.message : String(err) };
}
}
async function downloadAssets(assetUrls) {
const results = [];
for (const asset of assetUrls) {
const buffer = await downloadAsset(asset.url);
results.push({
role: asset.role,
url: asset.url,
filename: asset.filename,
mimeType: asset.mimeType || guessMimeType(asset.filename),
data: buffer,
});
}
return results;
}
return {
getCatalog,
downloadAssets,
ITEM_TYPE_FOLDER,
};
}
module.exports = { createDiscordCollectiblesService, extractCollectibleItems, urlsForItem };

View File

@@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer } = require('electron');
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
const { exec, execFile, execFileSync } = require('child_process');
@@ -8,6 +8,7 @@ const openPkg = require('open');
const open = openPkg.default;
const openApps = openPkg.apps;
const PaarrotAPIServer = require('./api-server');
const { createDiscordCollectiblesService } = require('./discord-collectibles');
const https = require('https');
const http = require('http');
const AdmZip = require('adm-zip');
@@ -104,6 +105,7 @@ function isAppOwnedUrl(targetUrl, currentUrl) {
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const store = new Store();
const discordCollectibles = createDiscordCollectiblesService(store);
const PROTOCOL_SCHEME = 'paarrot';
let mainWindow = null;
@@ -1275,14 +1277,104 @@ ipcMain.handle('open-external-url', async (event, url) => {
}
});
/**
* Save a media blob from the renderer via a native Save dialog.
* Payload: { filename, mimeType?, data: Uint8Array | ArrayBuffer | number[] }
*/
ipcMain.handle('media:save-file', async (event, payload = {}) => {
try {
const filename = typeof payload.filename === 'string' && payload.filename.trim()
? path.basename(payload.filename.trim()) || 'download'
: 'download';
const mimeType = typeof payload.mimeType === 'string' ? payload.mimeType : '';
const raw = payload.data;
if (!raw) {
return { success: false, error: 'Missing file data' };
}
const buffer = Buffer.from(
raw instanceof ArrayBuffer
? new Uint8Array(raw)
: ArrayBuffer.isView(raw)
? new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
: raw
);
const ext = path.extname(filename).replace(/^\./, '').toLowerCase();
const filters = (() => {
if (mimeType.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) {
return [
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType.startsWith('video/') || ['mp4', 'webm', 'mkv', 'mov'].includes(ext)) {
return [
{ name: 'Videos', extensions: ['mp4', 'webm', 'mkv', 'mov'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType.startsWith('audio/') || ['mp3', 'ogg', 'wav', 'm4a', 'flac'].includes(ext)) {
return [
{ name: 'Audio', extensions: ['mp3', 'ogg', 'wav', 'm4a', 'flac'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType === 'application/pdf' || ext === 'pdf') {
return [
{ name: 'PDF', extensions: ['pdf'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (ext) {
return [
{ name: ext.toUpperCase(), extensions: [ext] },
{ name: 'All Files', extensions: ['*'] },
];
}
return [{ name: 'All Files', extensions: ['*'] }];
})();
const win = BrowserWindow.fromWebContents(event.sender) || mainWindow;
const result = await dialog.showSaveDialog(win || undefined, {
title: 'Save file',
defaultPath: filename,
filters,
});
if (result.canceled || !result.filePath) {
return { success: true, canceled: true };
}
await fs.promises.writeFile(result.filePath, buffer);
return { success: true, path: result.filePath };
} catch (error) {
console.error('[media:save-file] failed:', error);
return { success: false, error: error.message || String(error) };
}
});
// Read clipboard image
ipcMain.handle('read-clipboard-image', async () => {
try {
// Extra Things: only trust an image when the clipboard actually advertises one.
// On Linux, readImage() can return a stale bitmap after copying plain text/URLs.
const formats = clipboard.availableFormats();
const hasImageFormat = formats.some((format) => format.startsWith('image/'));
if (!hasImageFormat) {
return { success: true, data: null };
}
const image = clipboard.readImage();
if (image.isEmpty()) {
return { success: true, data: null };
}
const { width, height } = image.getSize();
if (width < 2 || height < 2) {
return { success: true, data: null };
}
// Convert to PNG base64
const pngBuffer = image.toPNG();
const base64 = pngBuffer.toString('base64');
@@ -1333,33 +1425,33 @@ ipcMain.handle('play-notification-sound', async (event, soundType = 'message') =
}
});
// Get YouTube stream using yt-dlp
ipcMain.handle('get-youtube-stream', async (event, url) => {
// Get a direct YouTube stream using yt-dlp.
async function getYouTubeStream(url) {
try {
// Check if yt-dlp is available
try {
await execAsync('yt-dlp --version');
await execFileAsync('yt-dlp', ['--version']);
} catch {
return {
success: false,
error: 'yt-dlp is not installed. Please install yt-dlp to use YouTube features.'
error: 'yt-dlp is not installed. Please install yt-dlp to use YouTube features.',
};
}
// Get title
let title = 'YouTube Video';
try {
const titleResult = await execAsync(`yt-dlp --get-title "${url}"`);
const titleResult = await execFileAsync('yt-dlp', ['--no-playlist', '--get-title', url]);
title = titleResult.stdout.trim();
} catch (e) {
console.warn('Failed to get video title:', e.message);
}
// Get video URL
const result = await execAsync(
`yt-dlp -g -f "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best" "${url}"`
);
const result = await execFileAsync('yt-dlp', [
'--no-playlist',
'-g',
'-f',
'best[height<=1080]/bestvideo[height<=1080]+bestaudio/best',
url,
]);
const videoUrl = result.stdout.trim().split('\n')[0];
if (!videoUrl) {
@@ -1368,14 +1460,19 @@ ipcMain.handle('get-youtube-stream', async (event, url) => {
return {
success: true,
data: { video_url: videoUrl, title }
data: { video_url: videoUrl, title },
};
} catch (error) {
return {
success: false,
error: `yt-dlp error: ${error.message}`
error: `yt-dlp error: ${error.message}`,
};
}
}
// Get YouTube stream using yt-dlp
ipcMain.handle('get-youtube-stream', async (event, url) => {
return getYouTubeStream(url);
});
// Background sync stubs (desktop doesn't need it)
@@ -1811,6 +1908,35 @@ ipcMain.handle('plugin:read-code', async (event, pluginId) => {
}
});
ipcMain.handle('discord-collectibles:fetch-catalog', async (event, { force } = {}) => {
try {
return await discordCollectibles.getCatalog({ force: Boolean(force) });
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('discord-collectibles:download-assets', async (event, { assets }) => {
try {
if (!Array.isArray(assets) || assets.length === 0) {
return { success: false, error: 'No assets to download' };
}
const downloaded = await discordCollectibles.downloadAssets(assets);
return {
success: true,
data: downloaded.map((item) => ({
role: item.role,
url: item.url,
filename: item.filename,
mimeType: item.mimeType,
data: item.data,
})),
};
} catch (error) {
return { success: false, error: error.message };
}
});
console.log('Paarrot Electron main process started');
console.log('Development mode:', isDev);
console.log('Platform:', process.platform);

View File

@@ -104,6 +104,12 @@ contextBridge.exposeInMainWorld('electron', {
readImage: () => ipcRenderer.invoke('read-clipboard-image'),
writeText: (text) => ipcRenderer.invoke('write-clipboard-text', text)
},
// Media save (native Save dialog for images / videos / files)
media: {
saveFile: ({ filename, mimeType, data }) =>
ipcRenderer.invoke('media:save-file', { filename, mimeType, data }),
},
// Audio
audio: {
playNotificationSound: (soundType = 'message') => ipcRenderer.invoke('play-notification-sound', soundType),
@@ -187,7 +193,13 @@ contextBridge.exposeInMainWorld('electron', {
protocol: {
getStatus: () => ipcRenderer.invoke('protocol:get-status'),
repair: () => ipcRenderer.invoke('protocol:repair')
}
},
// Discord shop collectibles (published catalog + on-demand CDN download)
discordCollectibles: {
fetchCatalog: (force = false) => ipcRenderer.invoke('discord-collectibles:fetch-catalog', { force }),
downloadAssets: (assets) => ipcRenderer.invoke('discord-collectibles:download-assets', { assets }),
},
});
// Preload script ready

View File

@@ -208,6 +208,81 @@
{
"name": "Messaging",
"item": [
{
"name": "Get Group Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/groups?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"groups"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across joined group channels (non-DMs)."
}
},
{
"name": "Get DM Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/dms?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"dms"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across joined direct messages."
}
},
{
"name": "Get Combined Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/combined?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"combined"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across groups and DMs together."
}
},
{
"name": "Send Message to Room",
"request": {

View File

@@ -1,6 +1,6 @@
{
"name": "paarrot",
"version": "4.11.163",
"version": "4.11.191",
"description": "Paarrot - A Matrix client based on Cinny",
"homepage": "https://github.com/Paarrot/Paarrot-Desktop",
"repository": {
@@ -15,11 +15,13 @@
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
"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 .",
"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": "node scripts/generate-update-manifest.mjs && node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
"build:local": "node scripts/generate-update-manifest.mjs && 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:win": "npm run build -- --win",
"postman:generate": "node scripts/generate-postman-collection.js"
"postman:generate": "node scripts/generate-postman-collection.js",
"generate:update-manifest": "node scripts/generate-update-manifest.mjs",
"playground": "npm --prefix cinny run playground"
},
"keywords": [],
"author": {

View File

@@ -164,6 +164,87 @@ const collection = {
{
name: 'Messaging',
item: [
{
name: 'Get Group Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/groups?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'groups'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across joined group channels (non-DMs).',
},
},
{
name: 'Get DM Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/dms?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'dms'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across joined direct messages.',
},
},
{
name: 'Get Combined Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/combined?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'combined'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across groups and DMs together.',
},
},
{
name: 'Get Group Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/groups',
host: ['{{baseUrl}}'],
path: ['unreads', 'groups'],
},
description: 'List unread group conversations (non-DMs), including counts and latest message preview.',
},
},
{
name: 'Get DM Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/dms',
host: ['{{baseUrl}}'],
path: ['unreads', 'dms'],
},
description: 'List unread DM conversations, including counts and latest message preview.',
},
},
{
name: 'Get Combined Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/combined',
host: ['{{baseUrl}}'],
path: ['unreads', 'combined'],
},
description: 'List unread conversations across groups and DMs.',
},
},
{
name: 'Send Message to Room',
request: {

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
/**
* Desktop repo wrapper — manifest generator lives in cinny/scripts/.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const script = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'cinny',
'scripts',
'generate-update-manifest.mjs'
);
const result = spawnSync(process.execPath, [script], { stdio: 'inherit' });
process.exit(result.status ?? 1);

View File

@@ -18,6 +18,17 @@
"updater:allow-download",
"updater:allow-download-and-install",
"dialog:default",
"fs:default",
"fs:allow-write-file",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$HOME/**" },
{ "path": "$DOWNLOAD/**" },
{ "path": "$DESKTOP/**" },
{ "path": "$DOCUMENT/**" }
]
},
"process:allow-restart",
{
"identifier": "http:default",

View File

@@ -61,6 +61,16 @@ async function runTests() {
// Get current room
await testEndpoint('Get Current Room', 'GET', '/room/current');
// Recent messages by scope
await testEndpoint('Get Group Messages', 'GET', '/messages/groups');
await testEndpoint('Get DM Messages', 'GET', '/messages/dms');
await testEndpoint('Get Combined Messages', 'GET', '/messages/combined?limit=10');
// Unread conversations by scope
await testEndpoint('Get Group Unreads', 'GET', '/unreads/groups');
await testEndpoint('Get DM Unreads', 'GET', '/unreads/dms');
await testEndpoint('Get Combined Unreads', 'GET', '/unreads/combined');
// Toggle mute
await testEndpoint('Toggle Mute', 'POST', '/mute/toggle');