Skip to content

connector: send group name and photo changes to LINE - #279

Open
clins1994 wants to merge 7 commits into
beeper:mainfrom
clins1994:claude/group-name-avatar-bb8fe5
Open

clins1994 wants to merge 7 commits into
beeper:mainfrom
clins1994:claude/group-name-avatar-bb8fe5

Conversation

@clins1994

@clins1994 clins1994 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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.

Edit Group Screen on Beeper Desktop
🇬🇧 More details · 8 min read

Summary

The bridge now handles m.room.name and m.room.avatar in LINE groups (MIDs that start with C):

Matrix change LINE call
Rename TalkService.updateChat with the full chat object from getChats, updatedAttribute: 1
Set photo POST /api/obs/uploadProfile?mid=<group MID>, raw JPEG bytes, Content-Type: image/jpeg
Remove photo updateChat with picturePath: "", updatedAttribute: 2

Groups advertise both state events in their room capabilities, so Beeper shows the edit options.

Before testing

  • Beeper apps send room name and photo changes as com.beeper.send_state requests. bridgev2 rejects them with "send state requests are not enabled" unless the bridge config has bridge.enable_send_state_requests: true.
  • The default is false, and bbctl's bridgev2 config template does not set it. The README now says to turn it on.
  • Question for maintainers: do Beeper Cloud configs already turn it on?

Why

Issue #143 asks for this. My earlier attempt, #184, was closed when I stepped away, and main has 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

  • LINE Chrome Extension 3.7.2, recorded with the repo's CDP recorder (scripts/line-chrome-capture.sh, .agents/commands/line-capture.md).
  • A test group with 2 members and Letter Sealing on.
  • 20 labeled sample files: JPEG, PNG (opaque and transparent), WebP, HEIC, AVIF, BMP, TIFF, GIF (static and animated), animated WebP, APNG, four video formats, wide, tall, 4096² and 64² images.
  • The bridge was logged out during the capture, because it uses the same Chrome Extension slot.
  • Each step waited about 10 seconds, so every step maps to its own network calls.

Test matrix and results

Group Steps Result
A. Name Rename to Capture Test, to テスト 🎉 Ünïcode, to 60 characters, to an empty name, and to the same name Each saved rename sends one updateChat with attribute 1. The 60-character name reached the server cut to 50. An empty or unchanged name sends nothing.
B. Photo Upload each of the 20 samples JPEG, PNG, GIF and BMP upload. The file picker greys out WebP, HEIC, AVIF, TIFF, APNG, animated WebP and all video. Every upload is Content-Type: image/jpeg. An animated GIF keeps only its first frame, also on LINE iOS. There is no crop step.
C. Remove Remove the photo, then try again Removing sends updateChat with an empty picturePath and attribute 2. There is no delete endpoint. The option is hidden when the group has the default photo. The extension has no preset images.
D. Banner Look for a banner editor The extension cannot show or edit the group banner.
E. From a phone On LINE iOS, rename, change the photo, remove it, pick a preset, change and remove the banner Rename gives operation 121 with param2 = 1. Photo changes, removals and presets give 121 with param2 = 2. Banner changes give no operation and no request at all.

