Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7487b42b | ||
|
|
ac9f68c80d | ||
|
|
43ddd23258 | ||
|
|
ccb0216484 | ||
|
|
37bf8de132 | ||
|
|
63d50b815c | ||
|
|
81be5a0f70 | ||
|
|
89c504d019 | ||
|
|
14d407eff1 | ||
|
|
d4fcfda112 | ||
|
|
ce11fd40c4 | ||
|
|
454b5f0980 |
@@ -62,6 +62,29 @@ func ChangeEnableGroupRankStatus(ctx *gin.Context) {
|
|||||||
ctx.String(http.StatusOK, "操作成功")
|
ctx.String(http.StatusOK, "操作成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangeEnableWelcomeStatus
|
||||||
|
// @description: 修改是否开启迎新
|
||||||
|
// @param ctx
|
||||||
|
func ChangeEnableWelcomeStatus(ctx *gin.Context) {
|
||||||
|
var p changeStatusParam
|
||||||
|
if err := ctx.ShouldBindJSON(&p); err != nil {
|
||||||
|
ctx.String(http.StatusBadRequest, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("待修改的群Id:%s", p.WxId)
|
||||||
|
|
||||||
|
err := client.MySQL.Model(&entity.Friend{}).
|
||||||
|
Where("wxid = ?", p.WxId).
|
||||||
|
Update("`enable_welcome`", gorm.Expr(" !`enable_welcome`")).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("修改开启迎新失败:%s", err)
|
||||||
|
ctx.String(http.StatusInternalServerError, "操作失败: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.String(http.StatusOK, "操作成功")
|
||||||
|
}
|
||||||
|
|
||||||
// ChangeSkipGroupRankStatus
|
// ChangeSkipGroupRankStatus
|
||||||
// @description: 修改是否跳过水群排行榜
|
// @description: 修改是否跳过水群排行榜
|
||||||
// @param ctx
|
// @param ctx
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"go-wechat/config"
|
"go-wechat/config"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +18,13 @@ func InitMySQLClient() {
|
|||||||
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式
|
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式
|
||||||
DontSupportRenameColumn: true, // 用 `change` 重命名列
|
DontSupportRenameColumn: true, // 用 `change` 重命名列
|
||||||
}
|
}
|
||||||
conn, err := gorm.Open(mysql.New(mysqlConfig))
|
|
||||||
|
// gorm 配置
|
||||||
|
gormConfig := gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := gorm.Open(mysql.New(mysqlConfig), &gormConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("初始化MySQL连接失败, 错误信息: %v", err)
|
log.Panicf("初始化MySQL连接失败, 错误信息: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
19
common/current/robot.go
Normal file
19
common/current/robot.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package current
|
||||||
|
|
||||||
|
import "go-wechat/model"
|
||||||
|
|
||||||
|
var robotInfo model.RobotUserInfo
|
||||||
|
|
||||||
|
// SetRobotInfo
|
||||||
|
// @description: 设置机器人信息
|
||||||
|
// @param info
|
||||||
|
func SetRobotInfo(info model.RobotUserInfo) {
|
||||||
|
robotInfo = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRobotInfo
|
||||||
|
// @description: 获取机器人信息
|
||||||
|
// @return model.RobotUserInfo
|
||||||
|
func GetRobotInfo() model.RobotUserInfo {
|
||||||
|
return robotInfo
|
||||||
|
}
|
||||||
10
config.yaml
10
config.yaml
@@ -6,6 +6,9 @@ wechat:
|
|||||||
autoSetCallback: false
|
autoSetCallback: false
|
||||||
# 回调IP,如果是Docker运行,本参数必填,如果Docker修改了映射,格式为 ip:port
|
# 回调IP,如果是Docker运行,本参数必填,如果Docker修改了映射,格式为 ip:port
|
||||||
callback: 10.0.0.51
|
callback: 10.0.0.51
|
||||||
|
# 转发到其他地址
|
||||||
|
forward:
|
||||||
|
- 10.0.0.247:4299
|
||||||
|
|
||||||
# 数据库
|
# 数据库
|
||||||
mysql:
|
mysql:
|
||||||
@@ -39,3 +42,10 @@ ai:
|
|||||||
baseUrl: https://sxxx
|
baseUrl: https://sxxx
|
||||||
# 人设
|
# 人设
|
||||||
personality: 你的名字叫张三,你是一个百科机器人,你的爱好是看电影,你的性格是开朗的,你的专长是讲故事,你的梦想是当一名童话故事作家。你对政治没有一点儿兴趣,也不会讨论任何与政治相关的话题,你甚至可以拒绝回答这一类话题。
|
personality: 你的名字叫张三,你是一个百科机器人,你的爱好是看电影,你的性格是开朗的,你的专长是讲故事,你的梦想是当一名童话故事作家。你对政治没有一点儿兴趣,也不会讨论任何与政治相关的话题,你甚至可以拒绝回答这一类话题。
|
||||||
|
|
||||||
|
# 资源配置
|
||||||
|
resource:
|
||||||
|
# 欢迎新成员表情包
|
||||||
|
welcomeNew:
|
||||||
|
type: emotion
|
||||||
|
path: 58e4150be2bba8f7b71974b10391f9e9
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ type Config struct {
|
|||||||
MySQL mysql `json:"mysql" yaml:"mysql"` // MySQL 配置
|
MySQL mysql `json:"mysql" yaml:"mysql"` // MySQL 配置
|
||||||
Wechat wechat `json:"wechat" yaml:"wechat"` // 微信助手
|
Wechat wechat `json:"wechat" yaml:"wechat"` // 微信助手
|
||||||
Ai ai `json:"ai" yaml:"ai"` // AI配置
|
Ai ai `json:"ai" yaml:"ai"` // AI配置
|
||||||
|
Resource map[string]resourceItem `json:"resource" yaml:"resource"` // 资源配置
|
||||||
}
|
}
|
||||||
|
|||||||
8
config/resource.go
Normal file
8
config/resource.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// resourceItem
|
||||||
|
// @description: 资源项
|
||||||
|
type resourceItem struct {
|
||||||
|
Type string `json:"type" yaml:"type"` // 类型
|
||||||
|
Path string `json:"path" yaml:"path"` // 路径
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ type wechat struct {
|
|||||||
Host string `json:"host" yaml:"host"` // 接口地址
|
Host string `json:"host" yaml:"host"` // 接口地址
|
||||||
AutoSetCallback bool `json:"autoSetCallback" yaml:"autoSetCallback"` // 是否自动设置回调地址
|
AutoSetCallback bool `json:"autoSetCallback" yaml:"autoSetCallback"` // 是否自动设置回调地址
|
||||||
Callback string `json:"callback" yaml:"callback"` // 回调地址
|
Callback string `json:"callback" yaml:"callback"` // 回调地址
|
||||||
|
Forward []string `json:"forward" yaml:"forward"` // 转发地址
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check
|
// Check
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Friend
|
// Friend
|
||||||
// @description: 好友列表
|
// @description: 好友列表
|
||||||
type Friend struct {
|
type Friend struct {
|
||||||
|
Wxid string `json:"wxid"` // 微信原始Id
|
||||||
CustomAccount string `json:"customAccount"` // 微信号
|
CustomAccount string `json:"customAccount"` // 微信号
|
||||||
Nickname string `json:"nickname"` // 昵称
|
Nickname string `json:"nickname"` // 昵称
|
||||||
Pinyin string `json:"pinyin"` // 昵称拼音大写首字母
|
Pinyin string `json:"pinyin"` // 昵称拼音大写首字母
|
||||||
PinyinAll string `json:"pinyinAll"` // 昵称全拼
|
PinyinAll string `json:"pinyinAll"` // 昵称全拼
|
||||||
Wxid string `json:"wxid"` // 微信原始Id
|
|
||||||
EnableAi bool `json:"enableAI" gorm:"type:tinyint(1) default 0 not null"` // 是否使用AI
|
EnableAi bool `json:"enableAI" gorm:"type:tinyint(1) default 0 not null"` // 是否使用AI
|
||||||
EnableChatRank bool `json:"enableChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否使用聊天排行
|
EnableChatRank bool `json:"enableChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否使用聊天排行
|
||||||
|
EnableWelcome bool `json:"enableWelcome" gorm:"type:tinyint(1) default 0 not null"` // 是否启用迎新
|
||||||
IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常
|
IsOk bool `json:"isOk" gorm:"type:tinyint(1) default 0 not null"` // 是否正常
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +26,12 @@ func (Friend) TableName() string {
|
|||||||
// @description: 群成员
|
// @description: 群成员
|
||||||
type GroupUser struct {
|
type GroupUser struct {
|
||||||
GroupId string `json:"groupId"` // 群Id
|
GroupId string `json:"groupId"` // 群Id
|
||||||
|
Wxid string `json:"wxid"` // 微信Id
|
||||||
Account string `json:"account"` // 账号
|
Account string `json:"account"` // 账号
|
||||||
HeadImage string `json:"headImage"` // 头像
|
HeadImage string `json:"headImage"` // 头像
|
||||||
Nickname string `json:"nickname"` // 昵称
|
Nickname string `json:"nickname"` // 昵称
|
||||||
Wxid string `json:"wxid"` // 微信Id
|
|
||||||
IsMember bool `json:"isMember" gorm:"type:tinyint(1) default 0 not null"` // 是否群成员
|
IsMember bool `json:"isMember" gorm:"type:tinyint(1) default 0 not null"` // 是否群成员
|
||||||
|
JoinTime time.Time `json:"joinTime"` // 加入时间
|
||||||
LeaveTime *time.Time `json:"leaveTime"` // 离开时间
|
LeaveTime *time.Time `json:"leaveTime"` // 离开时间
|
||||||
SkipChatRank bool `json:"skipChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否跳过聊天排行
|
SkipChatRank bool `json:"skipChatRank" gorm:"type:tinyint(1) default 0 not null"` // 是否跳过聊天排行
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"go-wechat/client"
|
||||||
"go-wechat/config"
|
"go-wechat/config"
|
||||||
"go-wechat/entity"
|
"go-wechat/entity"
|
||||||
"go-wechat/service"
|
"go-wechat/model"
|
||||||
"go-wechat/utils"
|
"go-wechat/utils"
|
||||||
"log"
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -16,26 +17,15 @@ import (
|
|||||||
// handleAtMessage
|
// handleAtMessage
|
||||||
// @description: 处理At机器人的消息
|
// @description: 处理At机器人的消息
|
||||||
// @param m
|
// @param m
|
||||||
func handleAtMessage(m entity.Message) {
|
func handleAtMessage(m model.Message) {
|
||||||
if !config.Conf.Ai.Enable {
|
if !config.Conf.Ai.Enable {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取出所有启用了AI的好友或群组
|
// 取出所有启用了AI的好友或群组
|
||||||
us, err := service.GetAllEnableAI()
|
var count int64
|
||||||
if err != nil {
|
client.MySQL.Model(&entity.Friend{}).Where("enable_ai IS TRUE").Where("wxid = ?", m.FromUser).Count(&count)
|
||||||
utils.SendMessage(m.FromUser, m.GroupUser, "#系统异常\n"+err.Error(), 0)
|
if count < 1 {
|
||||||
return
|
|
||||||
}
|
|
||||||
// 判断是否启用,如果没有启用,直接返回
|
|
||||||
var canUse bool
|
|
||||||
for _, u := range us {
|
|
||||||
if u.Wxid == m.FromUser {
|
|
||||||
canUse = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !canUse {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +53,9 @@ func handleAtMessage(m entity.Message) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 配置模型
|
// 配置模型
|
||||||
model := openai.GPT3Dot5Turbo0613
|
chatModel := openai.GPT3Dot5Turbo0613
|
||||||
if config.Conf.Ai.Model != "" {
|
if config.Conf.Ai.Model != "" {
|
||||||
model = config.Conf.Ai.Model
|
chatModel = config.Conf.Ai.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认使用AI回复
|
// 默认使用AI回复
|
||||||
@@ -77,7 +67,7 @@ func handleAtMessage(m entity.Message) {
|
|||||||
resp, err := client.CreateChatCompletion(
|
resp, err := client.CreateChatCompletion(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
openai.ChatCompletionRequest{
|
openai.ChatCompletionRequest{
|
||||||
Model: model,
|
Model: chatModel,
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"go-wechat/model"
|
"go-wechat/model"
|
||||||
"go-wechat/service"
|
"go-wechat/service"
|
||||||
"go-wechat/types"
|
"go-wechat/types"
|
||||||
|
"go-wechat/utils"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -23,43 +24,47 @@ func Parse(remoteAddr net.Addr, msg []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 提取出群成员信息
|
// 提取出群成员信息
|
||||||
groupUser := ""
|
// Sys类型的消息正文不包含微信 Id,所以不需要处理
|
||||||
msgStr := m.Content
|
if m.IsGroup() && m.Type != types.MsgTypeSys {
|
||||||
if strings.Contains(m.FromUser, "@") {
|
// 群消息,处理一下消息和发信人
|
||||||
switch m.Type {
|
groupUser := strings.Split(m.Content, "\n")[0]
|
||||||
case types.MsgTypeRecalled:
|
|
||||||
// 消息撤回
|
|
||||||
case types.MsgTypeSys:
|
|
||||||
// 系统消息
|
|
||||||
go handleSysMessage(m)
|
|
||||||
default:
|
|
||||||
// 默认消息处理
|
|
||||||
groupUser = strings.Split(m.Content, "\n")[0]
|
|
||||||
groupUser = strings.ReplaceAll(groupUser, ":", "")
|
groupUser = strings.ReplaceAll(groupUser, ":", "")
|
||||||
|
// 如果两个id一致,说明是系统发的
|
||||||
|
if m.FromUser != groupUser {
|
||||||
|
m.GroupUser = groupUser
|
||||||
|
}
|
||||||
|
// 用户的操作单独提出来处理一下
|
||||||
|
m.Content = strings.Join(strings.Split(m.Content, "\n")[1:], "\n")
|
||||||
|
}
|
||||||
|
log.Printf("%s\n消息来源: %s\n群成员: %s\n消息类型: %v\n消息内容: %s", remoteAddr, m.FromUser, m.GroupUser, m.Type, m.Content)
|
||||||
|
|
||||||
// 文字消息单独提出来处理一下
|
// 异步处理消息
|
||||||
msgStr = strings.Join(strings.Split(m.Content, "\n")[1:], "\n")
|
go func() {
|
||||||
|
if m.IsNewUserJoin() {
|
||||||
|
log.Printf("%s -> 开始迎新 -> %s", m.FromUser, m.Content)
|
||||||
|
// 欢迎新成员
|
||||||
|
go handleNewUserJoin(m)
|
||||||
|
} else if m.IsAt() {
|
||||||
|
// @机器人的消息
|
||||||
|
go handleAtMessage(m)
|
||||||
|
} else if !strings.Contains(m.FromUser, "@") && m.Type == types.MsgTypeText {
|
||||||
|
// 私聊消息处理
|
||||||
|
utils.SendMessage(m.FromUser, "", "暂未开启私聊AI", 0)
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
log.Printf("%s\n消息来源: %s\n群成员: %s\n消息类型: %v\n消息内容: %s", remoteAddr, m.FromUser, groupUser, m.Type, msgStr)
|
|
||||||
|
|
||||||
// 转换为结构体之后入库
|
// 转换为结构体之后入库
|
||||||
var ent entity.Message
|
var ent entity.Message
|
||||||
ent.MsgId = m.MsgId
|
ent.MsgId = m.MsgId
|
||||||
ent.CreateTime = m.CreateTime
|
ent.CreateTime = m.CreateTime
|
||||||
ent.CreateAt = time.Unix(int64(m.CreateTime), 0)
|
ent.CreateAt = time.Unix(int64(m.CreateTime), 0)
|
||||||
ent.Content = msgStr
|
ent.Content = m.Content
|
||||||
ent.FromUser = m.FromUser
|
ent.FromUser = m.FromUser
|
||||||
ent.GroupUser = groupUser
|
ent.GroupUser = m.GroupUser
|
||||||
ent.ToUser = m.ToUser
|
ent.ToUser = m.ToUser
|
||||||
ent.Type = m.Type
|
ent.Type = m.Type
|
||||||
ent.DisplayFullContent = m.DisplayFullContent
|
ent.DisplayFullContent = m.DisplayFullContent
|
||||||
ent.Raw = string(msg)
|
ent.Raw = string(msg)
|
||||||
|
|
||||||
// 处理At机器人的消息
|
|
||||||
if strings.HasSuffix(m.DisplayFullContent, "在群聊中@了你") {
|
|
||||||
go handleAtMessage(ent)
|
|
||||||
}
|
|
||||||
|
|
||||||
go service.SaveMessage(ent)
|
go service.SaveMessage(ent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go-wechat/client"
|
||||||
|
"go-wechat/config"
|
||||||
|
"go-wechat/entity"
|
||||||
"go-wechat/model"
|
"go-wechat/model"
|
||||||
"go-wechat/utils"
|
"go-wechat/utils"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleSysMessage
|
// handleNewUserJoin
|
||||||
// @description: 系统消息处理
|
// @description: 欢迎新成员
|
||||||
// @param m
|
// @param m
|
||||||
func handleSysMessage(m model.Message) {
|
func handleNewUserJoin(m model.Message) {
|
||||||
// 有人进群
|
// 判断是否开启迎新
|
||||||
if strings.Contains(m.Content, "\"邀请\"") && strings.Contains(m.Content, "\"加入了群聊") {
|
var count int64
|
||||||
// 发一张图乐呵乐呵
|
client.MySQL.Model(&entity.Friend{}).Where("enable_welcome IS TRUE").Where("wxid = ?", m.FromUser).Count(&count)
|
||||||
// 自己欢迎自己图片地址 D:\Share\emoticon\welcome-yourself.gif
|
if count < 1 {
|
||||||
utils.SendImage(m.FromUser, "D:\\Share\\emoticon\\welcome-yourself.gif", 0)
|
return
|
||||||
|
}
|
||||||
|
if count < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取欢迎新成员配置
|
||||||
|
conf, ok := config.Conf.Resource["welcomeNew"]
|
||||||
|
if !ok {
|
||||||
|
// 未配置,跳过
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch conf.Type {
|
||||||
|
case "text":
|
||||||
|
// 文字类型
|
||||||
|
utils.SendMessage(m.FromUser, "", conf.Path, 0)
|
||||||
|
case "image":
|
||||||
|
// 图片类型
|
||||||
|
utils.SendImage(m.FromUser, conf.Path, 0)
|
||||||
|
case "emotion":
|
||||||
|
// 表情类型
|
||||||
|
utils.SendEmotion(m.FromUser, conf.Path, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
initialization/wechat.go
Normal file
31
initialization/wechat.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package initialization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"go-wechat/common/current"
|
||||||
|
"go-wechat/config"
|
||||||
|
"go-wechat/model"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitWechatRobotInfo
|
||||||
|
// @description: 初始化微信机器人信息
|
||||||
|
func InitWechatRobotInfo() {
|
||||||
|
// 获取数据
|
||||||
|
var base model.Response[model.RobotUserInfo]
|
||||||
|
_, err := resty.New().R().
|
||||||
|
SetHeader("Content-Type", "application/json;chartset=utf-8").
|
||||||
|
SetResult(&base).
|
||||||
|
Post(config.Conf.Wechat.GetURL("/api/userInfo"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取机器人信息失败: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("机器人Id: %s", base.Data.WxId)
|
||||||
|
log.Printf("机器人微信号: %s", base.Data.Account)
|
||||||
|
log.Printf("机器人名称: %s", base.Data.Name)
|
||||||
|
|
||||||
|
// 设置为单例
|
||||||
|
current.SetRobotInfo(base.Data)
|
||||||
|
}
|
||||||
5
main.go
5
main.go
@@ -16,8 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
initialization.InitConfig()
|
initialization.InitConfig() // 初始化配置
|
||||||
tasks.InitTasks()
|
initialization.InitWechatRobotInfo() // 初始化机器人信息
|
||||||
|
tasks.InitTasks() // 初始化定时任务
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,18 +1,91 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "go-wechat/types"
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"go-wechat/types"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
// @description: 消息
|
// @description: 消息
|
||||||
type Message struct {
|
type Message struct {
|
||||||
MsgId int64 `json:"msgId" gorm:"primarykey"`
|
MsgId int64 `json:"msgId"`
|
||||||
CreateTime int `json:"createTime"`
|
CreateTime int `json:"createTime"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
DisplayFullContent string `json:"displayFullContent" gorm:"-"`
|
DisplayFullContent string `json:"displayFullContent"`
|
||||||
FromUser string `json:"fromUser"`
|
FromUser string `json:"fromUser"`
|
||||||
|
GroupUser string `json:"-"`
|
||||||
MsgSequence int `json:"msgSequence"`
|
MsgSequence int `json:"msgSequence"`
|
||||||
Pid int `json:"pid"`
|
Pid int `json:"pid"`
|
||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
ToUser string `json:"toUser"`
|
ToUser string `json:"toUser"`
|
||||||
Type types.MessageType `json:"type"`
|
Type types.MessageType `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// systemMsgDataXml
|
||||||
|
// @description: 微信系统消息的xml结构
|
||||||
|
type systemMsgDataXml struct {
|
||||||
|
SysMsg sysMsg `xml:"sysmsg"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// sysMsg
|
||||||
|
// @description: 消息主体
|
||||||
|
type sysMsg struct{}
|
||||||
|
|
||||||
|
func (m Message) IsGroup() bool {
|
||||||
|
return strings.HasSuffix(m.FromUser, "@chatroom")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPat
|
||||||
|
// @description: 是否是拍一拍消息
|
||||||
|
// @receiver m
|
||||||
|
// @return bool
|
||||||
|
func (m Message) IsPat() bool {
|
||||||
|
// 解析xml
|
||||||
|
var d systemMsgDataXml
|
||||||
|
if err := xml.Unmarshal([]byte(m.Content), &d); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Type == types.MsgTypeRecalled && d.Type == "pat"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRevokeMsg
|
||||||
|
// @description: 是否是撤回消息
|
||||||
|
// @receiver m
|
||||||
|
// @return bool
|
||||||
|
func (m Message) IsRevokeMsg() bool {
|
||||||
|
// 解析xml
|
||||||
|
var d systemMsgDataXml
|
||||||
|
if err := xml.Unmarshal([]byte(m.Content), &d); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Type == types.MsgTypeRecalled && d.Type == "revokemsg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNewUserJoin
|
||||||
|
// @description: 是否是新人入群
|
||||||
|
// @receiver m
|
||||||
|
// @return bool
|
||||||
|
func (m Message) IsNewUserJoin() bool {
|
||||||
|
sysFlag := m.Type == types.MsgTypeSys && strings.Contains(m.Content, "\"邀请\"") && strings.Contains(m.Content, "\"加入了群聊")
|
||||||
|
if sysFlag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 解析另一种情况
|
||||||
|
var d systemMsgDataXml
|
||||||
|
if err := xml.Unmarshal([]byte(m.Content), &d); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.Type == types.MsgTypeSys && d.Type == "delchatroommember"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAt
|
||||||
|
// @description: 是否是At机器人的消息
|
||||||
|
// @receiver m
|
||||||
|
// @return bool
|
||||||
|
func (m Message) IsAt() bool {
|
||||||
|
return strings.HasSuffix(m.DisplayFullContent, "在群聊中@了你")
|
||||||
|
}
|
||||||
|
|||||||
18
model/userinfo.go
Normal file
18
model/userinfo.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// RobotUserInfo
|
||||||
|
// @description: 机器人用户信息
|
||||||
|
type RobotUserInfo struct {
|
||||||
|
WxId string `json:"wxid"` // 微信Id
|
||||||
|
Account string `json:"account"` // 微信号
|
||||||
|
Name string `json:"name"` // 昵称
|
||||||
|
HeadImage string `json:"headImage"` // 头像
|
||||||
|
Mobile string `json:"mobile"` // 手机
|
||||||
|
Signature string `json:"signature"` // 个人签名
|
||||||
|
Country string `json:"country"` // 国家
|
||||||
|
Province string `json:"province"` // 省
|
||||||
|
City string `json:"city"` // 城市
|
||||||
|
CurrentDataPath string `json:"currentDataPath"` // 当前数据目录,登录的账号目录
|
||||||
|
DataSavePath string `json:"dataSavePath"` // 微信保存目录
|
||||||
|
DbKey string `json:"dbKey"` // 数据库的SQLCipher的加密key,可以使用该key配合decrypt.py解密数据库
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ func Init(g *gin.Engine) {
|
|||||||
// 接口
|
// 接口
|
||||||
api := g.Group("/api")
|
api := g.Group("/api")
|
||||||
api.PUT("/ai/status", app.ChangeEnableAiStatus) // 修改是否开启AI状态
|
api.PUT("/ai/status", app.ChangeEnableAiStatus) // 修改是否开启AI状态
|
||||||
|
api.PUT("/welcome/status", app.ChangeEnableWelcomeStatus) // 修改是否开启迎新状态
|
||||||
api.PUT("/grouprank/status", app.ChangeEnableGroupRankStatus) // 修改是否开启水群排行榜状态
|
api.PUT("/grouprank/status", app.ChangeEnableGroupRankStatus) // 修改是否开启水群排行榜状态
|
||||||
api.PUT("/grouprank/skip", app.ChangeSkipGroupRankStatus) // 修改是否跳过水群排行榜状态
|
api.PUT("/grouprank/skip", app.ChangeSkipGroupRankStatus) // 修改是否跳过水群排行榜状态
|
||||||
api.GET("/group/users", app.GetGroupUsers) // 获取群成员列表
|
api.GET("/group/users", app.GetGroupUsers) // 获取群成员列表
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func GetGroupUsersByGroupId(groupId string) (records []vo.GroupUserItem, err err
|
|||||||
Select("tgu.*", "MAX(tm.create_at) AS last_active_time").
|
Select("tgu.*", "MAX(tm.create_at) AS last_active_time").
|
||||||
Where("tgu.group_id = ?", groupId).
|
Where("tgu.group_id = ?", groupId).
|
||||||
Group("tgu.group_id, tgu.wxid").
|
Group("tgu.group_id, tgu.wxid").
|
||||||
Order("tgu.wxid ASC").
|
Order("tgu.join_time DESC").
|
||||||
Find(&records).Error
|
Find(&records).Error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func Sync() {
|
|||||||
Pinyin: friend.Pinyin,
|
Pinyin: friend.Pinyin,
|
||||||
PinyinAll: friend.PinyinAll,
|
PinyinAll: friend.PinyinAll,
|
||||||
Wxid: friend.Wxid,
|
Wxid: friend.Wxid,
|
||||||
|
IsOk: true,
|
||||||
}).Error
|
}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("新增好友失败: %s", err.Error())
|
log.Printf("新增好友失败: %s", err.Error())
|
||||||
@@ -154,6 +155,7 @@ func syncGroupUsers(tx *gorm.DB, gid string) {
|
|||||||
Nickname: cp.Nickname,
|
Nickname: cp.Nickname,
|
||||||
Wxid: cp.Wxid,
|
Wxid: cp.Wxid,
|
||||||
IsMember: true,
|
IsMember: true,
|
||||||
|
JoinTime: time.Now().Local(),
|
||||||
}).Error
|
}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("新增群成员失败: %s", err.Error())
|
log.Printf("新增群成员失败: %s", err.Error())
|
||||||
|
|||||||
25
tcpserver/forward.go
Normal file
25
tcpserver/forward.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package tcpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-wechat/config"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// forward
|
||||||
|
// @description: 转发消息
|
||||||
|
func forward(msg []byte) {
|
||||||
|
// 使用socket转发消息
|
||||||
|
for _, s := range config.Conf.Wechat.Forward {
|
||||||
|
conn, err := net.Dial("tcp", s)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("转发消息失败,错误信息: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = conn.Write(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("转发消息失败,错误信息: %v", err)
|
||||||
|
}
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package tcpserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"go-wechat/config"
|
||||||
"go-wechat/handler"
|
"go-wechat/handler"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -24,6 +25,11 @@ func process(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
log.Printf("[%s]数据长度: %d", conn.RemoteAddr(), buf.Len())
|
log.Printf("[%s]数据长度: %d", conn.RemoteAddr(), buf.Len())
|
||||||
go handler.Parse(conn.RemoteAddr(), buf.Bytes())
|
go handler.Parse(conn.RemoteAddr(), buf.Bytes())
|
||||||
|
|
||||||
|
// 转发到其他地方去
|
||||||
|
if len(config.Conf.Wechat.Forward) > 0 {
|
||||||
|
go forward(buf.Bytes())
|
||||||
|
}
|
||||||
// 将接受到的数据返回给客户端
|
// 将接受到的数据返回给客户端
|
||||||
if _, err := conn.Write([]byte("200 OK")); err != nil {
|
if _, err := conn.Write([]byte("200 OK")); err != nil {
|
||||||
log.Printf("[%s]返回数据失败,错误信息: %v", conn.RemoteAddr(), err)
|
log.Printf("[%s]返回数据失败,错误信息: %v", conn.RemoteAddr(), err)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "fmt"
|
|||||||
|
|
||||||
type MessageType int
|
type MessageType int
|
||||||
|
|
||||||
|
// 微信定义的消息类型
|
||||||
const (
|
const (
|
||||||
MsgTypeText MessageType = 1 // 文本消息
|
MsgTypeText MessageType = 1 // 文本消息
|
||||||
MsgTypeImage MessageType = 3 // 图片消息
|
MsgTypeImage MessageType = 3 // 图片消息
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"go-wechat/common/current"
|
||||||
"go-wechat/config"
|
"go-wechat/config"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@@ -82,3 +84,40 @@ func SendImage(toId, imgPath string, retryCount int) {
|
|||||||
}
|
}
|
||||||
log.Printf("发送图片消息结果: %s", resp.String())
|
log.Printf("发送图片消息结果: %s", resp.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendEmotion
|
||||||
|
// @description: 发送自定义表情包
|
||||||
|
// @param toId string 群或者好友Id
|
||||||
|
// @param emotionHash string 表情包hash(md5值)
|
||||||
|
// @param retryCount int 重试次数
|
||||||
|
func SendEmotion(toId, emotionHash string, retryCount int) {
|
||||||
|
if retryCount > 5 {
|
||||||
|
log.Printf("重试五次失败,停止发送")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装表情包本地地址
|
||||||
|
// 规则:机器人数据目录\FileStorage\CustomEmotion\表情包hash前两位\表情包hash
|
||||||
|
emotionPath := fmt.Sprintf("%sFileStorage\\CustomEmotion\\%s\\%s",
|
||||||
|
current.GetRobotInfo().CurrentDataPath, emotionHash[:2], emotionHash)
|
||||||
|
|
||||||
|
// 组装参数
|
||||||
|
param := map[string]any{
|
||||||
|
"wxid": toId, // 群或好友Id
|
||||||
|
"filePath": emotionPath, // 图片地址
|
||||||
|
}
|
||||||
|
pbs, _ := json.Marshal(param)
|
||||||
|
|
||||||
|
res := resty.New()
|
||||||
|
resp, err := res.R().
|
||||||
|
SetHeader("Content-Type", "application/json;chartset=utf-8").
|
||||||
|
SetBody(string(pbs)).
|
||||||
|
Post(config.Conf.Wechat.GetURL("/api/sendCustomEmotion"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("发送表情包消息失败: %s", err.Error())
|
||||||
|
// 休眠五秒后重新发送
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
SendImage(toId, emotionHash, retryCount+1)
|
||||||
|
}
|
||||||
|
log.Printf("发送表情包消息结果: %s", resp.String())
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
<th>是否在通讯录</th>
|
<th>是否在通讯录</th>
|
||||||
<th>是否启用AI</th>
|
<th>是否启用AI</th>
|
||||||
<th>是否启用水群排行榜</th>
|
<th>是否启用水群排行榜</th>
|
||||||
|
<th>是否启用迎新</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -134,6 +135,14 @@
|
|||||||
<div class="swap-off">❌已禁用</div>
|
<div class="swap-off">❌已禁用</div>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<label class="swap swap-flip {{ checkSwap .EnableWelcome }}">
|
||||||
|
<input type="checkbox" onclick="changeWelcomeEnableStatus({{.Wxid}})"/>
|
||||||
|
|
||||||
|
<div class="swap-on">✔️已启用</div>
|
||||||
|
<div class="swap-off">❌已禁用</div>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-link" onclick="getGroupUsers({{.Wxid}}, {{.Nickname}})">查看群成员</button>
|
<button class="btn btn-link" onclick="getGroupUsers({{.Wxid}}, {{.Nickname}})">查看群成员</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -165,6 +174,7 @@
|
|||||||
<th>微信Id</th>
|
<th>微信Id</th>
|
||||||
<th>昵称</th>
|
<th>昵称</th>
|
||||||
<th>是否群成员</th>
|
<th>是否群成员</th>
|
||||||
|
<th>加群时间</th>
|
||||||
<th>最后活跃时间</th>
|
<th>最后活跃时间</th>
|
||||||
<th>退群时间</th>
|
<th>退群时间</th>
|
||||||
<th>是否跳过水群排行榜</th>
|
<th>是否跳过水群排行榜</th>
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ function changeGroupRankEnableStatus(wxId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改欢迎语开启状态
|
||||||
|
function changeWelcomeEnableStatus(wxId) {
|
||||||
|
axios({
|
||||||
|
method: 'put',
|
||||||
|
url: '/api/welcome/status',
|
||||||
|
data: {
|
||||||
|
wxId: wxId
|
||||||
|
}
|
||||||
|
}).then(function (response) {
|
||||||
|
console.log(`返回结果: ${JSON.stringify(response)}`);
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.log(`错误信息: ${error}`);
|
||||||
|
alert("修改失败")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 修改群成员是否参与排行榜状态
|
// 修改群成员是否参与排行榜状态
|
||||||
function changeUserGroupRankSkipStatus(groupId, userId) {
|
function changeUserGroupRankSkipStatus(groupId, userId) {
|
||||||
console.log("修改水群排行榜开启状态: ", groupId, userId)
|
console.log("修改水群排行榜开启状态: ", groupId, userId)
|
||||||
@@ -83,42 +99,39 @@ function getGroupUsers(groupId, groupName) {
|
|||||||
for (let i = 0; i < groupUsers.length; i++) {
|
for (let i = 0; i < groupUsers.length; i++) {
|
||||||
const groupUser = groupUsers[i]
|
const groupUser = groupUsers[i]
|
||||||
|
|
||||||
var row = tbody.insertRow(i); // 插入新行
|
let row = tbody.insertRow(i); // 插入新行
|
||||||
|
|
||||||
// 微信Id
|
// 微信Id
|
||||||
var wxId = row.insertCell(0);
|
let wxId = row.insertCell(0);
|
||||||
wxId.innerHTML = groupUser.wxid;
|
wxId.innerHTML = groupUser.wxid;
|
||||||
|
|
||||||
// 昵称
|
// 昵称
|
||||||
var nickname = row.insertCell(1);
|
let nickname = row.insertCell(1);
|
||||||
nickname.innerHTML = groupUser.nickname;
|
nickname.innerHTML = groupUser.nickname;
|
||||||
|
|
||||||
// 是否群成员
|
// 是否群成员
|
||||||
var isMember = row.insertCell(2);
|
let isMember = row.insertCell(2);
|
||||||
if (groupUser.isMember) {
|
if (groupUser.isMember) {
|
||||||
isMember.innerHTML = '<div class="badge badge-info gap-2">是</div>';
|
isMember.innerHTML = '<div class="badge badge-info gap-2">是</div>';
|
||||||
} else {
|
} else {
|
||||||
isMember.innerHTML = '<div class="badge badge-error gap-2">否</div>';
|
isMember.innerHTML = '<div class="badge badge-error gap-2">否</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加群时间
|
||||||
|
let joinTime = row.insertCell(3);
|
||||||
|
joinTime.innerHTML = groupUser.joinTime;
|
||||||
|
|
||||||
// 最后活跃时间
|
// 最后活跃时间
|
||||||
var lastActiveTime = row.insertCell(3);
|
let lastActiveTime = row.insertCell(4);
|
||||||
lastActiveTime.innerHTML = groupUser.lastActiveTime;
|
lastActiveTime.innerHTML = groupUser.lastActiveTime;
|
||||||
|
|
||||||
// 退群时间
|
// 退群时间
|
||||||
var leaveTime = row.insertCell(4);
|
let leaveTime = row.insertCell(5);
|
||||||
leaveTime.innerHTML = groupUser.leaveTime;
|
leaveTime.innerHTML = groupUser.leaveTime;
|
||||||
|
|
||||||
// 是否跳过水群排行榜
|
// 是否跳过水群排行榜
|
||||||
var skipChatRank = row.insertCell(5);
|
let skipChatRank = row.insertCell(6);
|
||||||
let skipChatRankHtml = `<input type="checkbox" class="toggle toggle-error" ${groupUser.skipChatRank ? 'checked' : ''} onclick="changeUserGroupRankSkipStatus(\'${groupId}\', \'${groupUser.wxid}\')" />`
|
skipChatRank.innerHTML = `<input type="checkbox" class="toggle toggle-error" ${groupUser.skipChatRank ? 'checked' : ''} onclick="changeUserGroupRankSkipStatus(\'${groupId}\', \'${groupUser.wxid}\')" />`;
|
||||||
skipChatRank.innerHTML = skipChatRankHtml;
|
|
||||||
|
|
||||||
// if (groupUser.skipChatRank) {
|
|
||||||
//
|
|
||||||
// } else {
|
|
||||||
// skipChatRank.innerHTML = '<input type="checkbox" class="toggle toggle-error" />';
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
console.log(`错误信息: ${error}`);
|
console.log(`错误信息: ${error}`);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type FriendItem struct {
|
|||||||
Wxid string // 微信原始Id
|
Wxid string // 微信原始Id
|
||||||
EnableAi bool // 是否使用AI
|
EnableAi bool // 是否使用AI
|
||||||
EnableChatRank bool // 是否使用聊天排行
|
EnableChatRank bool // 是否使用聊天排行
|
||||||
|
EnableWelcome bool // 是否使用迎新
|
||||||
IsOk bool // 是否还在通讯库(群聊是要还在群里也算)
|
IsOk bool // 是否还在通讯库(群聊是要还在群里也算)
|
||||||
LastActiveTime types.DateTime // 最后活跃时间
|
LastActiveTime types.DateTime // 最后活跃时间
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ type GroupUserItem struct {
|
|||||||
HeadImage string `json:"headImage"` // 头像
|
HeadImage string `json:"headImage"` // 头像
|
||||||
Nickname string `json:"nickname"` // 昵称
|
Nickname string `json:"nickname"` // 昵称
|
||||||
IsMember bool `json:"isMember" ` // 是否群成员
|
IsMember bool `json:"isMember" ` // 是否群成员
|
||||||
|
JoinTime types.DateTime `json:"joinTime"` // 加入时间
|
||||||
LastActiveTime types.DateTime `json:"lastActiveTime"` // 最后活跃时间
|
LastActiveTime types.DateTime `json:"lastActiveTime"` // 最后活跃时间
|
||||||
LeaveTime types.DateTime `json:"leaveTime"` // 离开时间
|
LeaveTime types.DateTime `json:"leaveTime"` // 离开时间
|
||||||
SkipChatRank bool `json:"skipChatRank" ` // 是否跳过聊天排行
|
SkipChatRank bool `json:"skipChatRank" ` // 是否跳过聊天排行
|
||||||
|
|||||||
Reference in New Issue
Block a user