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"
|
||||
"fmt"
|
||||
"message-pusher/model"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -25,6 +25,10 @@ type telegramMessageResponse struct {
|
||||
|
||||
func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error {
|
||||
// https://core.telegram.org/bots/api#sendmessage
|
||||
client, err := newHTTPClient(channel_.URL, 10*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
messageRequest := telegramMessageRequest{
|
||||
ChatId: channel_.AccountId,
|
||||
}
|
||||
@@ -53,13 +57,14 @@ func SendTelegramMessage(message *model.Message, user *model.User, channel_ *mod
|
||||
if err != nil {
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var res telegramMessageResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ func channel2item(channel_ *model.Channel) TokenStoreItem {
|
||||
CorpId: corpId,
|
||||
AgentSecret: channel_.Secret,
|
||||
AgentId: agentId,
|
||||
Proxy: channel_.URL,
|
||||
}
|
||||
return item
|
||||
case model.TypeLarkApp:
|
||||
@@ -227,12 +228,13 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
|
||||
CorpId: corpId,
|
||||
AgentSecret: oldChannel.Secret,
|
||||
AgentId: agentId,
|
||||
Proxy: oldChannel.URL,
|
||||
}
|
||||
// Yeah, it's a deep copy.
|
||||
newItem := oldItem
|
||||
// This means the user updated those fields.
|
||||
if newChannel.AppId != "" {
|
||||
corpId, agentId, err := parseWechatCorpAccountAppId(oldChannel.AppId)
|
||||
corpId, agentId, err := parseWechatCorpAccountAppId(newChannel.AppId)
|
||||
if err != nil {
|
||||
common.SysError(err.Error())
|
||||
return
|
||||
@@ -243,6 +245,9 @@ func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channe
|
||||
if newChannel.Secret != "" {
|
||||
newItem.AgentSecret = newChannel.Secret
|
||||
}
|
||||
if newChannel.URL != oldChannel.URL {
|
||||
newItem.Proxy = newChannel.URL
|
||||
}
|
||||
if !oldItem.IsShared() {
|
||||
TokenStoreRemoveItem(&oldItem)
|
||||
}
|
||||
|
||||
@@ -23,18 +23,19 @@ type WeChatCorpAccountTokenStoreItem struct {
|
||||
CorpId string
|
||||
AgentSecret string
|
||||
AgentId string
|
||||
Proxy string
|
||||
AccessToken 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 {
|
||||
appId := fmt.Sprintf("%s|%s", i.CorpId, i.AgentId)
|
||||
var count int64 = 0
|
||||
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ?",
|
||||
i.AgentSecret, appId, model.TypeWeChatCorpAccount).Count(&count)
|
||||
model.DB.Model(&model.Channel{}).Where("secret = ? and app_id = ? and type = ? and url = ?",
|
||||
i.AgentSecret, appId, model.TypeWeChatCorpAccount, strings.TrimSpace(i.Proxy)).Count(&count)
|
||||
return count > 1
|
||||
}
|
||||
|
||||
@@ -48,8 +49,10 @@ func (i *WeChatCorpAccountTokenStoreItem) Token() string {
|
||||
|
||||
func (i *WeChatCorpAccountTokenStoreItem) Refresh() {
|
||||
// https://work.weixin.qq.com/api/doc/90000/90135/91039
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
client, err := newHTTPClient(i.Proxy, 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",
|
||||
i.CorpId, i.AgentSecret), nil)
|
||||
@@ -148,13 +151,24 @@ func SendWeChatCorpMessage(message *model.Message, user *model.User, channel_ *m
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := fmt.Sprintf("%s%s%s", corpId, agentId, agentSecret)
|
||||
accessToken := TokenStoreGetToken(key)
|
||||
resp, err := http.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json",
|
||||
proxyAddress := channel_.URL
|
||||
tokenItem := WeChatCorpAccountTokenStoreItem{
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var res wechatCorpMessageResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
|
||||
@@ -337,6 +337,14 @@ const EditChannel = () => {
|
||||
value={inputs.other}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
@@ -520,6 +528,16 @@ const EditChannel = () => {
|
||||
placeholder='在此设置 Telegram 会话 ID'
|
||||
/>
|
||||
</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}>
|
||||
获取会话 ID
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user