After each change, the extension calls getChats for the group. Every upload or preset gives a new picturePath (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

Behavior Chrome Extension 3.7.2 This PR
Rename call updateChat, full chat object, attribute 1 Same. The chat from getChats goes back unchanged except chatName. Numbers stay exact (json.Number).
Name rules Cut to 50, empty and unchanged names send nothing Names over 50 UTF-16 units and empty names fail with a clear status, and bridgev2 reverts the Matrix name. bridgev2 skips unchanged names.
Photo call obs/uploadProfile?mid=, raw bytes Same
Photo format JPEG only, picker allows JPEG, PNG, GIF and BMP Same formats: JPEG, PNG, GIF and BMP become JPEG, and other formats are refused with "LINE group photos must be JPEG, PNG, GIF or BMP". JPEGs up to 4096 px are sent unchanged. Bigger photos are scaled down to fit 4096 px with their EXIF rotation applied, and photos over 50 megapixels are refused before decoding.
Animation First frame only First frame only
Transparency Not recorded Drawn on white
Remove call updateChat, picturePath: "", attribute 2 Same
After the change getChats for the group Same. The new picturePath becomes the portal avatar ID, so the op 121 resync does not bridge the photo back.
Headers on uploadProfile The recorder showed only Accept, Content-Type and X-Line-Access The headers of the other line-chrome-gw calls, including x-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: GetRawChat reads one chat from getChats as a map, with exact numbers. UpdateChat sends it back with an attribute.
  • pkg/line/client.go: UploadChatPicture posts JPEG bytes to obs/uploadProfile. postWithHMAC now has a variant that takes a context and a content type.
  • pkg/line/structs.go: ChatAttribute (ChatAttributeName = 1, ChatAttributePicture = 2) and UpdateChatRequest.
  • pkg/connector/group_meta.go: HandleMatrixRoomName, HandleMatrixRoomAvatar, the name rules and convertAvatarToJPEG with its size limits. Legacy rooms (R MIDs) and DMs are rejected.
  • pkg/connector/userinfo.go: groups add +group to the capability ID and list m.room.name and m.room.avatar as fully supported.
  • pkg/connector/sync.go, pkg/connector/creategroup.go: groups set the power level for m.room.name and m.room.avatar to 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 need bridge.enable_send_state_requests: true.

Receiving changes did not change. Operation 121 still runs syncSingleChat, and the C_PN system message still renames the room.

What changed from #184

#184 This PR
Uploaded the original content type Converts every image to JPEG, like the extension
No conversion for animated images First frame only
No name length check Rejects names over 50 characters
Stored a hash as the avatar ID after an upload Fetches the chat again and stores the new picturePath, which is the ID the resync reports
Its own retry wrapper The current callLine recovery and wrapSendError statuses
Changed FlexibleMidMap parsing Not needed: the raw map from GetRawChat keeps every field

Kept 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 vet and staticcheck on $(go list ./... | grep -v /ltsm): no findings.
  • go fmt ./... and goimports -local "github.com/beeper/line": no changes.
  • New tests in pkg/line/chat_update_test.go: the getChats to updateChat round trip keeps a 19-digit number exact, the uploadProfile URL, headers and raw body, and non-zero response codes.
  • New tests in pkg/connector/group_meta_test.go cover 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.
  • A one-off run of convertAvatarToJPEG on 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_requests on.

  • Names: renames with ASCII, Japanese and emoji, and a 50-character name, reached LINE. A 60-character name and an empty name were refused with a clear message, and the Matrix name went back. The same name sent nothing.
  • Photos: JPEG, PNG (opaque and transparent), GIF and BMP reached LINE. An animated GIF showed its first frame on LINE and Beeper iOS. Removing the photo showed LINE's default placeholder.
  • From LINE: renames and photo changes made in LINE iOS still reached Beeper.
  • Permissions: editing worked in a group the tester did not create. DMs did not offer the options, and groups created before this change got them.
  • Formats: this round ran before the bridge started refusing formats the extension does not allow. The new "must be JPEG, PNG, GIF or BMP" message still needs a retest. Large portrait photos and very wide or tall images were not tried.

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.name and m.room.avatar state 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)

  • Headers on uploadProfile. The recorder did not show x-hmac on any call, including updateChat, 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.
  • Image size. The recorder does not save binary request bodies, so we do not know if the extension resizes large images. LINE accepted a 4096² image from the extension, so this PR sends photos up to 4096 px at full size and scales bigger ones down to fit.
  • Transparency color. The capture did not record what the extension does with transparent pixels. This PR uses white.
  • Letter Sealing off groups. Only a Letter Sealing on group was captured. These calls carry no encrypted fields, so we expect the same behavior, but nobody checked it.

Related

Diagram by Greptile

Screenshot 2026-10-25 at 5 40 13

🪴 Generated with Claude Code on Opus 5.5 using clins:create-pr

