feat(channel): 为 Telegram 和微信企业号添加代理支持
Publish Docker image (amd64) / Push Docker image to multiple registries (push) Has been cancelled
Publish Docker image (arm64) / Push Docker image to multiple registries (push) Has been cancelled
Linux Release / release (push) Has been cancelled
macOS Release / release (push) Has been cancelled
Windows Release / release (push) Has been cancelled
Publish Docker image (amd64) / Push Docker image to multiple registries (push) Has been cancelled
Publish Docker image (arm64) / Push Docker image to multiple registries (push) Has been cancelled
Linux Release / release (push) Has been cancelled
macOS Release / release (push) Has been cancelled
Windows Release / release (push) Has been cancelled
新增 HTTP 客户端工厂函数以支持通过代理发送请求,代理地址可在频道配置中设置。同时修复了微信企业号 TokenStore 更新时代理地址未同步的问题,并确保 Token 存储键值包含代理信息以避免冲突。
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newHTTPClient(proxyAddress string, timeout time.Duration) (*http.Client, error) {
|
||||||
|
transport, err := newHTTPTransport(proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: transport,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPTransport(proxyAddress string) (*http.Transport, error) {
|
||||||
|
base, ok := http.DefaultTransport.(*http.Transport)
|
||||||
|
if !ok {
|
||||||
|
return &http.Transport{}, nil
|
||||||
|
}
|
||||||
|
transport := base.Clone()
|
||||||
|
proxyAddress = strings.TrimSpace(proxyAddress)
|
||||||
|
if proxyAddress == "" {
|
||||||
|
transport.Proxy = nil
|
||||||
|
return transport, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(proxyAddress, "://") {
|
||||||
|
proxyAddress = "http://" + proxyAddress
|
||||||
|
}
|
||||||
|
u, err := url.Parse(proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "http", "https":
|
||||||
|
transport.Proxy = http.ProxyURL(u)
|
||||||
|
return transport, nil
|
||||||
|
case "socks5", "socks5h":
|
||||||
|
normalized := *u
|
||||||
|
normalized.Scheme = "socks5"
|
||||||
|
dialer, err := proxy.FromURL(&normalized, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport.Proxy = nil
|
||||||
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.Dial(network, addr)
|
||||||
|
}
|
||||||
|
return transport, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported proxy scheme: " + strings.TrimSpace(u.Scheme))
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -6,7 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"message-pusher/model"
|
"message-pusher/model"
|
||||||
"net/http"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +25,10 @@ type telegramMessageResponse struct {
|
|||||||
|
|
||||||
func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error {
|
func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error {
|
||||||
// https://core.telegram.org/bots/api#sendmessage
|
// https://core.telegram.org/bots/api#sendmessage
|
||||||
|
client, err := newHTTPClient(channel_.URL, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
messageRequest := telegramMessageRequest{
|
messageRequest := telegramMessageRequest{
|
||||||
ChatId: channel_.AccountId,
|
ChatId: channel_.AccountId,
|
||||||
}
|
}
|
||||||
@@ -53,13 +57,14 @@ func SendTelegramMessage(message *model.Message, user *model.User, channel_ *mod
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", channel_.Secret), "application/json",
|
resp, err := client.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", channel_.Secret), "application/json",
|
||||||
bytes.NewBuffer(jsonData))
|
bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var res telegramMessageResponse
|
var res telegramMessageResponse
|
||||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
_ = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func channel2item(channel_ *model.Channel) TokenStoreItem {
|
|||||||
CorpId: corpId,
|
CorpId: corpId,
|
||||||
AgentSecret: channel_.Secret,
|
AgentSecret: channel_.Secret,
|
||||||
AgentId: agentId,
|
AgentId: agentId,
|
||||||
|
Proxy: channel_.URL,
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
case model.TypeLarkApp:
|
case model.TypeLarkApp:
|
||||||
@@ -227,12 +228,13 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
|
|||||||
CorpId: corpId,
|
CorpId: corpId,
|
||||||
AgentSecret: oldChannel.Secret,
|
AgentSecret: oldChannel.Secret,
|
||||||
AgentId: agentId,
|
AgentId: agentId,
|
||||||
|
Proxy: oldChannel.URL,
|
||||||
}
|
}
|
||||||
// Yeah, it's a deep copy.
|
// Yeah, it's a deep copy.
|
||||||
newItem := oldItem
|
newItem := oldItem
|
||||||
// This means the user updated those fields.
|
// This means the user updated those fields.
|
||||||
if newChannel.AppId != "" {
|
if newChannel.AppId != "" {
|
||||||
corpId, agentId, err := parseWechatCorpAccountAppId(oldChannel.AppId)
|
corpId, agentId, err := parseWechatCorpAccountAppId(newChannel.AppId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError(err.Error())
|
common.SysError(err.Error())
|
||||||
return
|
return
|
||||||
@@ -243,6 +245,9 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
|
|||||||
if newChannel.Secret != "" {
|
if newChannel.Secret != "" {
|
||||||
newItem.AgentSecret = newChannel.Secret
|
newItem.AgentSecret = newChannel.Secret
|
||||||
}
|
}
|
||||||
|
if newChannel.URL != oldChannel.URL {
|
||||||
|
newItem.Proxy = newChannel.URL
|
||||||
|
}
|
||||||
if !oldItem.IsShared() {
|
if !oldItem.IsShared() {
|
||||||
TokenStoreRemoveItem(&oldItem)
|
TokenStoreRemoveItem(&oldItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,18 +23,19 @@ type WeChatCorpAccountTokenStoreItem struct {
|
|||||||
CorpId string
|
CorpId string
|
||||||
AgentSecret string
|
AgentSecret string
|
||||||
AgentId string
|
AgentId string
|
||||||
|
Proxy string
|
||||||
AccessToken string
|
AccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *WeChatCorpAccountTokenStoreItem) Key() string {
|
func (i *WeChatCorpAccountTokenStoreItem) Key() string {
|
||||||
return i.CorpId + i.AgentId + i.AgentSecret
|
return i.CorpId + i.AgentId + i.AgentSecret + strings.TrimSpace(i.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *WeChatCorpAccountTokenStoreItem) IsShared() bool {
|
func (i *WeChatCorpAccountTokenStoreItem) IsShared() bool {
|
||||||
appId := fmt.Sprintf("%s|%s", i.CorpId, i.AgentId)
|
appId := fmt.Sprintf("%s|%s", i.CorpId, i.AgentId)
|
||||||
var count int64 = 0
|
var count int64 = 0
|
||||||
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ?",
|
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ? and url = ?",
|
||||||
i.AgentSecret, appId, model.TypeWeChatCorpAccount).Count(&count)
|
i.AgentSecret, appId, model.TypeWeChatCorpAccount, strings.TrimSpace(i.Proxy)).Count(&count)
|
||||||
return count > 1
|
return count > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +49,10 @@ func (i *WeChatCorpAccountTokenStoreItem) Token() string {
|
|||||||
|
|
||||||
func (i *WeChatCorpAccountTokenStoreItem) Refresh() {
|
func (i *WeChatCorpAccountTokenStoreItem) Refresh() {
|
||||||
// https://work.weixin.qq.com/api/doc/90000/90135/91039
|
// https://work.weixin.qq.com/api/doc/90000/90135/91039
|
||||||
client := http.Client{
|
client, err := newHTTPClient(i.Proxy, 5*time.Second)
|
||||||
Timeout: 5 * time.Second,
|
if err != nil {
|
||||||
|
common.SysError("failed to create http client: " + err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
|
||||||
i.CorpId, i.AgentSecret), nil)
|
i.CorpId, i.AgentSecret), nil)
|
||||||
@@ -148,13 +151,24 @@ func SendWeChatCorpMessage(message *model.Message, user *model.User, channel_ *m
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
key := fmt.Sprintf("%s%s%s", corpId, agentId, agentSecret)
|
proxyAddress := channel_.URL
|
||||||
accessToken := TokenStoreGetToken(key)
|
tokenItem := WeChatCorpAccountTokenStoreItem{
|
||||||
resp, err := http.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json",
|
CorpId: corpId,
|
||||||
|
AgentSecret: agentSecret,
|
||||||
|
AgentId: agentId,
|
||||||
|
Proxy: proxyAddress,
|
||||||
|
}
|
||||||
|
accessToken := TokenStoreGetToken(tokenItem.Key())
|
||||||
|
client, err := newHTTPClient(proxyAddress, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := client.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json",
|
||||||
bytes.NewBuffer(jsonData))
|
bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
var res wechatCorpMessageResponse
|
var res wechatCorpMessageResponse
|
||||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -337,6 +337,14 @@ const EditChannel = () => {
|
|||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='代理地址(可选)'
|
||||||
|
name='url'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.url}
|
||||||
|
placeholder='http://127.0.0.1:7890 或 socks5://127.0.0.1:7890'
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -520,6 +528,16 @@ const EditChannel = () => {
|
|||||||
placeholder='在此设置 Telegram 会话 ID'
|
placeholder='在此设置 Telegram 会话 ID'
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
<Form.Group widths={1}>
|
||||||
|
<Form.Input
|
||||||
|
label='代理地址(可选)'
|
||||||
|
name='url'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.url}
|
||||||
|
placeholder='http://127.0.0.1:7890 或 socks5://127.0.0.1:7890'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
<Button onClick={getTelegramChatId} loading={loading}>
|
<Button onClick={getTelegramChatId} loading={loading}>
|
||||||
获取会话 ID
|
获取会话 ID
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user