mirror of
https://github.com/Mrs4s/MiraiGo.git
synced 2025-05-04 19:17:38 +08:00
* kick batch * Update group_info.go * Update client.go * Update builders.go --------- Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com>
438 lines
12 KiB
Go
438 lines
12 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/Mrs4s/MiraiGo/binary"
|
|
"github.com/Mrs4s/MiraiGo/binary/jce"
|
|
"github.com/Mrs4s/MiraiGo/client/internal/network"
|
|
"github.com/Mrs4s/MiraiGo/client/pb/oidb"
|
|
"github.com/Mrs4s/MiraiGo/client/pb/profilecard"
|
|
"github.com/Mrs4s/MiraiGo/internal/proto"
|
|
"github.com/Mrs4s/MiraiGo/utils"
|
|
)
|
|
|
|
type (
|
|
GroupInfo struct {
|
|
Uin int64
|
|
Code int64
|
|
Name string
|
|
OwnerUin int64
|
|
GroupCreateTime uint32
|
|
GroupLevel uint32
|
|
MemberCount uint16
|
|
MaxMemberCount uint16
|
|
Members []*GroupMemberInfo
|
|
// 最后一条信息的SEQ,只有通过 GetGroupInfo 函数获取的 GroupInfo 才会有
|
|
LastMsgSeq int64
|
|
|
|
client *QQClient
|
|
|
|
lock sync.RWMutex
|
|
}
|
|
|
|
GroupMemberInfo struct {
|
|
Group *GroupInfo
|
|
Uin int64
|
|
Nickname string
|
|
CardName string
|
|
JoinTime int64
|
|
LastSpeakTime int64
|
|
SpecialTitle string
|
|
ShutUpTimestamp int64
|
|
Permission MemberPermission
|
|
Level uint16
|
|
Gender byte
|
|
}
|
|
|
|
// GroupSearchInfo 通过搜索得到的群信息
|
|
GroupSearchInfo struct {
|
|
Code int64 // 群号
|
|
Name string // 群名
|
|
Memo string // 简介
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
decoders["SummaryCard.ReqSearch"] = decodeGroupSearchResponse
|
|
decoders["OidbSvc.0x88d_0"] = decodeGroupInfoResponse
|
|
}
|
|
|
|
func (c *QQClient) GetGroupInfo(groupCode int64) (*GroupInfo, error) {
|
|
i, err := c.sendAndWait(c.buildGroupInfoRequestPacket(groupCode))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.(*GroupInfo), nil
|
|
}
|
|
|
|
// OidbSvc.0x88d_0
|
|
func (c *QQClient) buildGroupInfoRequestPacket(groupCode int64) (uint16, []byte) {
|
|
body := &oidb.D88DReqBody{
|
|
AppId: proto.Uint32(c.version().AppId),
|
|
ReqGroupInfo: []*oidb.ReqGroupInfo{
|
|
{
|
|
GroupCode: proto.Uint64(uint64(groupCode)),
|
|
Stgroupinfo: &oidb.D88DGroupInfo{
|
|
GroupOwner: proto.Uint64(0),
|
|
GroupUin: proto.Uint64(0),
|
|
GroupCreateTime: proto.Uint32(0),
|
|
GroupFlag: proto.Uint32(0),
|
|
GroupMemberMaxNum: proto.Uint32(0),
|
|
GroupMemberNum: proto.Uint32(0),
|
|
GroupOption: proto.Uint32(0),
|
|
GroupLevel: proto.Uint32(0),
|
|
GroupFace: proto.Uint32(0),
|
|
GroupName: EmptyBytes,
|
|
GroupMemo: EmptyBytes,
|
|
GroupFingerMemo: EmptyBytes,
|
|
GroupLastMsgTime: proto.Uint32(0),
|
|
GroupCurMsgSeq: proto.Uint32(0),
|
|
GroupQuestion: EmptyBytes,
|
|
GroupAnswer: EmptyBytes,
|
|
GroupGrade: proto.Uint32(0),
|
|
ActiveMemberNum: proto.Uint32(0),
|
|
HeadPortraitSeq: proto.Uint32(0),
|
|
MsgHeadPortrait: &oidb.D88DGroupHeadPortrait{},
|
|
StGroupExInfo: &oidb.D88DGroupExInfoOnly{},
|
|
GroupSecLevel: proto.Uint32(0),
|
|
CmduinPrivilege: proto.Uint32(0),
|
|
NoFingerOpenFlag: proto.Uint32(0),
|
|
NoCodeFingerOpenFlag: proto.Uint32(0),
|
|
},
|
|
},
|
|
},
|
|
PcClientVersion: proto.Uint32(0),
|
|
}
|
|
payload := c.packOIDBPackageProto(2189, 0, body)
|
|
return c.uniPacket("OidbSvc.0x88d_0", payload)
|
|
}
|
|
|
|
// SearchGroupByKeyword 通过关键词搜索陌生群组
|
|
func (c *QQClient) SearchGroupByKeyword(keyword string) ([]GroupSearchInfo, error) {
|
|
rsp, err := c.sendAndWait(c.buildGroupSearchPacket(keyword))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "group search failed")
|
|
}
|
|
return rsp.([]GroupSearchInfo), nil
|
|
}
|
|
|
|
// SummaryCard.ReqSearch
|
|
func (c *QQClient) buildGroupSearchPacket(keyword string) (uint16, []byte) {
|
|
comm, _ := proto.Marshal(&profilecard.BusiComm{
|
|
Ver: proto.Int32(1),
|
|
Seq: proto.Int32(rand.Int31()),
|
|
Service: proto.Int32(80000001),
|
|
Platform: proto.Int32(2),
|
|
Qqver: proto.String("8.5.0.5025"),
|
|
Build: proto.Int32(5025),
|
|
})
|
|
search, _ := proto.Marshal(&profilecard.AccountSearch{
|
|
Start: proto.Int32(0),
|
|
End: proto.Uint32(4),
|
|
Keyword: proto.Some(keyword),
|
|
Highlight: []string{keyword},
|
|
UserLocation: &profilecard.Location{
|
|
Latitude: proto.Float64(0),
|
|
Longitude: proto.Float64(0),
|
|
},
|
|
Filtertype: proto.Int32(0),
|
|
})
|
|
req := &jce.SummaryCardReqSearch{
|
|
Keyword: keyword,
|
|
CountryCode: "+86",
|
|
Version: 3,
|
|
ReqServices: [][]byte{
|
|
binary.NewWriterF(func(w *binary.Writer) {
|
|
w.WriteByte(0x28)
|
|
w.WriteUInt32(uint32(len(comm)))
|
|
w.WriteUInt32(uint32(len(search)))
|
|
w.Write(comm)
|
|
w.Write(search)
|
|
w.WriteByte(0x29)
|
|
}),
|
|
},
|
|
}
|
|
head := jce.NewJceWriter()
|
|
head.WriteInt32(2, 0)
|
|
buf := &jce.RequestDataVersion3{Map: map[string][]byte{
|
|
"ReqHead": packUniRequestData(head.Bytes()),
|
|
"ReqSearch": packUniRequestData(req.ToBytes()),
|
|
}}
|
|
pkt := &jce.RequestPacket{
|
|
IVersion: 3,
|
|
SServantName: "SummaryCardServantObj",
|
|
SFuncName: "ReqSearch",
|
|
SBuffer: buf.ToBytes(),
|
|
Context: make(map[string]string),
|
|
Status: make(map[string]string),
|
|
}
|
|
return c.uniPacket("SummaryCard.ReqSearch", pkt.ToBytes())
|
|
}
|
|
|
|
// SummaryCard.ReqSearch
|
|
func decodeGroupSearchResponse(_ *QQClient, pkt *network.Packet) (any, error) {
|
|
request := &jce.RequestPacket{}
|
|
request.ReadFrom(jce.NewJceReader(pkt.Payload))
|
|
data := &jce.RequestDataVersion2{}
|
|
data.ReadFrom(jce.NewJceReader(request.SBuffer))
|
|
if len(data.Map["RespHead"]["SummaryCard.RespHead"]) > 20 {
|
|
return nil, errors.New("not found")
|
|
}
|
|
rsp := data.Map["RespSearch"]["SummaryCard.RespSearch"][1:]
|
|
r := jce.NewJceReader(rsp)
|
|
// rspService := r.ReadAny(2).([]interface{})[0].([]byte)
|
|
rspService := r.ReadByteArrArr(2)[0]
|
|
sr := binary.NewReader(rspService)
|
|
sr.ReadByte()
|
|
ld1 := sr.ReadInt32()
|
|
ld2 := sr.ReadInt32()
|
|
if ld1 > 0 && ld2+9 < int32(len(rspService)) {
|
|
sr.ReadBytes(int(ld1)) // busi comm
|
|
searchPb := sr.ReadBytes(int(ld2))
|
|
searchRsp := profilecard.AccountSearch{}
|
|
err := proto.Unmarshal(searchPb, &searchRsp)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get search result failed")
|
|
}
|
|
var ret []GroupSearchInfo
|
|
for _, g := range searchRsp.List {
|
|
ret = append(ret, GroupSearchInfo{
|
|
Code: int64(g.Code.Unwrap()),
|
|
Name: g.Name.Unwrap(),
|
|
Memo: g.Brief.Unwrap(),
|
|
})
|
|
}
|
|
return ret, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// OidbSvc.0x88d_0
|
|
func decodeGroupInfoResponse(c *QQClient, pkt *network.Packet) (any, error) {
|
|
rsp := oidb.D88DRspBody{}
|
|
err := unpackOIDBPackage(pkt.Payload, &rsp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rsp.RspGroupInfo) == 0 {
|
|
return nil, errors.New(string(rsp.StrErrorInfo))
|
|
}
|
|
info := rsp.RspGroupInfo[0]
|
|
if info.GroupInfo == nil {
|
|
return nil, errors.New("group info not found")
|
|
}
|
|
return &GroupInfo{
|
|
Uin: int64(info.GroupInfo.GroupUin.Unwrap()),
|
|
Code: int64(info.GroupCode.Unwrap()),
|
|
Name: string(info.GroupInfo.GroupName),
|
|
GroupCreateTime: info.GroupInfo.GroupCreateTime.Unwrap(),
|
|
GroupLevel: info.GroupInfo.GroupLevel.Unwrap(),
|
|
OwnerUin: int64(info.GroupInfo.GroupOwner.Unwrap()),
|
|
MemberCount: uint16(info.GroupInfo.GroupMemberNum.Unwrap()),
|
|
MaxMemberCount: uint16(info.GroupInfo.GroupMemberMaxNum.Unwrap()),
|
|
Members: []*GroupMemberInfo{},
|
|
LastMsgSeq: int64(info.GroupInfo.GroupCurMsgSeq.Unwrap()),
|
|
client: c,
|
|
}, nil
|
|
}
|
|
|
|
func (c *QQClient) uploadGroupHeadPortrait(groupCode int64, img []byte) error {
|
|
url := fmt.Sprintf("http://htdata3.qq.com/cgi-bin/httpconn?htcmd=0x6ff0072&ver=5520&ukey=%v&range=0&uin=%v&seq=23&groupuin=%v&filetype=3&imagetype=5&userdata=0&subcmd=1&subver=101&clip=0_0_0_0&filesize=%v",
|
|
c.getSKey(), c.Uin, groupCode, len(img))
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(img))
|
|
req.Header["User-Agent"] = []string{"Dalvik/2.1.0 (Linux; U; Android 7.1.2; PCRT00 Build/N2G48H)"}
|
|
req.Header["Content-Type"] = []string{"multipart/form-data;boundary=****"}
|
|
rsp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to upload group head portrait")
|
|
}
|
|
rsp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (g *GroupInfo) UpdateName(newName string) {
|
|
if g.AdministratorOrOwner() && newName != "" && strings.Count(newName, "") <= 20 {
|
|
g.client.updateGroupName(g.Code, newName)
|
|
g.Name = newName
|
|
}
|
|
}
|
|
|
|
func (g *GroupInfo) UpdateGroupHeadPortrait(img []byte) {
|
|
if g.AdministratorOrOwner() {
|
|
_ = g.client.uploadGroupHeadPortrait(g.Uin, img)
|
|
}
|
|
}
|
|
|
|
func (g *GroupInfo) MuteAll(mute bool) {
|
|
if g.AdministratorOrOwner() {
|
|
g.client.groupMuteAll(g.Code, mute)
|
|
}
|
|
}
|
|
|
|
func (g *GroupInfo) MuteAnonymous(id, nick string, seconds int32) error {
|
|
payload := fmt.Sprintf("anony_id=%v&group_code=%v&seconds=%v&anony_nick=%v&bkn=%v", url.QueryEscape(id), g.Code, seconds, nick, g.client.getCSRFToken())
|
|
rsp, err := utils.HttpPostBytesWithCookie("https://qqweb.qq.com/c/anonymoustalk/blacklist", []byte(payload), g.client.getCookies(), "application/x-www-form-urlencoded")
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to request blacklist")
|
|
}
|
|
var muteResp struct {
|
|
RetCode int `json:"retcode"`
|
|
CGICode int `json:"cgicode"`
|
|
}
|
|
err = json.Unmarshal(rsp, &muteResp)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse muteResp")
|
|
}
|
|
if muteResp.RetCode != 0 {
|
|
return errors.Errorf("retcode %v", muteResp.RetCode)
|
|
}
|
|
if muteResp.CGICode != 0 {
|
|
return errors.Errorf("retcode %v", muteResp.CGICode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *GroupInfo) Quit() {
|
|
if g.SelfPermission() != Owner {
|
|
g.client.quitGroup(g.Code)
|
|
}
|
|
}
|
|
|
|
func (g *GroupInfo) SelfPermission() MemberPermission {
|
|
return g.FindMember(g.client.Uin).Permission
|
|
}
|
|
|
|
func (g *GroupInfo) AdministratorOrOwner() bool {
|
|
return g.SelfPermission() == Administrator || g.SelfPermission() == Owner
|
|
}
|
|
|
|
func (g *GroupInfo) FindMember(uin int64) *GroupMemberInfo {
|
|
r := g.Read(func(info *GroupInfo) any {
|
|
return info.FindMemberWithoutLock(uin)
|
|
})
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.(*GroupMemberInfo)
|
|
}
|
|
|
|
func (g *GroupInfo) FindMemberWithoutLock(uin int64) *GroupMemberInfo {
|
|
i := sort.Search(len(g.Members), func(i int) bool {
|
|
return g.Members[i].Uin >= uin
|
|
})
|
|
if i >= len(g.Members) || g.Members[i].Uin != uin {
|
|
return nil
|
|
}
|
|
return g.Members[i]
|
|
}
|
|
|
|
// sort call this method must hold the lock
|
|
func (g *GroupInfo) sort() {
|
|
sort.Slice(g.Members, func(i, j int) bool {
|
|
return g.Members[i].Uin < g.Members[j].Uin
|
|
})
|
|
}
|
|
|
|
func (g *GroupInfo) Update(f func(*GroupInfo)) {
|
|
g.lock.Lock()
|
|
defer g.lock.Unlock()
|
|
f(g)
|
|
}
|
|
|
|
func (g *GroupInfo) Read(f func(*GroupInfo) any) any {
|
|
g.lock.RLock()
|
|
defer g.lock.RUnlock()
|
|
return f(g)
|
|
}
|
|
|
|
func (m *GroupMemberInfo) DisplayName() string {
|
|
if m.CardName == "" {
|
|
return m.Nickname
|
|
}
|
|
return m.CardName
|
|
}
|
|
|
|
func (m *GroupMemberInfo) EditCard(card string) {
|
|
if m.CardChangable() && len(card) <= 60 {
|
|
m.Group.client.editMemberCard(m.Group.Code, m.Uin, card)
|
|
m.CardName = card
|
|
}
|
|
}
|
|
|
|
func (m *GroupMemberInfo) Poke() {
|
|
m.Group.client.SendGroupPoke(m.Group.Code, m.Uin)
|
|
}
|
|
|
|
func (m *GroupMemberInfo) SetAdmin(flag bool) {
|
|
if m.Group.OwnerUin == m.Group.client.Uin {
|
|
m.Group.client.setGroupAdmin(m.Group.Code, m.Uin, flag)
|
|
}
|
|
}
|
|
|
|
func (m *GroupMemberInfo) EditSpecialTitle(title string) {
|
|
if m.Group.SelfPermission() == Owner && len(title) <= 18 {
|
|
m.Group.client.editMemberSpecialTitle(m.Group.Code, m.Uin, title)
|
|
m.SpecialTitle = title
|
|
}
|
|
}
|
|
|
|
func (m *GroupMemberInfo) Kick(msg string, block bool) error {
|
|
if m.Uin != m.Group.client.Uin && m.Manageable() {
|
|
m.Group.client.KickGroupMembers(m.Group.Code, msg, block, m.Uin)
|
|
return nil
|
|
} else {
|
|
return errors.New("not manageable")
|
|
}
|
|
}
|
|
|
|
func (m *GroupMemberInfo) Mute(time uint32) error {
|
|
if time >= 2592000 {
|
|
return errors.New("time is not in range")
|
|
}
|
|
if m.Uin != m.Group.client.Uin && m.Manageable() {
|
|
m.Group.client.groupMute(m.Group.Code, m.Uin, time)
|
|
return nil
|
|
} else {
|
|
return errors.New("not manageable")
|
|
}
|
|
}
|
|
|
|
func (g *GroupInfo) SetAnonymous(enable bool) {
|
|
if g.AdministratorOrOwner() {
|
|
g.client.setGroupAnonymous(g.Code, enable)
|
|
}
|
|
}
|
|
|
|
func (m *GroupMemberInfo) Manageable() bool {
|
|
if m.Uin == m.Group.client.Uin {
|
|
return true
|
|
}
|
|
self := m.Group.SelfPermission()
|
|
if self == Member || m.Permission == Owner {
|
|
return false
|
|
}
|
|
return m.Permission != Administrator || self == Owner
|
|
}
|
|
|
|
func (m *GroupMemberInfo) CardChangable() bool {
|
|
if m.Uin == m.Group.client.Uin {
|
|
return true
|
|
}
|
|
self := m.Group.SelfPermission()
|
|
if self == Member {
|
|
return false
|
|
}
|
|
return m.Permission != Owner
|
|
}
|