Repository navigation
Webhook avatars were stored full size and never resized (GRYT-1185) - #191
Merged
Merged
Conversation
The avatar a webhook post carries needs the same processing as a member's avatar. That code only lived inside the upload route's handler. It moves out as it was, except it returns a result now instead of writing the response. The SVG path, the size limit and the user row stay in the route. No behaviour change. `git diff --color-moved` shows the moved lines. Part of GRYT-1185. Co-Authored-By: Claude Opus 5 <[email protected]>
The webhook settings tab uploaded its avatar through POST /api/uploads, the
attachment route. So it was stored at whatever size it was sent. A per-message
avatar_url went the same way, as a card picture. Both now go through the same
pipeline as member avatars and group pictures. A still picture ends up as a
256 px AVIF under avatars/, with a 128 px thumbnail.
- New POST /api/uploads/webhook-avatar, which needs manage_webhooks. It's
shaped like the group icon route and replies { fileId, processing }. It
leaves the uploader's own avatar alone.
- avatar_url goes through storeAvatarPicture. It gets its own key in
webhook_media, so the same bytes in a card still get their own file, stored
as sent. The URL is still only downloaded once.
- The avatar_url description in the OpenAPI document says it's resized.
Co-Authored-By: Claude Opus 5 <[email protected]>
This was referenced Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing here is in a review-required path.
src/db,src/storage,src/middlewareandsrc/autharen't touched. The change only callsinsertFile,putObjectandensurePermissionas they are.Vikunja: GRYT-1185. Client: Gryt-chat/client#569. Docs: Gryt-chat/docs#115.
What was wrong
The webhook settings tab uploaded its avatar through
POST /api/uploads. That's the attachment route, and it keeps a picture as it was sent. A 2400×1600 PNG sent through it is stored as a 5.9 MB PNG with no thumbnail. The chat view draws the webhook's avatar from that full file. A per-messageavatar_urlfrom server#187 was stored the same way, as a card picture.What changed
POST /api/uploads/webhook-avatarrunsstoreAvatarImage("webhook"), the same handler as the avatar and group icon routes. It needsmanage_webhooks, like every other webhook route. It replies{ fileId, processing }and doesn't write the user row.src/services/avatarImage.tsasstoreAvatarPicture, so the webhook post route can call it too. The first commit is only that move.git diff --color-movedon it shows the moved lines. The other edits make it return a result instead of writing the response.avatar_urlon a webhook post goes throughstoreAvatarPicturenow. Card pictures are stored as sent, like before.avatar_urldescription inopenapi/webhooks.jsonsays it's resized.Please look at
webhook_mediakey. A per-message avatar is deduplicated underavatar:<sha256>instead of the bare hash. Otherwise the same bytes in a card could get handed the resized copy. It's a plain string in thesha256column, and that column has no format check, so there's no schema change.avatar_urland a card icon use the same URL, it's still downloaded once, then stored twice: resized for the avatar, as sent for the icon. If one store fails, only that slot getsstore_failed./api/uploads, and the server still accepts that file as a webhook avatar. I didn't add a check for it.File access and the sweep
uploaded_by_server_user_id, somayUseAvatarlets the uploader set it (A file token read any file, private channels and DMs included (GRYT-921) #183). Until a webhook wears it, only the uploader can read it. After that it counts as an avatar and every member can. Older files nobody is recorded as uploading stay readable (GRYT-921) #184's check isn't touched.getAllWebhookAvatarFileIdsfrom The media sweep deleted webhook avatars (GRYT-1183) #186 keeps it once a webhook wears it. Per-message avatars are inmessage_attachments, as before.Tests
webhookAvatar.test.tsuploads through the new route now, and adds:avatars/with a 128 px thumbnail, read back from storagemanage_webhooksgets 403 and nothing is storedavatar_urlis stored resized, while the same picture in a card isn'twebhookMedia.test.ts: the avatar and an icon from the same URL are one download and two stores, and a failed avatar store leaves the icon alone.uploadAuthOrder.test.tslists the new route.avatar_url, writing the user row for webhook uploads, sharing the dedupe key, the wrong permission, and mixing up the per-slot file id each fail at least one test.yarn test(1292 tests),yarn test:examples,yarn build,npx eslint .and the comment check pass locally.Verified
Throwaway server on this branch with filesystem storage, the client branch, headless Chrome:
avatar_file_idstayed null.avatar_urlpointed at a 644 KB 1200×630 PNG on raw.githubusercontent.com stored a 1.9 KB 256×256 AVIF for the avatar. The same URL as the card'simage_urlstayed the 644 KB PNG.POST /api/uploadsis still stored as it was sent, which is what the tab did before.🤖 Generated with Claude Code