clins1994 and others added 2 commits October 25, 2026 04:11
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>
@clins1994

clins1994 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

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 read

Timeline

Steps 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 updateChat carry its reqSeq. Ops caused by an upload carry reqSeq=-1.

Time (UTC) Call Fields code
18:39:07 op 121 (SSE) param1=, param2=1, reqSeq=756430167
18:39:07 updateChat updatedAttribute=1, chatName='Capture Test', picturePath=set 0
18:39:28 updateChat updatedAttribute=1, chatName='テスト 🎉 Ünïcode', picturePath=set 0
18:39:29 op 121 (SSE) param1=, param2=1, reqSeq=756430168
18:39:43 op 121 (SSE) param1=, param2=1, reqSeq=756430169
18:39:43 updateChat updatedAttribute=1, chatName='12345678901234567890123456789012345678901234567890', picturePath=set 0
18:40:17 op 121 (SSE) param1=, param2=1, reqSeq=756430170
18:40:17 updateChat updatedAttribute=1, chatName='LSON Group', picturePath=set 0
18:42:31 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:31 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:39 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:40 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:45 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:45 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:51 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:51 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:56 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:56 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:05 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:05 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:54 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:54 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:58 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:58 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:04 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:44:04 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:08 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:44:08 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:19 op 121 (SSE) param1=, param2=2, reqSeq=756430171
18:44:19 updateChat updatedAttribute=2, chatName='LSON Group', picturePath="" 0
18:49:25 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:49:25 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:51:27 op 121 (SSE) param1=, param2=1, reqSeq=57442308
18:52:09 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:52:20 op 121 (SSE) param1=, param2=2, reqSeq=57442309
18:52:31 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:52:38 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:53:41 op 121 (SSE) param1=, param2=1, reqSeq=57442310

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 lecture

Chronologie

Les é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 updateChat portent son reqSeq. Les ops causées par un envoi portent reqSeq=-1.

Heure (UTC) Appel Champs code
18:39:07 op 121 (SSE) param1=, param2=1, reqSeq=756430167
18:39:07 updateChat updatedAttribute=1, chatName='Capture Test', picturePath=set 0
18:39:28 updateChat updatedAttribute=1, chatName='テスト 🎉 Ünïcode', picturePath=set 0
18:39:29 op 121 (SSE) param1=, param2=1, reqSeq=756430168
18:39:43 op 121 (SSE) param1=, param2=1, reqSeq=756430169
18:39:43 updateChat updatedAttribute=1, chatName='12345678901234567890123456789012345678901234567890', picturePath=set 0
18:40:17 op 121 (SSE) param1=, param2=1, reqSeq=756430170
18:40:17 updateChat updatedAttribute=1, chatName='LSON Group', picturePath=set 0
18:42:31 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:31 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:39 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:40 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:45 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:45 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:51 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:51 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:42:56 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:42:56 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:05 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:05 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:54 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:54 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:43:58 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:43:58 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:04 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:44:04 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:08 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:44:08 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:44:19 op 121 (SSE) param1=, param2=2, reqSeq=756430171
18:44:19 updateChat updatedAttribute=2, chatName='LSON Group', picturePath="" 0
18:49:25 obs/uploadProfile?mid=<groupMid> Content-Type=image/jpeg 0
18:49:25 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:51:27 op 121 (SSE) param1=, param2=1, reqSeq=57442308
18:52:09 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:52:20 op 121 (SSE) param1=, param2=2, reqSeq=57442309
18:52:31 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:52:38 op 121 (SSE) param1=, param2=2, reqSeq=-1
18:53:41 op 121 (SSE) param1=, param2=1, reqSeq=57442310

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

clins1994 and others added 5 commits October 25, 2026 04:35
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>
@clins1994
clins1994 marked this pull request as ready for review October 24, 2026 20:30
@clins1994

Copy link
Copy Markdown
Contributor Author

@highesttt 👋 long time no contribute. please review when you have a moment. lmk if u need more info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

feat: ✨ allow editing group image and name

1 participant