Add REST endpoint for joining a group by invitation link.

signal-cli joinGroup --uri had no HTTP route. POST /v1/groups/{number}/join accepts the invite link. Joining by group id is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh 2026-09-22 12:41:11 +03:00
parent ebb7c1d4dc
commit 4747a6ddfc
5 changed files with 258 additions and 0 deletions

View File

@ -75,6 +75,10 @@ type ChangeGroupAdminsRequest struct {
Admins []string `json:"admins"`
}
type JoinGroupByLinkRequest struct {
Uri string `json:"uri"`
}
type LoggingConfiguration struct {
Level string `json:"Level"`
}
@ -1739,6 +1743,47 @@ func (a *Api) JoinGroup(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// @Summary Join a Signal Group via an invitation link.
// @Tags Groups
// @Description Join a Signal Group using an invitation link. The uri starts with https://signal.group/#
// @Accept json
// @Produce json
// @Success 204 {string} OK
// @Failure 400 {object} Error
// @Param number path string true "Registered Phone Number"
// @Param data body JoinGroupByLinkRequest true "Invitation link"
// @Router /v1/groups/{number}/join [post]
func (a *Api) JoinGroupByLink(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number"))
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - malformed number"})
return
}
if number == "" {
c.JSON(400, Error{Msg: "Couldn't process request - number missing"})
return
}
var req JoinGroupByLinkRequest
err = c.BindJSON(&req)
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - invalid request"})
return
}
if req.Uri == "" {
c.JSON(400, Error{Msg: "Couldn't process request - uri missing"})
return
}
err = a.signalClient.JoinGroupByLink(number, req.Uri)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// @Summary Quit a Signal Group.
// @Tags Groups
// @Description Quit the specified Signal Group.

View File

