Repository navigation
Conversation
Renaming a bridged LINE group or changing or removing its photo from Matrix now reaches LINE, using the same calls as the Chrome extension: - rename: updateChat with the chat object from getChats, attribute 1 - set photo: POST obs/uploadProfile?mid=<group> with JPEG bytes - remove photo: updateChat with an empty picturePath, attribute 2 Any image is converted to JPEG (first frame for animations, transparency drawn on white) with Go decoders, or ffmpeg for HEIC, AVIF and similar. Names must be 1-50 characters. Groups advertise both state events in their capabilities and let every member send them. Closes beeper#143 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Capture evidence for this PR: every name and photo request the LINE Chrome Extension 3.7.2 sent in the test group, with the op 121 events LINE sent back. Group and user MIDs, picture paths, times and the access token are replaced with placeholders. Voici chaque requête de nom et de photo de l'extension Chrome LINE 3.7.2 dans le groupe de test, avec les op 121 renvoyées par LINE. Les MID, les picturePath, les heures et le jeton d'accès sont remplacés par des marqueurs. 🇬🇧 More details · 3 min readTimelineSteps A1 to A3 and the rename back are the 4 renames. The 60-character name in A3 reached the server as 50 characters. B is the first 10 uploads, C2 is the removal and C5 is the upload at 18:49. The events from 18:51 on come from LINE iOS (step E). Ops caused by
Rename request body (A1), redacted: [
{
"reqSeq": 756430167,
"chat": {
"type": 1,
"chatMid": "<groupMid>",
"createdTime": "<time>",
"notificationDisabled": false,
"favoriteTimestamp": "0",
"chatName": "Capture Test",
"picturePath": "<picturePath-1>",
"extra": {
"groupExtra": {
"creator": "<userMid-1>",
"preventedJoinByTicket": false,
"invitationTicket": "",
"memberMids": {
"<userMid-2>": "<time>",
"<userMid-3>": "<time>",
"<userMid-4>": "<time>",
"<userMid-1>": "<time>"
},
"inviteeMids": {},
"addFriendDisabled": false,
"ticketDisabled": false,
"autoName": false
}
}
},
"updatedAttribute": 1
}
]Remove-photo request body (C2), redacted: [
{
"reqSeq": 756430171,
"chat": {
"type": 1,
"chatMid": "<groupMid>",
"createdTime": "<time>",
"notificationDisabled": false,
"favoriteTimestamp": "0",
"chatName": "LSON Group",
"picturePath": "",
"extra": {
"groupExtra": {
"creator": "<userMid-1>",
"preventedJoinByTicket": false,
"invitationTicket": "",
"memberMids": {
"<userMid-2>": "<time>",
"<userMid-3>": "<time>",
"<userMid-4>": "<time>",
"<userMid-1>": "<time>"
},
"inviteeMids": {},
"addFriendDisabled": false,
"ticketDisabled": false,
"autoName": false
}
}
},
"updatedAttribute": 2
}
]Photo upload request as recorded, redacted: {
"method": "POST",
"url": "https://line-chrome-gw.line-apps.com/api/obs/uploadProfile?mid=<groupMid>",
"request_headers": {
"X-Line-Access": "<redacted>",
"Accept": "application/json, text/plain, */*",
"Content-Type": "image/jpeg"
},
"status": 200,
"response_body": {
"code": 0,
"message": "OK"
}
}🇫🇷 Plus de détails · 3 min de lectureChronologieLes étapes A1 à A3 et le retour au nom d'origine sont les 4 renommages. Le nom de 60 caractères de A3 est arrivé au serveur avec 50 caractères. B correspond aux 10 premiers envois, C2 à la suppression et C5 à l'envoi de 18:49. Les événements à partir de 18:51 viennent de LINE iOS (étape E). Les ops causées par
Corps de la requête de renommage (A1), caviardé : [
{
"reqSeq": 756430167,
"chat": {
"type": 1,
"chatMid": "<groupMid>",
"createdTime": "<time>",
"notificationDisabled": false,
"favoriteTimestamp": "0",
"chatName": "Capture Test",
"picturePath": "<picturePath-1>",
"extra": {
"groupExtra": {
"creator": "<userMid-1>",
"preventedJoinByTicket": false,
"invitationTicket": "",
"memberMids": {
"<userMid-2>": "<time>",
"<userMid-3>": "<time>",
"<userMid-4>": "<time>",
"<userMid-1>": "<time>"
},
"inviteeMids": {},
"addFriendDisabled": false,
"ticketDisabled": false,
"autoName": false
}
}
},
"updatedAttribute": 1
}
]Corps de la requête de suppression de photo (C2), caviardé : [
{
"reqSeq": 756430171,
"chat": {
"type": 1,
"chatMid": "<groupMid>",
"createdTime": "<time>",
"notificationDisabled": false,
"favoriteTimestamp": "0",
"chatName": "LSON Group",
"picturePath": "",
"extra": {
"groupExtra": {
"creator": "<userMid-1>",
"preventedJoinByTicket": false,
"invitationTicket": "",
"memberMids": {
"<userMid-2>": "<time>",
"<userMid-3>": "<time>",
"<userMid-4>": "<time>",
"<userMid-1>": "<time>"
},
"inviteeMids": {},
"addFriendDisabled": false,
"ticketDisabled": false,
"autoName": false
}
}
},
"updatedAttribute": 2
}
]Requête d'envoi de photo telle qu'enregistrée, caviardée : {
"method": "POST",
"url": "https://line-chrome-gw.line-apps.com/api/obs/uploadProfile?mid=<groupMid>",
"request_headers": {
"X-Line-Access": "<redacted>",
"Accept": "application/json, text/plain, */*",
"Content-Type": "image/jpeg"
},
"status": 200,
"response_body": {
"code": 0,
"message": "OK"
}
}🤖 Generated with Claude Code using clins:create-pr |
Refuse photos over 50 megapixels before decoding them, scale bigger ones down to fit 4096x4096 (the largest size LINE accepted from the Chrome Extension), and have ffmpeg scale its output and cap how much of it the bridge reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fall back to ffmpeg when Go reads an image's header but can't decode it, which is the case for animated WebP. When a JPEG is re-encoded because it is over 4096 pixels, apply its EXIF orientation first so it isn't sideways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beeper apps send room name and photo changes as com.beeper.send_state requests, which bridgev2 rejects unless bridge.enable_send_state_requests is on. bbctl's config template does not set it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffmpeg before 9.0, which Alpine ships, can't decode animated WebP, so these photos failed on the Docker image. Each frame of an animated WebP holds the chunks of a still image, so wrap the first one as a still WebP for Go's decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LINE Chrome Extension's file picker only allows JPEG, PNG, GIF and BMP for group photos. Refuse other formats with a clear message instead of converting them with ffmpeg, and drop the animated WebP decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@highesttt 👋 long time no contribute. please review when you have a moment. lmk if u need more info |
Renaming a LINE group from Beeper, or changing or removing its photo, now reaches LINE with the same calls the LINE Chrome Extension sends. It passed a live test from Beeper Desktop and Beeper iOS, and it is ready for review.
🇬🇧 More details · 8 min read
Summary
The bridge now handles
m.room.nameandm.room.avatarin LINE groups (MIDs that start withC):TalkService.updateChatwith the full chat object fromgetChats,updatedAttribute: 1POST /api/obs/uploadProfile?mid=<group MID>, raw JPEG bytes,Content-Type: image/jpegupdateChatwithpicturePath: "",updatedAttribute: 2Groups advertise both state events in their room capabilities, so Beeper shows the edit options.
Before testing
com.beeper.send_staterequests. bridgev2 rejects them with "send state requests are not enabled" unless the bridge config hasbridge.enable_send_state_requests: true.false, and bbctl's bridgev2 config template does not set it. The README now says to turn it on.Why
Issue #143 asks for this. My earlier attempt, #184, was closed when I stepped away, and
mainhas changed a lot since then. So we captured the Chrome Extension again from scratch and built this PR from that capture.How we figured it out
Setup
scripts/line-chrome-capture.sh,.agents/commands/line-capture.md).Test matrix and results
Capture Test, toテスト 🎉 Ünïcode, to 60 characters, to an empty name, and to the same nameupdateChatwith attribute 1. The 60-character name reached the server cut to 50. An empty or unchanged name sends nothing.Content-Type: image/jpeg. An animated GIF keeps only its first frame, also on LINE iOS. There is no crop step.updateChatwith an emptypicturePathand attribute 2. There is no delete endpoint. The option is hidden when the group has the default photo. The extension has no preset images.param2 = 1. Photo changes, removals and presets give 121 withparam2 = 2. Banner changes give no operation and no request at all.After each change, the extension calls
getChatsfor the group. Every upload or preset gives a newpicturePath(14 distinct values in this capture). The redacted requests and events are in the first comment of this PR.Chrome Extension and bridge, side by side
updateChat, full chat object, attribute 1getChatsgoes back unchanged exceptchatName. Numbers stay exact (json.Number).obs/uploadProfile?mid=, raw bytesupdateChat,picturePath: "", attribute 2getChatsfor the grouppicturePathbecomes the portal avatar ID, so the op 121 resync does not bridge the photo back.uploadProfileAccept,Content-TypeandX-Line-Accessline-chrome-gwcalls, includingx-hmac. Not verified, see below.Banner: out of scope
LINE sends no event when the banner changes, and the Chrome Extension cannot edit it. So this bridge cannot read or set the banner.
Changes
pkg/line/methods.go:GetRawChatreads one chat fromgetChatsas a map, with exact numbers.UpdateChatsends it back with an attribute.pkg/line/client.go:UploadChatPictureposts JPEG bytes toobs/uploadProfile.postWithHMACnow has a variant that takes a context and a content type.pkg/line/structs.go:ChatAttribute(ChatAttributeName = 1,ChatAttributePicture = 2) andUpdateChatRequest.pkg/connector/group_meta.go:HandleMatrixRoomName,HandleMatrixRoomAvatar, the name rules andconvertAvatarToJPEGwith its size limits. Legacy rooms (RMIDs) and DMs are rejected.pkg/connector/userinfo.go: groups add+groupto the capability ID and listm.room.nameandm.room.avataras fully supported.pkg/connector/sync.go,pkg/connector/creategroup.go: groups set the power level form.room.nameandm.room.avatarto 0, because any LINE member can change them.pkg/connector/connector.go: the capability version goes from 7 to 8, so existing rooms get the new capabilities.README.md: Beeper setups needbridge.enable_send_state_requests: true.Receiving changes did not change. Operation 121 still runs
syncSingleChat, and theC_PNsystem message still renames the room.What changed from #184
picturePath, which is the ID the resync reportscallLinerecovery andwrapSendErrorstatusesFlexibleMidMapparsingGetRawChatkeeps every fieldKept from #184: the full chat object round trip,
obs/uploadProfile?mid=, HMAC signing, the power level overrides and the state capabilities.How it was tested
go test ./...: all packages pass.go vetandstaticcheckon$(go list ./... | grep -v /ltsm): no findings.go fmt ./...andgoimports -local "github.com/beeper/line": no changes.pkg/line/chat_update_test.go: thegetChatstoupdateChatround trip keeps a 19-digit number exact, theuploadProfileURL, headers and raw body, and non-zero response codes.pkg/connector/group_meta_test.gocover the name rules (50 ASCII, 50 Japanese, 25 and 26 emoji, empty) and the capabilities per chat type. They also cover JPEG pass-through, transparent PNG on white, the first GIF frame, BMP, refused formats, the size limits and EXIF rotation. The rename and remove-photo handlers run against a fake LINE server, and legacy rooms are rejected.convertAvatarToJPEGon all 20 capture samples. The JPEG, PNG, GIF and BMP files gave a valid JPEG at the source size, and the other files were refused, like in the extension's file picker. APNG is the one difference: it is a PNG file, so the bridge accepts it and uses its first frame.Live test
We tested on a self-hosted bridge from Beeper Desktop and Beeper iOS, in the same test group, with
enable_send_state_requestson.Open point: no notice in the timeline
Beeper shows no notice in the group's timeline when the name or photo changes, although the changes apply on both sides. The changes are in the room as
m.room.nameandm.room.avatarstate events. Beeper changes come from the user's own account, and LINE changes come from the bridge bot. Beeper does not show these state events in the timeline. LINE shows its own system message instead, like "Caio changed the group name". A follow-up can post those LINE system messages as notices.Not tested (and why)
uploadProfile. The recorder did not showx-hmacon any call, includingupdateChat, which needs it. So the recorder probably misses headers that Chrome adds late. This PR signs the upload like the other gateway calls. If LINE rejects the upload, try it without the HMAC first.Related
line-captureskill from clins1994/beeper-line-skillsDiagram by Greptile
🪴 Generated with Claude Code on Opus 5.5 using clins:create-pr