diff --git a/internal/docker/robot.go b/internal/docker/robot.go index 61c61b1..91487fe 100644 --- a/internal/docker/robot.go +++ b/internal/docker/robot.go @@ -384,3 +384,56 @@ func GetWechatMessages(ctx context.Context, containerHost, robotWxId string) (ms msg = response.Data.AddMsgs return } + +// GetContactList +// @description: 获取联系人微信Id +// @param ctx +// @param containerHost +// @param robotWxId +// @return list +// @return err +func GetContactList(ctx context.Context, containerHost, robotWxId string) (list []string, err error) { + client := newHTTPClient() + url := fmt.Sprintf("http://%s/GetContractList", containerHost) + + var response BaseResponse[ContactListResponse] + _, err = client.R(). + SetContext(ctx). + SetBody(map[string]any{ + "CurrentChatroomContactSeq": 0, + "CurrentWxcontactSeq": 0, + "Wxid": robotWxId, + }). + SetResult(&response). + Post(url) + + list = response.Data.ContactUsernameList + return +} + +// GetContactDetail +// @description: 获取联系人详细信息 +// @param ctx +// @param containerHost +// @param robotWxId +// @param wxId +// @return info +// @return err +func GetContactDetail(ctx context.Context, containerHost, robotWxId string, wxId []string) (info []ContactDetailInfoItem, err error) { + client := newHTTPClient() + url := fmt.Sprintf("http://%s/GetContact", containerHost) + + var response BaseResponse[ContactDetailInfoResponse] + _, err = client.R(). + SetContext(ctx). + SetBody(map[string]any{ + "Chatroom": "", + "RequestWxids": strings.Join(wxId, ","), + "Wxid": robotWxId, + }). + SetResult(&response). + Post(url) + + info = response.Data.ContactList + return +} diff --git a/internal/docker/robot_model.go b/internal/docker/robot_model.go index 5ee6e68..d887530 100644 --- a/internal/docker/robot_model.go +++ b/internal/docker/robot_model.go @@ -103,3 +103,116 @@ type Message struct { MsgSeq int `json:"MsgSeq"` PushContent string `json:"PushContent,omitempty"` } + +// =================================================================== + +type ContactListResponse struct { + BaseResponse struct { + Ret int `json:"ret"` + ErrMsg struct { + String string `json:"string"` + } `json:"errMsg"` + } `json:"BaseResponse"` + CurrentWxcontactSeq int `json:"CurrentWxcontactSeq"` + CurrentChatRoomContactSeq int `json:"CurrentChatRoomContactSeq"` + CountinueFlag int `json:"CountinueFlag"` + ContactUsernameList []string `json:"ContactUsernameList"` // 联系人微信Id列表 +} + +// ContactDetailInfo +// @description: 联系人详情 +type ContactDetailInfoResponse struct { + BaseResponse struct { + Ret int `json:"ret"` + ErrMsg struct { + } `json:"errMsg"` + } `json:"BaseResponse"` + ContactCount int `json:"ContactCount"` + ContactList []ContactDetailInfoItem `json:"ContactList"` + Ret []int `json:"Ret"` + Ticket []struct { + } `json:"Ticket"` +} + +// ContactDetailInfoItem +// @description: 详情 +type ContactDetailInfoItem struct { + UserName struct { + String string `json:"string"` + } `json:"UserName"` // 微信Id + NickName struct { + String string `json:"string"` + } `json:"NickName"` // 昵称 + Pyinitial struct { + String string `json:"string"` + } `json:"Pyinitial"` // 昵称拼音首字母大写 + QuanPin struct { + String string `json:"string"` + } `json:"QuanPin"` // 昵称拼音全拼小写 + Sex int `json:"Sex"` // 性别 0:未知 1:男 2:女 + ImgBuf struct { + ILen int `json:"iLen"` + } `json:"ImgBuf"` + BitMask int64 `json:"BitMask"` + BitVal int `json:"BitVal"` + ImgFlag int `json:"ImgFlag"` + Remark struct { + } `json:"Remark"` + RemarkPyinitial struct { + } `json:"RemarkPyinitial"` + RemarkQuanPin struct { + } `json:"RemarkQuanPin"` + ContactType int `json:"ContactType"` + RoomInfoCount int `json:"RoomInfoCount"` + DomainList struct { + } `json:"DomainList"` + ChatRoomNotify int `json:"ChatRoomNotify"` + AddContactScene int `json:"AddContactScene"` + Province string `json:"Province"` // 省份 + City string `json:"City"` // 城市 + Signature string `json:"Signature"` // 个性签名 + PersonalCard int `json:"PersonalCard"` + HasWeiXinHdHeadImg int `json:"HasWeiXinHdHeadImg"` + VerifyFlag int `json:"VerifyFlag"` + Level int `json:"Level"` + Source int `json:"Source"` + Alias string `json:"Alias"` // 微信号 + WeiboFlag int `json:"WeiboFlag"` + AlbumStyle int `json:"AlbumStyle"` + AlbumFlag int `json:"AlbumFlag"` + SnsUserInfo struct { + SnsFlag int `json:"SnsFlag"` + SnsBgimgId string `json:"SnsBgimgId"` // 朋友圈背景图 + SnsBgobjectId float64 `json:"SnsBgobjectId"` + SnsFlagEx int `json:"SnsFlagEx"` + } `json:"SnsUserInfo"` + Country string `json:"Country"` // 国家 + BigHeadImgUrl string `json:"BigHeadImgUrl"` // 大头像地址 + SmallHeadImgUrl string `json:"SmallHeadImgUrl"` // 小头像地址 + MyBrandList string `json:"MyBrandList"` + CustomizedInfo struct { + BrandFlag int `json:"BrandFlag"` + } `json:"CustomizedInfo"` + HeadImgMd5 string `json:"HeadImgMd5"` + EncryptUserName string `json:"EncryptUserName"` + AdditionalContactList struct { + LinkedinContactItem struct { + } `json:"LinkedinContactItem"` + } `json:"AdditionalContactList"` + ChatroomVersion int `json:"ChatroomVersion"` + ChatroomMaxCount int `json:"ChatroomMaxCount"` + ChatroomAccessType int `json:"ChatroomAccessType"` + NewChatroomData struct { + MemberCount int `json:"MemberCount"` + InfoMask int `json:"InfoMask"` + } `json:"NewChatroomData"` + DeleteFlag int `json:"DeleteFlag"` + LabelIdlist string `json:"LabelIdlist"` + PhoneNumListInfo struct { + Count int `json:"Count"` + } `json:"PhoneNumListInfo"` + ChatroomInfoVersion int `json:"ChatroomInfoVersion"` + DeleteContactScene int `json:"DeleteContactScene"` + ChatroomStatus int `json:"ChatroomStatus"` + ExtFlag int `json:"ExtFlag"` +} diff --git a/internal/model/contact.go b/internal/model/contact.go index 37f1f9d..6c7218d 100644 --- a/internal/model/contact.go +++ b/internal/model/contact.go @@ -11,12 +11,21 @@ const ( // Contact 表示微信联系人,包括好友和群组 type Contact struct { BaseModel - RobotID uint `gorm:"column:robot_id;index" json:"robot_id"` - WechatID string `gorm:"column:wechat_id;index:idx_contact_wechat_id,length:64" json:"wechat_id"` // 添加索引长度 - Nickname string `gorm:"column:nickname" json:"nickname"` - Avatar string `gorm:"column:avatar" json:"avatar"` - Type ContactType `gorm:"column:type" json:"type"` - Remark string `gorm:"column:remark" json:"remark"` + RobotID uint `gorm:"column:robot_id;index:deleted,unique" json:"robot_id"` + WechatID string `gorm:"column:wechat_id;index:deleted,unique" json:"wechat_id"` // 添加索引长度 + Alias string `gorm:"column:alias" json:"alias"` // 微信号 + Nickname string `gorm:"column:nickname" json:"nickname"` + Avatar string `gorm:"column:avatar" json:"avatar"` + Type ContactType `gorm:"column:type" json:"type"` + Remark string `gorm:"column:remark" json:"remark"` + Pyinitial string `gorm:"column:pyinitial" json:"pyinitial"` // 昵称拼音首字母大写 + QuanPin string `gorm:"column:quan_pin" json:"quan_pin"` // 昵称拼音全拼小写 + Sex int `gorm:"column:sex" json:"sex"` // 性别 0:未知 1:男 2:女 + Country string `gorm:"column:country" json:"country"` // 国家 + Province string `gorm:"column:province" json:"province"` // 省份 + City string `gorm:"column:city" json:"city"` // 城市 + Signature string `gorm:"column:signature" json:"signature"` // 个性签名 + SnsBackground string `gorm:"column:sns_background" json:"sns_background"` // 朋友圈背景图 } // TableName 指定表名 diff --git a/internal/tasks/contract.go b/internal/tasks/contract.go new file mode 100644 index 0000000..1f03c2b --- /dev/null +++ b/internal/tasks/contract.go @@ -0,0 +1,115 @@ +package tasks + +import ( + "context" + "gitee.ltd/lxh/wechat-robot/internal/docker" + "gitee.ltd/lxh/wechat-robot/internal/model" + "github.com/gofiber/fiber/v2/log" + "slices" + "strings" + "time" +) + +// syncContact +// @description: 同步联系人 +// @param containerHost 容器接口地址 +// @param robotWxId 机器人微信号 +// @param robotId 机器人ID +func syncContact(containerHost, robotWxId string, robotId uint) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // 先获取全部id + ids, err := docker.GetContactList(ctx, containerHost, robotWxId) + if err != nil { + // 处理错误 + log.Errorf("[%s]获取联系人列表失败: %v", robotWxId, err) + return + } + // 过滤掉特殊微信Id + var specialId = []string{"filehelper", "newsapp", "fmessage", "weibo", "qqmail", "tmessage", "qmessage", "qqsync", + "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "readerapp", "blogapp", "facebookapp", "masssendapp", + "meishiapp", "feedsapp", "voip", "blogappweixin", "weixin", "brandsessionholder", "weixinreminder", "officialaccounts", + "notification_messages", "wxitil", "userexperience_alarm", "notification_messages", "exmail_tool", "mphelper"} + ids = slices.DeleteFunc(ids, func(id string) bool { + return slices.Contains(specialId, id) || strings.HasPrefix(id, "gh_") || strings.TrimSpace(id) == "" + }) + + // 获取昵称等详细信息 + contacts, err := docker.GetContactDetail(ctx, containerHost, robotWxId, ids) + if err != nil { + // 处理错误 + log.Errorf("[%s]获取联系人详情失败: %v", robotWxId, err) + return + } + + // 循环联系人信息,打印一下 + db := model.GetDB() + nowIds := make([]string, 0) + for _, contact := range contacts { + //log.Infof("[%s]联系人信息: %+v", robotWxId, contact) + + if strings.TrimSpace(contact.UserName.String) == "" { + // 微信号为空,跳过 + continue + } + nowIds = append(nowIds, contact.UserName.String) + + // 判断数据库是否存在当前数据,不存在就新建,存在就更新 + var existId uint + db.Model(&model.Contact{}).Where("robot_id = ?", robotId).Where("wechat_id = ?", contact.UserName.String).Pluck("id", &existId) + if existId > 0 { + // 存在,修改 + pm := map[string]any{ + "alias": contact.Alias, + "nickname": contact.NickName.String, + "avatar": contact.BigHeadImgUrl, + "pyinitial": contact.Pyinitial.String, + "quan_pin": contact.QuanPin.String, + "sex": contact.Sex, + "country": contact.Country, + "province": contact.Province, + "city": contact.City, + "signature": contact.Signature, + "sns_background": contact.SnsUserInfo.SnsBgimgId, + } + if contact.BigHeadImgUrl == "" { + pm["avatar"] = contact.SmallHeadImgUrl + } + err = db.Model(&model.Contact{}).Where("id = ?", existId).Updates(pm).Error + } else { + // 组装联系人信息,然后存入数据库 + var c model.Contact + c.RobotID = robotId + c.WechatID = contact.UserName.String + c.Alias = contact.Alias + c.Nickname = contact.NickName.String + c.Avatar = contact.BigHeadImgUrl + if contact.BigHeadImgUrl == "" { + c.Avatar = contact.SmallHeadImgUrl + } + c.Type = "friend" + if strings.HasSuffix(contact.UserName.String, "@chatroom") { + // 群聊 + c.Type = "group" + } + c.Pyinitial = contact.Pyinitial.String + c.QuanPin = contact.QuanPin.String + c.Sex = contact.Sex + c.Country = contact.Country + c.Province = contact.Province + c.City = contact.City + c.Signature = contact.Signature + c.SnsBackground = contact.SnsUserInfo.SnsBgimgId + + err = db.Create(&c).Error + } + if err != nil { + log.Debugf("[%s]保存联系人失败: %v", robotWxId, err) + } + + } + + // 清理掉不存在的联系人 + db.Model(&model.Contact{}).Where("robot_id = ?", robotId).Where("wechat_id NOT IN ?", nowIds).Delete(&model.Contact{}) +} diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go index 7d01e91..ae1abbc 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -12,7 +12,8 @@ import ( var scheduler gocron.Scheduler // 已启动定时任务的微信机器人 -var enabledMap = sync.Map{} +var enabledSyncMessageMap = sync.Map{} +var enabledSyncContactMap = sync.Map{} // Start // @description: 启动任务 @@ -41,7 +42,18 @@ func Start() { log.Panicf("添加定时任务失败: %v", err) } // 添加到已启动的任务列表 - enabledMap.Store(robot.ID, job.ID()) + enabledSyncMessageMap.Store(robot.ID, job.ID()) + + // 添加联系人同步任务 + job, err = scheduler.NewJob( + gocron.CronJob("0 */1 * * *", true), // 每小时同步一次 + gocron.NewTask(syncContact, robot.ContainerHost, robot.WechatID, robot.ID), + ) + if err != nil { + log.Panicf("添加联系人同步任务失败: %v", err) + } + // 添加到已启动的任务列表 + enabledSyncContactMap.Store(robot.ID, job.ID()) } // 启动定时任务 @@ -61,7 +73,18 @@ func AddJob(robot model.Robot) { return } // 添加到已启动的任务列表 - enabledMap.Store(robot.ID, job.ID()) + enabledSyncMessageMap.Store(robot.ID, job.ID()) + + // 添加联系人同步任务 + job, err = scheduler.NewJob( + gocron.CronJob("0 */1 * * *", true), // 每小时同步一次 + gocron.NewTask(syncContact, robot.ContainerHost, robot.WechatID, robot.ID), + ) + if err != nil { + log.Panicf("添加联系人同步任务失败: %v", err) + } + // 添加到已启动的任务列表 + enabledSyncContactMap.Store(robot.ID, job.ID()) } // DeleteJob @@ -69,7 +92,7 @@ func AddJob(robot model.Robot) { // @param robotId func DeleteJob(robotId uint) { // 先取出任务Id - jobId, ok := enabledMap.Load(robotId) + jobId, ok := enabledSyncMessageMap.Load(robotId) if !ok { log.Printf("定时任务不存在,robotId: %d", robotId) return @@ -78,4 +101,19 @@ func DeleteJob(robotId uint) { log.Printf("删除定时任务失败: %v", err) return } + // 删除已启动的任务列表 + enabledSyncMessageMap.Delete(robotId) + + // 删除联系人同步任务 + jobId, ok = enabledSyncContactMap.Load(robotId) + if !ok { + log.Printf("联系人同步任务不存在,robotId: %d", robotId) + return + } + if err := scheduler.RemoveJob(jobId.(uuid.UUID)); err != nil { + log.Printf("删除联系人同步任务失败: %v", err) + return + } + // 删除已启动的任务列表 + enabledSyncContactMap.Delete(robotId) }