commit 744fb7b1d0027329a99ea6946d006e7f59a54cec Author: 李寻欢 Date: Thu Sep 21 17:33:59 2023 +0800 :tada: first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e7bd86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +vendor +logs +*.exe +*.pprof +cache +log +dist +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..01e915c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:alpine as builder +WORKDIR /builder +COPY . . + +ENV GO111MODULE=on +ENV GOPROXY=https://goproxy.cn,direct + +RUN go mod download && go build -o app +RUN ls -lh && chmod +x ./app + +FROM repo.lxh.io/alpine:3.16.0 as runner +LABEL org.opencontainers.image.authors="lxh@cxh.cn" + +EXPOSE 19099 + +WORKDIR /app +COPY --from=builder /builder/app ./app +CMD ./app \ No newline at end of file diff --git a/client/mysql.go b/client/mysql.go new file mode 100644 index 0000000..87e66ca --- /dev/null +++ b/client/mysql.go @@ -0,0 +1,28 @@ +package client + +import ( + "gorm.io/driver/mysql" + "gorm.io/gorm" + "log" +) + +// MySQL客户端 +var MySQL *gorm.DB + +func InitMySQLClient() { + dsn := "wechat:wechat123@tcp(10.0.0.31:3307)/wechat?charset=utf8mb4&parseTime=True&loc=Local" + + // 创建连接对象 + mysqlConfig := mysql.Config{ + DSN: dsn, + DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式 + DontSupportRenameColumn: true, // 用 `change` 重命名列 + } + conn, err := gorm.Open(mysql.New(mysqlConfig)) + if err != nil { + log.Panicf("初始化MySQL连接失败, 错误信息: %v", err) + } else { + log.Println("MySQL连接成功") + } + MySQL = conn +} diff --git a/constant/wxid.go b/constant/wxid.go new file mode 100644 index 0000000..a7364ef --- /dev/null +++ b/constant/wxid.go @@ -0,0 +1,6 @@ +package constant + +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"} diff --git a/entity/friend.go b/entity/friend.go new file mode 100644 index 0000000..c03ca1c --- /dev/null +++ b/entity/friend.go @@ -0,0 +1,20 @@ +package entity + +// Friend +// @description: 好友列表 +type Friend struct { + CustomAccount string `json:"customAccount"` // 微信号 + EncryptName string `json:"encryptName"` // 不知道 + Nickname string `json:"nickname"` // 昵称 + Pinyin string `json:"pinyin"` // 昵称拼音大写首字母 + PinyinAll string `json:"pinyinAll"` // 昵称全拼 + Reserved1 int `json:"reserved1"` // 未知 + Reserved2 int `json:"reserved2"` // 未知 + Type int `json:"type"` // 类型 + VerifyFlag int `json:"verifyFlag"` // 未知 + Wxid string `json:"wxid"` // 微信原始Id +} + +func (Friend) TableName() string { + return "t_friend" +} diff --git a/entity/message.go b/entity/message.go new file mode 100644 index 0000000..3f0c0cf --- /dev/null +++ b/entity/message.go @@ -0,0 +1,25 @@ +package entity + +import ( + "go-wechat/types" + "time" +) + +// Message +// @description: 消息数据库结构体 +type Message struct { + MsgId int64 `gorm:"primaryKey"` // 消息Id + CreateTime int // 发送时间戳 + CreateAt time.Time // 发送时间 + Type types.MessageType // 消息类型 + Content string // 内容 + DisplayFullContent string // 显示的完整内容 + FromUser string // 发送者 + GroupUser string // 群成员 + ToUser string // 接收者 + Raw string // 原始通知字符串 +} + +func (Message) TableName() string { + return "t_message" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08e9bc6 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module go-wechat + +go 1.21 + +require ( + github.com/go-co-op/gocron v1.34.1 + github.com/go-resty/resty/v2 v2.8.0 + gorm.io/driver/mysql v1.5.1 + gorm.io/gorm v1.25.4 +) + +require ( + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/net v0.15.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a5520b6 --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-co-op/gocron v1.34.1 h1:g4Y1ePFin2z4Rbb5gTHNYfj36moY4kU9s0ZmGy/ZddQ= +github.com/go-co-op/gocron v1.34.1/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= +github.com/go-resty/resty/v2 v2.8.0 h1:J29d0JFWwSWrDCysnOK/YjsPMLQTx0TvgJEHVGvf2L8= +github.com/go-resty/resty/v2 v2.8.0/go.mod h1:UCui0cMHekLrSntoMyofdSTaPpinlRHFtPpizuyDW2w= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/handler/parse.go b/handler/parse.go new file mode 100644 index 0000000..b6f653d --- /dev/null +++ b/handler/parse.go @@ -0,0 +1,54 @@ +package handler + +import ( + "encoding/json" + "go-wechat/entity" + "go-wechat/model" + "go-wechat/service" + "go-wechat/types" + "log" + "net" + "strings" + "time" +) + +// Parse +// @description: 解析消息 +// @param msg +func Parse(remoteAddr net.Addr, msg []byte) { + var m model.Message + if err := json.Unmarshal(msg, &m); err != nil { + log.Printf("[%s]消息解析失败: %v", remoteAddr, err) + log.Printf("[%s]消息内容: %d -> %v", remoteAddr, len(msg), string(msg)) + return + } + // 提取出群成员信息 + groupUser := "" + msgStr := m.Content + if strings.Contains(m.FromUser, "@") { + // 系统消息不单独处理 + if m.Type != types.MsgTypeRecalled && m.Type != types.MsgTypeSys { + groupUser = strings.Split(m.Content, "\n")[0] + groupUser = strings.ReplaceAll(groupUser, ":", "") + + // 文字消息单独提出来处理一下 + msgStr = strings.Join(strings.Split(m.Content, "\n")[1:], "\n") + } + } + log.Printf("%s\n消息来源: %s\n群成员: %s\n消息类型: %v\n消息内容: %s", remoteAddr, m.FromUser, groupUser, m.Type, msgStr) + + // 转换为结构体之后入库 + var ent entity.Message + ent.MsgId = m.MsgId + ent.CreateTime = m.CreateTime + ent.CreateAt = time.Unix(int64(m.CreateTime), 0) + ent.Content = msgStr + ent.FromUser = m.FromUser + ent.GroupUser = groupUser + ent.ToUser = m.ToUser + ent.Type = m.Type + ent.DisplayFullContent = m.DisplayFullContent + ent.Raw = string(msg) + + go service.SaveMessage(ent) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6239ff3 --- /dev/null +++ b/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "bytes" + "go-wechat/client" + "go-wechat/handler" + "go-wechat/tasks" + "io" + "log" + "net" +) + +func init() { + client.InitMySQLClient() + tasks.InitTasks() +} + +func process(conn net.Conn) { + // 处理完关闭连接 + defer func() { + log.Printf("处理完成: -> %s", conn.RemoteAddr()) + _ = conn.Close() + }() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, conn); err != nil { + log.Printf("[%s]返回数据失败,错误信息: %v", conn.RemoteAddr(), err) + } + log.Printf("[%s]数据长度: %d", conn.RemoteAddr(), buf.Len()) + go handler.Parse(conn.RemoteAddr(), buf.Bytes()) + // 将接受到的数据返回给客户端 + if _, err := conn.Write([]byte("200 OK")); err != nil { + log.Printf("[%s]返回数据失败,错误信息: %v", conn.RemoteAddr(), err) + } +} + +func main() { + // 建立 tcp 服务 + listen, err := net.Listen("tcp", "0.0.0.0:19099") + if err != nil { + log.Printf("listen failed, err:%v", err) + return + } + + for { + // 等待客户端建立连接 + conn, err := listen.Accept() + if err != nil { + log.Printf("accept failed, err:%v", err) + continue + } + // 启动一个单独的 goroutine 去处理连接 + go process(conn) + } +} diff --git a/model/friend.go b/model/friend.go new file mode 100644 index 0000000..34caa18 --- /dev/null +++ b/model/friend.go @@ -0,0 +1,36 @@ +package model + +// FriendItem +// @description: 好友列表数据 +type FriendItem struct { + CustomAccount string `json:"customAccount"` // 微信号 + EncryptName string `json:"encryptName"` // 不知道 + Nickname string `json:"nickname"` // 昵称 + Pinyin string `json:"pinyin"` // 昵称拼音大写首字母 + PinyinAll string `json:"pinyinAll"` // 昵称全拼 + Reserved1 int `json:"reserved1"` // 未知 + Reserved2 int `json:"reserved2"` // 未知 + Type int `json:"type"` // 类型 + VerifyFlag int `json:"verifyFlag"` // 未知 + Wxid string `json:"wxid"` // 微信原始Id +} + +// GroupUser +// @description: 群成员返回结果 +type GroupUser struct { + Admin string `json:"admin"` // 群主微信 + AdminNickname string `json:"adminNickname"` // 群主昵称 + ChatRoomId string `json:"chatRoomId"` // 群Id + MemberNickname string `json:"memberNickname"` // 成员昵称 `^G`切割 + Members string `json:"members"` // 成员Id `^G`切割 +} + +// ContactProfile +// @description: 好友资料 +type ContactProfile struct { + Account string `json:"account"` // 账号 + HeadImage string `json:"headImage"` // 头像 + Nickname string `json:"nickname"` // 昵称 + V3 string `json:"v3"` // v3 + Wxid string `json:"wxid"` // 微信Id +} diff --git a/model/message.go b/model/message.go new file mode 100644 index 0000000..cc7fee1 --- /dev/null +++ b/model/message.go @@ -0,0 +1,18 @@ +package model + +import "go-wechat/types" + +// Message +// @description: 消息 +type Message struct { + MsgId int64 `json:"msgId" gorm:"primarykey"` + CreateTime int `json:"createTime"` + Content string `json:"content"` + DisplayFullContent string `json:"displayFullContent" gorm:"-"` + FromUser string `json:"fromUser"` + MsgSequence int `json:"msgSequence"` + Pid int `json:"pid"` + Signature string `json:"signature"` + ToUser string `json:"toUser"` + Type types.MessageType `json:"type"` +} diff --git a/model/response.go b/model/response.go new file mode 100644 index 0000000..4bf376e --- /dev/null +++ b/model/response.go @@ -0,0 +1,9 @@ +package model + +// Response +// @description: 基础返回结构体 +type Response[T any] struct { + Code int `json:"code"` // 状态码 + Data T `json:"data"` // 数据 + Msg string `json:"msg"` // 消息 +} diff --git a/service/message.go b/service/message.go new file mode 100644 index 0000000..245a2ec --- /dev/null +++ b/service/message.go @@ -0,0 +1,28 @@ +package service + +import ( + "go-wechat/client" + "go-wechat/entity" + "log" +) + +// SaveMessage +// @description: 消息入库 +// @param msg +func SaveMessage(msg entity.Message) { + // 检查消息是否存在,存在就跳过 + var count int64 + err := client.MySQL.Model(&entity.Message{}).Where("msg_id = ?", msg.MsgId).Count(&count).Error + if err != nil { + log.Printf("检查消息是否存在失败, 错误信息: %v", err) + return + } + if count > 0 { + return + } + err = client.MySQL.Create(&msg).Error + if err != nil { + log.Printf("消息入库失败, 错误信息: %v", err) + } + log.Printf("消息入库成功,消息Id: %d", msg.MsgId) +} diff --git a/tasks/friends.go b/tasks/friends.go new file mode 100644 index 0000000..aa432c1 --- /dev/null +++ b/tasks/friends.go @@ -0,0 +1,111 @@ +package tasks + +import ( + "encoding/json" + "github.com/go-resty/resty/v2" + "go-wechat/constant" + "go-wechat/entity" + "go-wechat/model" + "log" + "slices" + "strings" +) + +// 同步群成员 + +// syncFriends +// @description: 同步好友列表 +func syncFriends() { + var base model.Response[[]entity.Friend] + + client := resty.New() + resp, err := client.R(). + SetHeader("Content-Type", "application/json;chartset=utf-8"). + SetResult(&base). + Post("http://10.0.0.73:19088/api/getContactList") + if err != nil { + log.Printf("获取好友列表失败: %s", err.Error()) + return + } + log.Printf("获取好友列表结果: %s", resp.String()) + for _, friend := range base.Data { + if strings.Contains(friend.Wxid, "gh_") || strings.Contains(friend.Wxid, "@openim") { + continue + } + // 特殊Id跳过 + if slices.Contains(constant.SpecialId, friend.Wxid) { + continue + } + log.Printf("昵称: %s -> 类型: %d -> 微信号: %s -> 微信原始Id: %s", friend.Nickname, friend.Type, friend.CustomAccount, friend.Wxid) + + // 群成员,同步一下成员信息 + if strings.Contains(friend.Wxid, "@chatroom") { + syncGroupUsers(friend.Wxid) + } + + } +} + +// syncGroupUsers +// @description: 同步群成员 +// @param gid +func syncGroupUsers(gid string) { + var baseResp model.Response[model.GroupUser] + + // 组装参数 + param := map[string]any{ + "chatRoomId": gid, // 群Id + } + pbs, _ := json.Marshal(param) + + client := resty.New() + _, err := client.R(). + SetHeader("Content-Type", "application/json;chartset=utf-8"). + SetBody(string(pbs)). + SetResult(&baseResp). + Post("http://10.0.0.73:19088/api/getMemberFromChatRoom") + if err != nil { + log.Printf("获取群成员信息失败: %s", err.Error()) + return + } + + // 昵称Id + wxIds := strings.Split(baseResp.Data.Members, "^G") + + log.Printf(" 群成员数: %d", len(wxIds)) + for _, wxid := range wxIds { + // 获取成员信息 + cp, _ := getContactProfile(wxid) + if cp.Wxid != "" { + log.Printf(" 微信Id: %s -> 昵称: %s -> 微信号: %s", wxid, cp.Nickname, cp.Account) + } + } +} + +// getContactProfile +// @description: 获取成员详情 +// @param wxid +// @return ent +// @return err +func getContactProfile(wxid string) (ent model.ContactProfile, err error) { + var baseResp model.Response[model.ContactProfile] + + // 组装参数 + param := map[string]any{ + "wxid": wxid, // 群Id + } + pbs, _ := json.Marshal(param) + + client := resty.New() + _, err = client.R(). + SetHeader("Content-Type", "application/json;chartset=utf-8"). + SetBody(string(pbs)). + SetResult(&baseResp). + Post("http://10.0.0.73:19088/api/getContactProfile") + if err != nil { + log.Printf("获取成员详情失败: %s", err.Error()) + return + } + ent = baseResp.Data + return +} diff --git a/tasks/tasks.go b/tasks/tasks.go new file mode 100644 index 0000000..4833f21 --- /dev/null +++ b/tasks/tasks.go @@ -0,0 +1,24 @@ +package tasks + +import ( + "github.com/go-co-op/gocron" + "log" + "time" +) + +// InitTasks +// @description: 初始化定时任务 +func InitTasks() { + // 定时任务发送消息 + s := gocron.NewScheduler(time.Local) + + // 每天早上九点半发送前一天的水群排行 + _, _ = s.Every(1).Day().At("09:30").Do(yesterday) + + // 每小时更新一次好友列表 + _, _ = s.Every(1).Minute().Do(syncFriends) + + // 开启定时任务 + s.StartAsync() + log.Println("定时任务初始化成功") +} diff --git a/tasks/water_group.go b/tasks/water_group.go new file mode 100644 index 0000000..eb304c5 --- /dev/null +++ b/tasks/water_group.go @@ -0,0 +1,46 @@ +package tasks + +import ( + "go-wechat/client" + "go-wechat/entity" + "log" +) + +// 水群排行榜 + +// yesterday +// @description: 昨日排行榜 +func yesterday() { + // 获取昨日消息总数 + var yesterdayMsgCount int64 + err := client.MySQL.Model(&entity.Message{}). + Where("from_user = ?", "18958257758@chatroom"). + Where("DATEDIFF(create_at,NOW()) = -1"). + Count(&yesterdayMsgCount).Error + if err != nil { + log.Printf("获取昨日消息总数失败, 错误信息: %v", err) + return + } + log.Printf("昨日消息总数: %d", yesterdayMsgCount) + + // 返回数据 + type record struct { + GroupUser string + Count int64 + } + + var records []record + err = client.MySQL.Model(&entity.Message{}). + Select("group_user", "count( 1 ) AS `count`"). + Where("from_user = ?", "18958257758@chatroom"). + Where("DATEDIFF(create_at,NOW()) = -1"). + Group("group_user").Order("`count` DESC"). + Limit(5).Find(&records).Error + if err != nil { + log.Printf("获取昨日消息失败, 错误信息: %v", err) + return + } + for _, r := range records { + log.Printf("账号: %s -> %d", r.GroupUser, r.Count) + } +} diff --git a/types/message.go b/types/message.go new file mode 100644 index 0000000..f5268d9 --- /dev/null +++ b/types/message.go @@ -0,0 +1,50 @@ +package types + +import "fmt" + +type MessageType int + +const ( + MsgTypeText MessageType = 1 // 文本消息 + MsgTypeImage MessageType = 3 // 图片消息 + MsgTypeVoice MessageType = 34 // 语音消息 + MsgTypeVerify MessageType = 37 // 认证消息 + MsgTypePossibleFriend MessageType = 40 // 好友推荐消息 + MsgTypeShareCard MessageType = 42 // 名片消息 + MsgTypeVideo MessageType = 43 // 视频消息 + MsgTypeEmoticon MessageType = 47 // 表情消息 + MsgTypeLocation MessageType = 48 // 地理位置消息 + MsgTypeApp MessageType = 49 // APP消息 + MsgTypeVoip MessageType = 50 // VOIP消息 + MsgTypeVoipNotify MessageType = 52 // VOIP结束消息 + MsgTypeVoipInvite MessageType = 53 // VOIP邀请 + MsgTypeMicroVideo MessageType = 62 // 小视频消息 + MsgTypeSys MessageType = 10000 // 系统消息 + MsgTypeRecalled MessageType = 10002 // 消息撤回 +) + +var MessageTypeMap = map[MessageType]string{ + MsgTypeText: "文本消息", + MsgTypeImage: "图片消息", + MsgTypeVoice: "语音消息", + MsgTypeVerify: "认证消息", + MsgTypePossibleFriend: "好友推荐消息", + MsgTypeShareCard: "名片消息", + MsgTypeVideo: "视频消息", + MsgTypeEmoticon: "表情消息", + MsgTypeLocation: "地理位置消息", + MsgTypeApp: "APP消息", + MsgTypeVoip: "VOIP消息", + MsgTypeVoipNotify: "VOIP结束消息", + MsgTypeVoipInvite: "VOIP邀请", + MsgTypeMicroVideo: "小视频消息", + MsgTypeSys: "系统消息", + MsgTypeRecalled: "消息撤回", +} + +func (mt MessageType) String() string { + if msg, ok := MessageTypeMap[mt]; ok { + return msg + } + return fmt.Sprintf("未知消息类型(%d)", mt) +}