@ -2054,6 +2054,26 @@ func (s *SignalClient) JoinGroup(number string, groupId string) error {
return err
}
func (s *SignalClient) JoinGroupByLink(number string, uri string) error {
var err error
var jsonRpc2Client *JsonRpc2Client
if s.signalCliMode == JsonRpc {
type Request struct {
Uri string `json:"uri"`
}
request := Request{Uri: uri}
jsonRpc2Client, err = s.getJsonRpc2Client()
if err != nil {
return err
}
_, err = jsonRpc2Client.getRaw("joinGroup", &number, request)
} else {
cmd := []string{"--config", s.signalCliConfig, "-a", number, "joinGroup", "--uri", uri}
_, err = s.cliClient.Execute(true, cmd, "")
}
return err
}
func (s *SignalClient) QuitGroup(number string, groupId string) error {
var err error
var jsonRpc2Client *JsonRpc2Client

View File

@ -208,6 +208,17 @@ const docTemplate = `{
],
"type": "object"
},
"api.JoinGroupByLinkRequest": {
"properties": {
"uri": {
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"api.LoggingConfiguration": {
"properties": {
"Level": {
@ -1176,6 +1187,9 @@ const docTemplate = `{
"id": {
"type": "string"
},
"isVoiceNote": {
"type": "boolean"
},
"size": {
"type": "integer"
},
@ -1186,6 +1200,9 @@ const docTemplate = `{
"type": "integer"
}
},
"required": [
"isVoiceNote"
],
"type": "object"
},
"receive.AttachmentData": {
@ -1452,12 +1469,24 @@ const docTemplate = `{
"expiresInSeconds": {
"type": "integer"
},
"groupCallUpdate": {
"$ref": "#/definitions/receive.GroupCallUpdate"
},
"groupInfo": {
"$ref": "#/definitions/receive.GroupInfo"
},
"hasProfileKey": {
"type": "boolean"
},
"isEndSession": {
"type": "boolean"
},
"isExpirationUpdate": {
"type": "boolean"
},
"isProfileKeyUpdate": {
"type": "boolean"
},
"mentions": {
"items": {
"$ref": "#/definitions/receive.Mention"
@ -1549,6 +1578,14 @@ const docTemplate = `{
},
"type": "object"
},
"receive.GroupCallUpdate": {
"properties": {
"eraId": {
"type": "string"
}
},
"type": "object"
},
"receive.GroupInfo": {
"properties": {
"groupId": {
@ -2139,12 +2176,24 @@ const docTemplate = `{
"expiresInSeconds": {
"type": "integer"
},
"groupCallUpdate": {
"$ref": "#/definitions/receive.GroupCallUpdate"
},
"groupInfo": {
"$ref": "#/definitions/receive.GroupInfo"
},
"hasProfileKey": {
"type": "boolean"
},
"isEndSession": {
"type": "boolean"
},
"isExpirationUpdate": {
"type": "boolean"
},
"isProfileKeyUpdate": {
"type": "boolean"
},
"mentions": {
"items": {
"$ref": "#/definitions/receive.Mention"
@ -3372,6 +3421,53 @@ const docTemplate = `{
]
}
},
"/v1/groups/{number}/join": {
"post": {
"consumes": [
"application/json"
],
"description": "Join a Signal Group using an invitation link. The uri starts with https://signal.group/#",
"parameters": [
{
"description": "Registered Phone Number",
"in": "path",
"name": "number",
"required": true,
"type": "string"
},
{
"description": "Invitation link",
"in": "body",
"name": "data",
"required": true,
"schema": {
"$ref": "#/definitions/api.JoinGroupByLinkRequest"
}
}
],
"produces": [
"application/json"
],
"responses": {
"204": {
"description": "No Content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
},
"summary": "Join a Signal Group via an invitation link.",
"tags": [
"Groups"
]
}
},
"/v1/groups/{number}/{groupid}": {
"delete": {
"consumes": [

View File

@ -203,6 +203,17 @@
],
"type": "object"
},
"api.JoinGroupByLinkRequest": {
"properties": {
"uri": {
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"api.LoggingConfiguration": {
"properties": {
"Level": {
@ -1171,6 +1182,9 @@
"id": {
"type": "string"
},
"isVoiceNote": {
"type": "boolean"
},
"size": {
"type": "integer"
},
@ -1181,6 +1195,9 @@
"type": "integer"
}
},
"required": [
"isVoiceNote"
],
"type": "object"
},
"receive.AttachmentData": {
@ -1447,12 +1464,24 @@
"expiresInSeconds": {
"type": "integer"
},
"groupCallUpdate": {
"$ref": "#/definitions/receive.GroupCallUpdate"
},
"groupInfo": {
"$ref": "#/definitions/receive.GroupInfo"
},
"hasProfileKey": {
"type": "boolean"
},
"isEndSession": {
"type": "boolean"
},
"isExpirationUpdate": {
"type": "boolean"
},
"isProfileKeyUpdate": {
"type": "boolean"
},
"mentions": {
"items": {
"$ref": "#/definitions/receive.Mention"
@ -1544,6 +1573,14 @@
},
"type": "object"
},
"receive.GroupCallUpdate": {
"properties": {
"eraId": {
"type": "string"
}
},
"type": "object"
},
"receive.GroupInfo": {
"properties": {
"groupId": {
@ -2134,12 +2171,24 @@
"expiresInSeconds": {
"type": "integer"
},
"groupCallUpdate": {
"$ref": "#/definitions/receive.GroupCallUpdate"
},
"groupInfo": {
"$ref": "#/definitions/receive.GroupInfo"
},
"hasProfileKey": {
"type": "boolean"
},
"isEndSession": {
"type": "boolean"
},
"isExpirationUpdate": {
"type": "boolean"
},
"isProfileKeyUpdate": {
"type": "boolean"
},
"mentions": {
"items": {
"$ref": "#/definitions/receive.Mention"
@ -3367,6 +3416,53 @@
]
}
},
"/v1/groups/{number}/join": {
"post": {
"consumes": [
"application/json"
],
"description": "Join a Signal Group using an invitation link. The uri starts with https://signal.group/#",
"parameters": [
{
"description": "Registered Phone Number",
"in": "path",
"name": "number",
"required": true,
"type": "string"
},
{
"description": "Invitation link",
"in": "body",
"name": "data",
"required": true,
"schema": {
"$ref": "#/definitions/api.JoinGroupByLinkRequest"
}
}
],
"produces": [
"application/json"
],
"responses": {
"204": {
"description": "No Content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
},
"summary": "Join a Signal Group via an invitation link.",
"tags": [
"Groups"
]
}
},
"/v1/groups/{number}/{groupid}": {
"delete": {
"consumes": [

View File

@ -220,6 +220,7 @@ func main() {
groups.DELETE(":number/:groupid", api.DeleteGroup)
groups.POST(":number/:groupid/block", api.BlockGroup)
groups.POST(":number/:groupid/join", api.JoinGroup)
groups.POST(":number/join", api.JoinGroupByLink)
groups.POST(":number/:groupid/quit", api.QuitGroup)
groups.PUT(":number/:groupid", api.UpdateGroup)
groups.POST(":number/:groupid/members", api.AddMembersToGroup)