Skip to content

Commit 423d215

Browse files
committed
feat(channels): reach an agent from Telegram and WeChat
1 parent 40ab857 commit 423d215

24 files changed

Lines changed: 2328 additions & 0 deletions

controllers/im_channel.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2026 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controllers
16+
17+
import (
18+
"encoding/json"
19+
20+
"github.com/apache/casbin-gateway/imbridge"
21+
"github.com/apache/casbin-gateway/object"
22+
)
23+
24+
// imChannelView is one stored channel and what its listener is doing.
25+
type imChannelView struct {
26+
*object.ImChannel
27+
Status imbridge.Status `json:"status"`
28+
}
29+
30+
// GetImChannels lists the chat platforms Gateway listens on. Every credential is
31+
// masked: a token stored here never goes back to the browser.
32+
func (c *ApiController) GetImChannels() {
33+
if c.RequireAdmin() {
34+
return
35+
}
36+
37+
channels, err := object.GetImChannels()
38+
if err != nil {
39+
c.ResponseError(err.Error())
40+
return
41+
}
42+
43+
statuses := map[string]imbridge.Status{}
44+
for _, status := range imbridge.Statuses() {
45+
statuses[status.Name] = status
46+
}
47+
48+
result := []*imChannelView{}
49+
for _, channel := range channels {
50+
result = append(result, &imChannelView{ImChannel: channel.Masked(), Status: statuses[channel.Name]})
51+
}
52+
c.ResponseOk(result)
53+
}
54+
55+
// UpdateImChannel writes one channel and starts or stops its listener to match.
56+
func (c *ApiController) UpdateImChannel() {
57+
if c.RequireAdmin() {
58+
return
59+
}
60+
61+
channel := object.ImChannel{}
62+
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &channel); err != nil {
63+
c.ResponseError(err.Error())
64+
return
65+
}
66+
if err := object.SaveImChannel(&channel); err != nil {
67+
c.ResponseError(err.Error())
68+
return
69+
}
70+
c.ResponseOk(channel.Masked())
71+
}
72+
73+
func (c *ApiController) DeleteImChannel() {
74+
if c.RequireAdmin() {
75+
return
76+
}
77+
78+
var request struct {
79+
Name string `json:"name"`
80+
}
81+
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &request); err != nil {
82+
c.ResponseError(err.Error())
83+
return
84+
}
85+
if err := object.DeleteImChannel(request.Name); err != nil {
86+
c.ResponseError(err.Error())
87+
return
88+
}
89+
c.ResponseOk()
90+
}
91+
92+
// StartWeixinLogin asks WeChat for a code to be scanned by the account that will
93+
// carry the bot.
94+
func (c *ApiController) StartWeixinLogin() {
95+
if c.RequireAdmin() {
96+
return
97+
}
98+
99+
qrcode, err := imbridge.StartWeixinLogin(c.Ctx.Request.Context())
100+
if err != nil {
101+
c.ResponseError(err.Error())
102+
return
103+
}
104+
c.ResponseOk(qrcode)
105+
}
106+
107+
// GetWeixinLoginStatus reports whether the code has been scanned. The token that
108+
// comes back is written straight into the named channel rather than handed to
109+
// the browser: it is the credential, and it has no business making that trip.
110+
func (c *ApiController) GetWeixinLoginStatus() {
111+
if c.RequireAdmin() {
112+
return
113+
}
114+
115+
status, err := imbridge.PollWeixinLogin(c.Ctx.Request.Context(), c.Input().Get("qrcode"))
116+
if err != nil {
117+
c.ResponseError(err.Error())
118+
return
119+
}
120+
121+
if status.Token != "" {
122+
channel, err := object.GetImChannel(c.Input().Get("channel"))
123+
if err != nil {
124+
c.ResponseError(err.Error())
125+
return
126+
}
127+
if channel == nil {
128+
c.ResponseError("no channel is waiting for this sign-in")
129+
return
130+
}
131+
channel.Token = status.Token
132+
if err := object.SaveImChannel(channel); err != nil {
133+
c.ResponseError(err.Error())
134+
return
135+
}
136+
status.Token = ""
137+
}
138+
c.ResponseOk(status)
139+
}

imbridge/bridge.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2026 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package imbridge carries a conversation between a chat platform and an agent
16+
// on this machine. The platforms differ only in how a message arrives and how a
17+
// reply goes back; everything between - which session a chat is bound to, what a
18+
// slash command means, how an answer is written out - is the same for all of
19+
// them and lives here.
20+
package imbridge
21+
22+
import "context"
23+
24+
// Message is one thing somebody said to the bot.
25+
type Message struct {
26+
Platform string
27+
// Channel is the stored channel this arrived on, which is what binds the
28+
// conversation to an agent and a working directory.
29+
Channel string
30+
// ChatId is where a reply goes.
31+
ChatId string
32+
UserId string
33+
UserName string
34+
Text string
35+
// ReplyToken is what a platform demands back on the reply for it to land in
36+
// the right conversation. WeChat's context_token is one; Telegram needs none.
37+
ReplyToken string
38+
}
39+
40+
// Reply is one thing the bot says. An empty Edit posts a new message; otherwise
41+
// it replaces the one already posted under that id.
42+
type Reply struct {
43+
Text string
44+
Edit string
45+
}
46+
47+
// Platform is one chat service. An implementation owns its own connection and
48+
// its own idea of an id, and nothing else about it reaches the rest of Gateway.
49+
type Platform interface {
50+
Name() string
51+
// Receive delivers messages to handle until ctx is done. It is expected to
52+
// keep going through the ordinary network failures, and to return only when
53+
// the channel is stopped or the credential is refused.
54+
Receive(ctx context.Context, handle func(Message)) error
55+
// Send posts a reply and reports the id of the message it created, empty on
56+
// a platform that has no way to name one.
57+
Send(message Message, reply Reply) (string, error)
58+
// Typing shows that the agent is working, where the platform has a way to.
59+
Typing(message Message)
60+
// CanEdit reports whether Send can replace a message it already posted,
61+
// which is what decides between an answer that grows in place and one that
62+
// arrives when it is finished.
63+
CanEdit() bool
64+
}

0 commit comments

Comments
 (0)