diff --git a/.gitignore b/.gitignore index 7397e03..6e5e379 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ sig.bin qrcode.png signature.bin *.toml +*.db diff --git a/README.md b/README.md index 6b2da8f..b185ef5 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,7 @@ ## 开发进度 -目前距离第一个Release版本的发布还有很多需要进行的工作: -- [x] 基础的事件总线系统 -- [x] 内置LagrangeGo适配器的部分事件发送 -- [x] 类Satori的消息解析 -- [x] 配置读取/生成/补全 -- [ ] 数据库连接,统一数据库接口设计 -- [ ] 消息发送/统一Client设计 -- [ ] 插件系统设计和实现 -- [ ] 完善内置LagrangeGo适配器的事件接收和处理 -- [ ] 集群模式设计和实现(集群的动态和静态总控模式) -- [ ] 排障和性能优化 -- [ ] 自动化测试 -- [ ] 确定各项基础程序设计,编写使用文档 +Refact分支正在进行重构,目前进度还在推进,过几天再来探索吧~ ## 特性 diff --git a/REFACT.md b/REFACT.md new file mode 100644 index 0000000..c11a273 --- /dev/null +++ b/REFACT.md @@ -0,0 +1,43 @@ +这个分支是对Iceinu现有设计的重构 + +因为写的实在是有点太混乱并且写的过程中又有了一些新的想法,需要对代码进行一遍重构来改善质量。 + +在之后完成度超过主分支之后会进行合并并替代主分支 + +## 重构目标 + +之前对于Iceinu的组网设计比较抽象,虽然设想了集群但是完全没有做出相应的设计。 + +Iceinu会在启动时为实例分配一个ID,作为对每个实例的操作基础,机器人的实例组网有多种方式。 + +首先是单节点模式,也就是默认的Bot运行模式,不进行集群,实例的事件总线完全本地运行。 + +静态集群模式,需要一个可以被访问的主实例,这个模式下每个实例的事件总线仍然是本地运行的,但是节点会向主实例推送 +自己的用户树,主节点会和所有子节点维护一个长连接网络,用于互相推送事件,在接收到用户树推送/更新后动态计算分配 +各个节点的用户可用性,实现各个bot之间消息处理的去重 + +动态集群模式,基本和静态集群模式一致,但是需要各个节点之间都可以相互访问,当主节点出现任何问题导致无法访问时,其 +他任何一个节点都可以重新成为主实例。 + +分布式模式,这个模式下的事件总线只在主实例上运行,主实例会通过长连接网络接受每一个节点的消息事件推送,并将消息处 +理事件对所有节点进行广播,子节点自动匹配对应的消息处理事件。 + +集群模式下各个节点各自具备完整的信息处理能力,有自己的消息缓存,适合有多台性能比较高的机器的情况,且动态集群模式 +如果可以实现应该是可用性总体来讲最优秀的状态。分布式实际上是将子节点变成了传统意义上的协议端,对低性能设备更友好 +,非常适合分散部署大量实例。 + +同时还要引入一个新的设计,模型(Model),用来定义一个或多个平台的消息事件,默认模型应该是基于Satori的,但是如果 +出现了自己定制私有的消息事件模型和适配器的情况,可以直接通过更换模型来实现,模型无需实现接口,它本身就不是通用的 +,而是在特殊情况下的一个解决方案,可以让适配器的设计更加灵活,用来设计一些不考虑全平台兼容的适配器,只需要实现适 +配单个平台的消息事件。 + +消息缓存,这个也是之前的设计基本没有考虑的一个东西,适配器可以自由实现对应平台的消息模型,Iceinu自动将消息模型实 +现为缓存数据库中的一个表,这个缓存数据表具有固定的缓存数量,用来缓存对应平台的消息,供适配器进行取用。 + +目前消息缓存基本上会使用一个额外的DuckDB来实现,因为消息可能需要一定程度上的持久化,加上嵌入式数据库部署更方便, +所以也就暂时不考虑Redis之类的缓存数据库。 + +在Iceinu的设计里,main.go实际上更接近于一个启动脚本,如果直接将Iceinu作为开发框架引入那么修改这个启动脚本就可以 +实现大部分对框架本身的配置,我希望如果其他开发者使用Iceinu进行二次开发的话那么他们在开发过程中能集中精力主要实 +现自己的想法和功能,框架能把绝大多数大家需要的功能都封装好,能够让大家都用最轻松的方式进行开发;同时如果将Iceinu +作为库来导入自由利用其中的部分设计来实现自己的功能也可以比较方便的进行调用。 \ No newline at end of file diff --git a/adapter/adapter.go b/adapter/adapter.go deleted file mode 100644 index f9806c0..0000000 --- a/adapter/adapter.go +++ /dev/null @@ -1,18 +0,0 @@ -package adapter - -// 继承IceAdapter接口即可实现适配器,具体的适配器实现模式参考文档 - -// IceAdapterMeta 适配器元信息 -type IceAdapterMeta struct { - AdapterName string // 适配器名称 - Version string // 适配器版本 - Platform string // 适配器平台 - Author []string // 适配器作者 - Introduce string // 适配器介绍 -} - -// IceAdapter Iceinu的适配器接口,用于实现适配器实例 -type IceAdapter interface { - GetMeta() *IceAdapterMeta // 获取适配器的元信息 - Init() // 适配器初始化 -} diff --git a/adapter/bot.go b/adapter/bot.go deleted file mode 100644 index 0a4fa0f..0000000 --- a/adapter/bot.go +++ /dev/null @@ -1,104 +0,0 @@ -package adapter - -import ( - "github.com/Iceinu-Project/iceinu/elements" - "github.com/Iceinu-Project/iceinu/resource" -) - -// Bot Iceinu的客户端接口API,用于实现iceinu对平台客户端的直接操作 -type Bot interface { - // 首先是基于Satori标准的API - // 这部分可以参考Satori文档的资源部分,但是有一定的不同,为了方便兼容各种使用方式进行了一些扩展 - // 比如没有直接支持分页这个东西,方便使用 - // https://satori.js.org/zh-CN/resources/channel.html - - GetChannel(channelId string) *resource.Channel // 获取频道信息 - GetChannelList(groupId string) []*resource.Channel // 获取指定群组中的频道列表 - GetChannelListByToken(next string) *resource.PagedList // 获取指定群组中的频道列表(分页) - CreateChannel(groupId string, data *resource.Channel) (*resource.Channel, error) // 创建频道 - UpdateChannel(groupId string, data *resource.Channel) error // 更新频道 - DeleteChannel(channelId string) error // 删除频道 - MuteChannel(channelId string, duration uint32) error // 禁言频道 - CreateUserChannel(userId string, groupId string) (*resource.Channel, error) // 创建用户(私聊)频道 - - GetGroup(groupId string) *resource.Group // 获取群组信息 - GetGroupList() []*resource.Group // 获取群组列表 - GetGroupListByToken(next string) *resource.PagedList // 获取群组列表(分页) - ApproveGroupInvite(messageId string, approve bool, comment string) error // 处理群组邀请bot加入请求 - - GetGroupMember(groupId string, userId string) *resource.GroupMember // 获取指定群组成员信息 - GetGroupMemberList(groupId string) []*resource.GroupMember // 获取群组成员列表 - GetGroupMemberListByToken(groupId string, next string) *resource.PagedList // 获取群组成员列表(分页) - KickGroupMember(groupId string, userId string, permanent bool) error // 踢出群组成员 - MuteGroupMember(groupId string, userId string, duration uint32) error // 禁言群组成员 - ApproveGroupRequest(messageId string, approve bool, comment string) error // 处理群组加入请求 - - SetGroupMemberRole(groupId string, userId string, roleId string) error // 设置群组成员角色权限 - UnsetGroupMemberRole(groupId string, userId string, roleId string) error // 取消群组成员角色权限 - GetGroupRoleList(groupId string) []*resource.GroupRole // 获取群组角色权限列表 - GetGroupRoleListByToken(groupId string, next string) *resource.PagedList // 获取群组角色权限列表(分页) - CreateGroupRole(groupId string, role *resource.GroupRole) (*resource.GroupRole, error) // 创建群组角色权限 - UpdateGroupRole(groupId string, roleId string, role *resource.GroupRole) error // 更新群组角色权限 - DeleteGroupRole(groupId string, roleId string) error // 删除群组角色权限 - - GetLoginInfo() *resource.Login // 获取登录信息 - - SendContent(groupId string, channelId string, content string) (*resource.Message, error) // 发送纯文本消息 - GetMessage(channelId string, messageId string) (*resource.Message, error) // (从缓存中)获取指定消息 - RecallMessage(channelId string, messageId string) error // 撤回指定消息 - UpdateMessage(channelId string, messageId string, content string) error // 编辑指定消息 - GetMessageList(channelId string, limit uint32, order bool) []*resource.Message // 获取一定数量的频道消息列表 - GetMessageListByRange(channelId string, messageId string, start uint32, count uint32) []*resource.Message // 获取频道消息列表(可指定范围) - - CreateReaction(channelId string, messageId string, emoji string) error // 添加消息反应 - DeleteReaction(channelId string, messageId string, emoji string, userId string) error // 删除消息反应 - ClearReaction(channelId string, messageId string, emoji string) error // 清除消息反应 - GetReactionList(channelId string, messageId string, emoji string) []resource.User // 获取消息反应列表 - GetReactionListByToken(channelId string, messageId string, emoji string, next string) *resource.PagedList // 获取消息反应列表(分页) - - GetUser(userId string) *resource.User // 获取指定用户信息 - GetFriendList() []*resource.User // 获取好友列表信息 - ApproveFriendRequest(messageId string, approve string, comment string) error // 处理好友请求 - - // Iceinu的特有API - // 其中一部分是对各个平台功能的扩展适配,还有一部分是其他功能的快捷方式 - - Send(groupId string, channelId string, elements []elements.IceinuMessageElement) (*resource.Message, error) // 直接发送Iceinu通用元素 - SendSatori(groupId string, channelId string, satori string) (*resource.Message, error) // 发送Satori XHTML格式的消息字符串,自动解析成通用元素并发送 - SendPoke(groupId string, channelId string, userId string) error // 发送戳一戳 - - GetSelfUserId() string // 获取自己的用户ID - GetSelfUserName() string // 获取自己的用户名 - GetSelfAvatarUrl() string // 获取自己的头像URL - GetSeldUserNickname() string // 获取自己的昵称 - GetGroupAvatarUrl(groupId string) string // 获取指定群组的头像URL - - RefreshUserListCache() error // 刷新用户列表 - RefreshGroupListCache() error // 刷新群组列表 - RefreshGroupMemberCache(groupId string, userId string) error // 刷新指定群组的指定成员的信息 - RefreshGroupAllMembersCache(groupId string) error // 刷新指定群组所有成员的信息 - RefreshChannelListCache(groupId string) error // 刷新指定群组的频道列表 - - RenameGroup(groupId string, name string) error // 重命名群组 - RenameGroupMember(groupId string, userId string, nickname string) error // 重命名群组成员 - RemarkGroup(groupId string, remark string) error // 设置群组备注 - SetGroupGlobalMute(groupId string, mute bool) error // 设置群组全员禁言 - LeaveGroup(groupId string) error // 退出群组 - SetGroupMemberTitle(groupId string, userId string, title string) error // 给群组成员设置头衔 - - // 这部分功能接口设计主要来自LagrangeGo,但是也可能在其他NTQQ平台上实现 - - UploadChannelFile(channelId string, filePath string) error // 向频道上传文件 - UploadGroupFile(groupId string, filePath string, targetFilePath string) error // 向群组上传文件 - GetGroupFileSystemInfo(groupId string) *resource.GroupFileSystemInfo // 获取群组文件系统信息(暂未确定) - GetGroupFilesByFolder(groupId string, folderId string) interface{} // 获取群组文件夹内的文件列表(暂未确定) - GetGroupRootFiles(groupId string) interface{} // 获取群组根目录文件列表(暂未确定) - MoveGroupFile(groupId string, fileId string, parentFolder string, targetFolderId string) error // 移动群组文件 - DeleteGroupFile(groupId string, fileId string) error // 删除群组文件 - CreateGroupFolder(groupId string, folderName string, parentFolder string) error // 创建群组文件夹 - RenameGroupFolder(groupId string, folderId string, newFolderName string) error // 重命名群组文件夹 - DeleteGroupFolder(groupId string, folderId string) error // 删除群组文件夹 - - // GetOriginalClient 获取适配器的原始客户端对象,部分适配器可能不需要这个东西,只是方便直接传递原本的客户端实例 - GetOriginalClient() interface{} // 获取原始客户端对象 -} diff --git a/adapter/lagrange/element_converter.go b/adapter/lagrange/element_converter.go deleted file mode 100644 index 1840cf5..0000000 --- a/adapter/lagrange/element_converter.go +++ /dev/null @@ -1,107 +0,0 @@ -package lagrange - -import ( - "github.com/Iceinu-Project/iceinu/elements" - "github.com/LagrangeDev/LagrangeGo/message" - "strconv" - "strings" - "time" -) - -// ConvertIceElement 将LagrangeGo的元素转换为Iceinu的元素 -func ConvertIceElement(e []message.IMessageElement) *[]elements.IceinuMessageElement { - // 从LagrangeGo的元素转换为Iceinu的元素 - var IceinuElements []elements.IceinuMessageElement - // 遍历传入的元素 - for _, ele := range e { - switch ele.Type() { - // 将元素依次对应转换并传入 - case message.Text: - ele := ele.(*message.TextElement) - // 检测文本中是否包含换行符 - if strings.Contains(ele.Content, "\n") { - // 如果包含换行符,进行拆分和处理 - textParts := strings.Split(ele.Content, "\n") - for i, part := range textParts { - // 将每段文本添加到 IceinuElements - IceinuElements = append(IceinuElements, &elements.TextElement{Text: part}) - // 如果不是最后一段文本,则插入 BrElement - if i < len(textParts)-1 { - IceinuElements = append(IceinuElements, &elements.BrElement{}) - } - } - } else { - // 如果不包含换行符,直接添加文本元素 - IceinuElements = append(IceinuElements, &elements.TextElement{Text: ele.Content}) - } - case message.At: - ele := ele.(*message.AtElement) - IceinuElements = append(IceinuElements, &elements.AtElement{ - Id: strconv.Itoa(int(ele.TargetUin)), - Name: ele.Display, - Role: "", - Type: strconv.Itoa(int(ele.Type())), - }) - case message.Face: - ele := ele.(*message.FaceElement) - IceinuElements = append(IceinuElements, &elements.FaceElement{ - Id: ele.FaceID, - }) - case message.Voice: - ele := ele.(*message.VoiceElement) - IceinuElements = append(IceinuElements, &elements.AudioElement{ - Src: ele.Url, - Title: ele.Name, - Duration: ele.Size, - Poster: "", - }) - case message.Image: - ele := ele.(*message.ImageElement) - IceinuElements = append(IceinuElements, &elements.ImageElement{ - Src: ele.Url, - Width: ele.Width, - Height: ele.Height, - Title: ele.ImageId, - }) - case message.File: - ele := ele.(*message.FileElement) - IceinuElements = append(IceinuElements, &elements.FileElement{ - Src: ele.FileUrl, - Title: ele.FileName, - }) - case message.Reply: - ele := ele.(*message.ReplyElement) - IceinuElements = append(IceinuElements, &elements.QuoteElement{ - UserId: strconv.Itoa(int(ele.SenderUin)), - UserName: ele.SenderUid, - GroupId: strconv.Itoa(int(ele.GroupUin)), - Timestamp: time.Unix(int64(ele.Time), 0), - Elements: ConvertIceElement(ele.Elements), - }) - case message.Forward: - ele := ele.(*message.ForwardMessage) - IceinuElements = append(IceinuElements, &elements.MessageElement{ - Forward: true, - Elements: UnzipNodes(ele.Nodes), - }) - - default: - IceinuElements = append(IceinuElements, &elements.UnsupportedElement{Type: strconv.Itoa(int(ele.Type()))}) - } - } - return &IceinuElements -} - -func UnzipNodes(n []*message.ForwardNode) *[]elements.IceinuMessageElement { - var IceinuElements []elements.IceinuMessageElement - for _, node := range n { - IceinuElements = append(IceinuElements, &elements.NodeElement{ - GroupId: node.GroupId, - SenderId: node.SenderId, - SenderName: node.SenderName, - Time: node.Time, - Message: ConvertIceElement(node.Message), - }) - } - return &IceinuElements -} diff --git a/adapter/lagrange/lagrange_adapter.go b/adapter/lagrange/lagrange_adapter.go deleted file mode 100644 index f2d6ce5..0000000 --- a/adapter/lagrange/lagrange_adapter.go +++ /dev/null @@ -1,143 +0,0 @@ -package lagrange - -import ( - "github.com/Iceinu-Project/iceinu/adapter" - "github.com/Iceinu-Project/iceinu/config" - "github.com/Iceinu-Project/iceinu/ice" - "github.com/Iceinu-Project/iceinu/logger" - "github.com/Iceinu-Project/iceinu/utils" - "github.com/LagrangeDev/LagrangeGo/client" - "github.com/LagrangeDev/LagrangeGo/client/auth" - "github.com/sirupsen/logrus" - "os" - "os/signal" - "syscall" - "time" -) - -type AdapterLagrange struct { -} - -type Bot struct { - *client.QQClient -} - -var LgrClient *Bot -var lagrangeConfig *AdapterLagrangeConfig - -func (lgr *AdapterLagrange) GetMeta() *adapter.IceAdapterMeta { - return &adapter.IceAdapterMeta{ - AdapterName: "Lagrange Adapter", - Version: "β0.2.1", - Platform: "NTQQ", - Author: []string{ - "Kyoku", - }, - Introduce: "基于Lagrange的NTQQ适配器,内置了LagrangeGo,无需再连接额外的协议端。", - } -} - -func (lgr *AdapterLagrange) Init() { - logger.Infof("正在初始化Lagrange适配器,适配器当前版本: %s", lgr.GetMeta().Version) - - // 获取配置文件内容 - cm := config.GetManager() - lagrangeConfig := cm.Get("lagrange.toml").(*AdapterLagrangeConfig) - logger.Infof("%v", lagrangeConfig.Password == "") - - // 发送一个适配器初始化事件 - ice.Bus.Publish("AdapterInitEvent", ice.AdapterInitEvent{ - Timestamp: time.Time{}, - AdapterMeta: lgr.GetMeta(), - }) - - // 创建LagrangeGo的客户端实例 - plogger := logger.GetProtocolLogger() - appInfo := auth.AppList["linux"]["3.2.10-25765"] - deviceInfo := auth.NewDeviceInfo(lagrangeConfig.Account) - qqClientInstance := client.NewClient(uint32(lagrangeConfig.Account), appInfo, lagrangeConfig.SignUrl) - qqClientInstance.SetLogger(plogger) - qqClientInstance.UseDevice(deviceInfo) - - data, err := os.ReadFile("signature.bin") - if err != nil { - logrus.Warnln("读取签名文件时发生错误:", err) - } else { - sig, err := auth.UnmarshalSigInfo(data, true) - if err != nil { - logrus.Warnln("加载签名文件时发生错误:", err) - } else { - qqClientInstance.UseSig(sig) - } - } - LgrClient = &Bot{QQClient: qqClientInstance} - - defer LgrClient.Release() - defer SaveSignature() - - // 登录 - err = Login() - if err != nil { - return - } - - var bot = GetBot() - - // 刷新client的缓存 - err = LgrClient.RefreshAllGroupsInfo() - if err != nil { - return - } - err = LgrClient.RefreshFriendCache() - if err != nil { - return - } - err = LgrClient.RefreshFriendCache() - - utils.JPrint(bot.GetGroupMemberList("970801565")) - bot.SendContent("0", "2913844577", "测试消息") - - // 设置事件订阅器,将LagrangeGo的事件转换并发送到iceinu的事件总线上 - SetAllHandler() - SetAllSubscribes() - - // 主协程关闭通道 - mc := make(chan os.Signal, 2) - signal.Notify(mc, os.Interrupt, syscall.SIGTERM) - for { - switch <-mc { - case os.Interrupt, syscall.SIGTERM: - return - } - } -} - -// Login 登录 -func Login() error { - // 声明 err 变量并进行错误处理 - err := LgrClient.Login("", "qrcode.png") - if err != nil { - logrus.Errorln("登录时发生错误:", err) - return err - } - // 推送登录事件 - ice.Bus.Publish("AdapterLoginEvent", ice.AdapterInitEvent{ - Timestamp: time.Time{}, - }) - return nil -} - -// SaveSignature 保存sign信息 -func SaveSignature() { - data, err := LgrClient.Sig().Marshal() - if err != nil { - logger.Error("生成签名文件时发生错误err:", err) - return - } - err = os.WriteFile("signature.bin", data, 0644) - if err != nil { - logger.Error("写入签名文件时发生错误 err:", err) - return - } - logger.Info("签名已被写入签名文件") -} diff --git a/adapter/lagrange/lagrange_bot.go b/adapter/lagrange/lagrange_bot.go deleted file mode 100644 index 54bab36..0000000 --- a/adapter/lagrange/lagrange_bot.go +++ /dev/null @@ -1,533 +0,0 @@ -package lagrange - -import ( - "github.com/Iceinu-Project/iceinu/elements" - "github.com/Iceinu-Project/iceinu/resource" - "github.com/LagrangeDev/LagrangeGo/message" - "strconv" - "time" -) - -type BotLagrange struct { -} - -func (b BotLagrange) GetChannel(channelId string) *resource.Channel { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetChannelList(groupId string) []*resource.Channel { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetChannelListByToken(next string) *resource.PagedList { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) CreateChannel(groupId string, data *resource.Channel) (*resource.Channel, error) { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) UpdateChannel(groupId string, data *resource.Channel) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) DeleteChannel(channelId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) MuteChannel(channelId string, duration uint32) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) CreateUserChannel(userId string, groupId string) (*resource.Channel, error) { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetGroup(groupId string) *resource.Group { - groupUin, _ := strconv.Atoi(groupId) - group := LgrClient.GetCachedGroupInfo(uint32(groupUin)) - return &resource.Group{ - Id: strconv.Itoa(int(group.GroupUin)), - Name: group.GroupName, - Avatar: group.Avatar, - Maxcount: group.MaxMember, - MemberCount: group.MemberCount, - } -} - -func (b BotLagrange) GetGroupList() []*resource.Group { - info, _ := LgrClient.GetAllGroupsInfo() - groups := make([]*resource.Group, 0) - for _, group := range info { - groups = append(groups, &resource.Group{ - Id: strconv.Itoa(int(group.GroupUin)), - Name: group.GroupName, - Avatar: group.Avatar, - Maxcount: group.MaxMember, - MemberCount: group.MemberCount, - }) - } - return groups -} - -func (b BotLagrange) GetGroupListByToken(next string) *resource.PagedList { - info, _ := LgrClient.GetAllGroupsInfo() - groups := make([]*resource.Group, 0) - for _, group := range info { - groups = append(groups, &resource.Group{ - Id: strconv.Itoa(int(group.GroupUin)), - Name: group.GroupName, - Avatar: group.Avatar, - Maxcount: group.MaxMember, - MemberCount: group.MemberCount, - }) - } - next = "" - return &resource.PagedList{ - Data: groups, - Next: "", - } -} - -func (b BotLagrange) ApproveGroupInvite(messageId string, approve bool, comment string) error { - messageId = "" - approve = false - comment = "" - return nil -} - -func (b BotLagrange) GetGroupMember(groupId string, userId string) *resource.GroupMember { - groupIdInt, _ := strconv.Atoi(groupId) - userIdInt, _ := strconv.Atoi(userId) - members, err := LgrClient.GetGroupMembersData(uint32(groupIdInt)) - if err != nil { - return nil - } - if member, ok := members[uint32(userIdInt)]; ok { - return &resource.GroupMember{ - User: &resource.User{ - Id: userId, - Name: member.MemberName, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - IsBot: false, - }, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - JoinedAt: time.Unix(int64(member.JoinTime), 0), - } - } - // 否则返回空 - return nil -} - -func (b BotLagrange) GetGroupMemberList(groupId string) []*resource.GroupMember { - groupIdInt, _ := strconv.Atoi(groupId) - members, err := LgrClient.GetGroupMembersData(uint32(groupIdInt)) - if err != nil { - return nil - } - groupMembers := make([]*resource.GroupMember, 0) - for _, member := range members { - groupMembers = append(groupMembers, &resource.GroupMember{ - User: &resource.User{ - Id: strconv.Itoa(int(member.Uin)), - Name: member.MemberName, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - IsBot: false, - }, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - JoinedAt: time.Unix(int64(member.JoinTime), 0), - }) - } - return groupMembers -} - -func (b BotLagrange) GetGroupMemberListByToken(groupId string, _ string) *resource.PagedList { - groupIdInt, _ := strconv.Atoi(groupId) - members, err := LgrClient.GetGroupMembersData(uint32(groupIdInt)) - if err != nil { - return nil - } - groupMembers := make([]*resource.GroupMember, 0) - for _, member := range members { - groupMembers = append(groupMembers, &resource.GroupMember{ - User: &resource.User{ - Id: strconv.Itoa(int(member.Uin)), - Name: member.MemberName, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - IsBot: false, - }, - Nickname: member.DisplayName(), - Avatar: member.Avatar, - JoinedAt: time.Unix(int64(member.JoinTime), 0), - }) - } - return &resource.PagedList{ - Data: groupMembers, - Next: "", - } -} - -func (b BotLagrange) KickGroupMember(groupId string, userId string, permanent bool) error { - groupIdInt, _ := strconv.Atoi(groupId) - userIdInt, _ := strconv.Atoi(userId) - err := LgrClient.GroupKickMember(uint32(groupIdInt), uint32(userIdInt), permanent) - if err != nil { - return err - } - return nil -} - -func (b BotLagrange) MuteGroupMember(groupId string, userId string, duration uint32) error { - groupIdInt, _ := strconv.Atoi(groupId) - userIdInt, _ := strconv.Atoi(userId) - err := LgrClient.GroupMuteMember(uint32(groupIdInt), uint32(userIdInt), duration) - if err != nil { - return err - } - return nil -} - -func (b BotLagrange) ApproveGroupRequest(messageId string, approve bool, comment string) error { - // 晚点实现 - panic("implement me") -} - -// SetGroupMemberRole 设置群成员角色,在NTQQ中实际上只能是设置管理员 -func (b BotLagrange) SetGroupMemberRole(groupId string, userId string, _ string) error { - groupIdInt, _ := strconv.Atoi(groupId) - userIdInt, _ := strconv.Atoi(userId) - err := LgrClient.GroupSetAdmin(uint32(groupIdInt), uint32(userIdInt), true) - if err != nil { - return err - } - return nil -} - -// UnsetGroupMemberRole 取消群成员角色,在NTQQ中实际上只能是取消管理员 -func (b BotLagrange) UnsetGroupMemberRole(groupId string, userId string, _ string) error { - groupIdInt, _ := strconv.Atoi(groupId) - userIdInt, _ := strconv.Atoi(userId) - err := LgrClient.GroupSetAdmin(uint32(groupIdInt), uint32(userIdInt), false) - if err != nil { - return err - } - return nil -} - -func (b BotLagrange) GetGroupRoleList(_ string) []*resource.GroupRole { - return []*resource.GroupRole{ - { - Id: "1", - Name: "管理员", - }, - { - Id: "2", - Name: "群员", - }, - } -} - -func (b BotLagrange) GetGroupRoleListByToken(groupId string, next string) *resource.PagedList { - return &resource.PagedList{ - Data: []*resource.GroupRole{ - { - Id: "1", - Name: "管理员", - }, - { - Id: "2", - Name: "群员", - }, - }, - Next: "", - } -} - -func (b BotLagrange) CreateGroupRole(_ string, _ *resource.GroupRole) (*resource.GroupRole, error) { - return nil, nil -} - -func (b BotLagrange) UpdateGroupRole(_ string, _ string, _ *resource.GroupRole) error { - return nil -} - -func (b BotLagrange) DeleteGroupRole(_ string, _ string) error { - return nil -} - -func (b BotLagrange) GetLoginInfo() *resource.Login { - return nil -} - -func (b BotLagrange) SendContent(groupId string, channelId string, content string) (*resource.Message, error) { - groupIdInt, _ := strconv.Atoi(groupId) - channelIdInt, _ := strconv.Atoi(channelId) - var msg []message.IMessageElement - if groupIdInt == 0 { - msg = append(msg, message.NewText(content)) - res, err := LgrClient.SendPrivateMessage(uint32(channelIdInt), msg) - if err != nil { - return nil, err - } - return &resource.Message{ - Id: strconv.Itoa(int(res.Id)), - Content: res.ToString(), - Channel: nil, - Group: nil, - Member: nil, - User: nil, - CreatedAt: time.Unix(int64(res.Time), 0), - UpdatedAt: time.Time{}, - MessageElements: ConvertIceElement(res.Elements), - }, nil - } else { - msg = append(msg, message.NewText(content)) - res, err := LgrClient.SendGroupMessage(uint32(groupIdInt), msg) - if err != nil { - return nil, err - } - return &resource.Message{ - Id: strconv.Itoa(int(res.Id)), - Content: res.ToString(), - Channel: nil, - Group: nil, - Member: nil, - User: nil, - CreatedAt: time.Unix(int64(res.Time), 0), - UpdatedAt: time.Time{}, - MessageElements: ConvertIceElement(res.Elements), - }, nil - } -} - -func (b BotLagrange) GetMessage(channelId string, messageId string) (*resource.Message, error) { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RecallMessage(channelId string, messageId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) UpdateMessage(channelId string, messageId string, content string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetMessageList(channelId string, limit uint32, order bool) []*resource.Message { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetMessageListByRange(channelId string, messageId string, start uint32, count uint32) []*resource.Message { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) CreateReaction(channelId string, messageId string, emoji string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) DeleteReaction(channelId string, messageId string, emoji string, userId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) ClearReaction(channelId string, messageId string, emoji string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetReactionList(channelId string, messageId string, emoji string) []resource.User { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetReactionListByToken(channelId string, messageId string, emoji string, next string) *resource.PagedList { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetUser(userId string) *resource.User { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetFriendList() []*resource.User { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) ApproveFriendRequest(messageId string, approve string, comment string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) Send(groupId string, channelId string, elements []elements.IceinuMessageElement) (*resource.Message, error) { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) SendSatori(groupId string, channelId string, satori string) (*resource.Message, error) { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) SendPoke(groupId string, channelId string, userId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetSelfUserId() string { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetSelfUserName() string { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetSelfAvatarUrl() string { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetSeldUserNickname() string { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetGroupAvatarUrl(groupId string) string { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RefreshUserListCache() error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RefreshGroupListCache() error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RefreshGroupMemberCache(groupId string, userId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RefreshGroupAllMembersCache(groupId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RefreshChannelListCache(groupId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RenameGroup(groupId string, name string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RenameGroupMember(groupId string, userId string, nickname string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RemarkGroup(groupId string, remark string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) SetGroupGlobalMute(groupId string, mute bool) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) LeaveGroup(groupId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) SetGroupMemberTitle(groupId string, userId string, title string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) UploadChannelFile(channelId string, filePath string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) UploadGroupFile(groupId string, filePath string, targetFilePath string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetGroupFileSystemInfo(groupId string) *resource.GroupFileSystemInfo { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetGroupFilesByFolder(groupId string, folderId string) interface{} { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetGroupRootFiles(groupId string) interface{} { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) MoveGroupFile(groupId string, fileId string, parentFolder string, targetFolderId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) DeleteGroupFile(groupId string, fileId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) CreateGroupFolder(groupId string, folderName string, parentFolder string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) RenameGroupFolder(groupId string, folderId string, newFolderName string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) DeleteGroupFolder(groupId string, folderId string) error { - //TODO implement me - panic("implement me") -} - -func (b BotLagrange) GetOriginalClient() interface{} { - //TODO implement me - panic("implement me") -} - -func GetBot() *BotLagrange { - return &BotLagrange{} -} diff --git a/adapter/lagrange/lagrange_config.go b/adapter/lagrange/lagrange_config.go deleted file mode 100644 index a035aac..0000000 --- a/adapter/lagrange/lagrange_config.go +++ /dev/null @@ -1,21 +0,0 @@ -package lagrange - -import "github.com/Iceinu-Project/iceinu/config" - -type AdapterLagrangeConfig struct { - Account int `toml:"account"` - Password string `toml:"password"` - SignUrl string `toml:"sign_url"` - MusicSignUrl string `toml:"music_sign_url"` -} - -var manager = config.GetManager() - -func RegisterConfig() { - manager.InitConfig("lagrange.toml", &AdapterLagrangeConfig{ - Account: 0, - Password: "", - SignUrl: "https://sign.lagrangecore.org/api/sign/25765", - MusicSignUrl: "", - }) -} diff --git a/adapter/lagrange/lagrange_handler.go b/adapter/lagrange/lagrange_handler.go deleted file mode 100644 index 17e7ab2..0000000 --- a/adapter/lagrange/lagrange_handler.go +++ /dev/null @@ -1,97 +0,0 @@ -package lagrange - -import ( - "fmt" - "strconv" - "time" - - "github.com/LagrangeDev/LagrangeGo/client" - "github.com/LagrangeDev/LagrangeGo/message" - - "github.com/Iceinu-Project/iceinu/ice" - "github.com/Iceinu-Project/iceinu/logger" - "github.com/Iceinu-Project/iceinu/resource" - "github.com/Iceinu-Project/iceinu/utils" -) - -func SetAllHandler() { - Manager.RegisterPrivateMessageHandler(func(client *client.QQClient, event *message.PrivateMessage) { - self, _ := client.FetchUserInfoUin(client.Uin) - e := ice.PlatformEvent{ - EventId: uint64(event.Id), - EventType: "PrivateMessageEvent", - Platform: "QQNT", - SelfId: strconv.Itoa(int(client.Uin)), - Timestamp: time.Unix(int64(event.Time), 0), - Group: &resource.Group{ - Id: "", - Name: "", - Avatar: "", - }, - Channel: &resource.Channel{ - Id: strconv.Itoa(int(event.Sender.Uin)), - Type: 1, - Name: event.Sender.Uid, - ParentId: "", - }, - Message: &resource.Message{ - Id: strconv.Itoa(int(event.InternalId)), - Content: event.ToString(), - MessageElements: ConvertIceElement(event.Elements), - }, - Operator: &resource.User{ - Id: strconv.Itoa(int(event.Sender.Uin)), - Name: event.Sender.Uid, - Nickname: event.Sender.Nickname, - Avatar: self.Avatar, - IsBot: false, - }, - User: &resource.User{ - Id: strconv.Itoa(int(event.Target)), - Name: client.GetUid(client.Uin), - Nickname: client.NickName(), - Avatar: self.Avatar, - IsBot: false, - }, - } - logger.Infof("[私聊][%s]%s:%s", e.Operator.Id, e.Operator.Nickname, utils.SatorizeIceElements(e.Message.MessageElements)) - ice.Bus.Publish("PrivateMessageEvent", &e) - }) - Manager.RegisterGroupMessageHandler(func(client *client.QQClient, event *message.GroupMessage) { - groupinfo := client.GetCachedGroupInfo(event.GroupUin) - fmt.Println(groupinfo) - self, _ := client.FetchUserInfoUin(client.Uin) - e := ice.PlatformEvent{ - EventId: uint64(event.Id), - EventType: "GroupMessageEvent", - Platform: "QQNT", - SelfId: strconv.Itoa(int(client.Uin)), - Timestamp: time.Unix(int64(event.Time), 0), - Channel: &resource.Channel{ - Id: strconv.Itoa(int(event.GroupUin)), - Type: 0, - Name: event.GroupName, - ParentId: "", - }, - Group: &resource.Group{ - Id: strconv.Itoa(int(event.GroupUin)), - Name: event.GroupName, - Avatar: groupinfo.Avatar, - }, - Message: &resource.Message{ - Id: strconv.Itoa(int(event.InternalId)), - Content: event.ToString(), - MessageElements: ConvertIceElement(event.Elements), - }, - Operator: &resource.User{ - Id: strconv.Itoa(int(event.Sender.Uin)), - Name: event.Sender.Uid, - Nickname: event.Sender.Nickname, - Avatar: self.Avatar, - IsBot: false, - }, - } - logger.Infof("[群聊][来自群%s][%s]%s:%s", e.Group.Id, e.Operator.Id, e.Operator.Nickname, utils.SatorizeIceElements(e.Message.MessageElements)) - ice.Bus.Publish("GroupMessageEvent", &e) - }) -} diff --git a/adapters/adapter.go b/adapters/adapter.go new file mode 100644 index 0000000..f2a3d94 --- /dev/null +++ b/adapters/adapter.go @@ -0,0 +1,39 @@ +package adapters + +// IceinuAdapter Iceinu的适配器接口,编写适配器时需要实现这个接口 +type IceinuAdapter interface { + Init() error // 初始化适配器 + SubscribeEvents() error // 订阅事件,用于操作适配器客户端 + Start() error // 启动适配器 + GetAdapterInfo() *AdapterInfo // 获取适配器元信息 + GetUserTree() *UserTree // 获取完整用户树 +} + +// AdapterInfo 适配器元信息 +type AdapterInfo struct { + Name string // 适配器名称 + Version string // 适配器版本 + Model string // 适配器模型 + Platform []string // 适配器平台 + Author []string // 适配器作者 + License []string // 适配器许可证 + Repo string // 适配器仓库地址 + Introduce string // 适配器简介 +} + +// UserTree 用户树结构 +// +// Iceinu由于本身设计了分布式/集群式的架构,所以需要保证各个节点不会重复处理数据,这需要维护一个用户树结构 +// +// 简而言之,每个适配器和客户端连接时都会将客户端的频道/群组/好友信息处理成用户树结构,这个用户树结构会被上传到主节点 +// +// 主节点会将所有适配器的用户树结构根据优先级合并成一个完整的用户树结构,广播给每个子节点,从而限制每个节点可以处理的用户范围 +// +// 当节点创建新连接/失去可用性/更新用户数据时会重新向主节点发送用户树结构,主节点会根据用户树结构更新节点的用户范围 +type UserTree struct { + SelfId string // 适配器自身ID + Platform string // 平台 + Users []string // 用户列表 + Groups []string // 群组列表 + Channels []string // 频道列表 +} diff --git a/adapters/bot.go b/adapters/bot.go new file mode 100644 index 0000000..5f9bc45 --- /dev/null +++ b/adapters/bot.go @@ -0,0 +1 @@ +package adapters diff --git a/adapters/lagrange/README.md b/adapters/lagrange/README.md new file mode 100644 index 0000000..84768ef --- /dev/null +++ b/adapters/lagrange/README.md @@ -0,0 +1,3 @@ +# LagrangeGo适配器 + +Iceinu内置的NTQQ适配器,详细文档参见:https://iceinu-project.github.io/adapters/lagrange_go.html \ No newline at end of file diff --git a/adapters/lagrange/event_publisher.go b/adapters/lagrange/event_publisher.go new file mode 100644 index 0000000..678fa04 --- /dev/null +++ b/adapters/lagrange/event_publisher.go @@ -0,0 +1,148 @@ +package lagrange + +import ( + "github.com/Iceinu-Project/Iceinu/ice" + "github.com/Iceinu-Project/Iceinu/models/satori" + "github.com/LagrangeDev/LagrangeGo/client" + "github.com/LagrangeDev/LagrangeGo/message" + "strconv" + "time" +) + +func BindEvents() { + // 注册私聊事件 + Manager.RegisterPrivateMessageHandler(func(client *client.QQClient, event *message.PrivateMessage) { + // 尝试从适配器的缓存中获取自身信息 + selfInfo := GetSelfInfoInCache(client) + // 尝试从适配器的缓存中获取好友信息 + friendInfo := GetFriendsDataInCache(client) + ice.Publish(&ice.IceinuEvent{ + Type: 10, + From: ice.GetSelfNodeId(), + Target: ice.GetMasterNodeId(), + Timestamp: time.Now().Unix(), + Summary: "PrivateMessageEvent", + Event: &satori.EventSatori{ + Id: uint64(event.Id), + Type: "PrivateMessageEvent", + Platform: "QQNT", + SelfId: strconv.Itoa(int(client.Uin)), + Timestamp: int64(event.Time), + Argv: nil, + Button: nil, + Channel: &satori.Channel{ + Id: strconv.Itoa(int(event.Sender.Uin)), + Type: 1, + Name: event.Sender.Uid, + ParentId: "", + }, + Group: &satori.Group{ + Id: "", + Name: "", + Avatar: "", + Maxcount: 0, + MemberCount: 0, + }, + Login: nil, + Member: nil, + Message: &satori.Message{ + Id: strconv.Itoa(int(event.InternalId)), + Content: event.ToString(), + Channel: nil, + Group: nil, + Member: nil, + User: nil, + CreatedAt: int64(event.Time), + UpdatedAt: int64(event.Time), + MessageElements: ToSatoriElements(event.Elements), + }, + Operator: &satori.User{ + Id: strconv.Itoa(int(event.Sender.Uin)), + Name: event.Sender.CardName, + Nickname: event.Sender.Nickname, + Avatar: friendInfo[event.Sender.Uin].Avatar, + IsBot: false, + }, + Role: nil, + User: &satori.User{ + Id: strconv.Itoa(int(event.Self)), + Name: client.NickName(), + Nickname: client.NickName(), + Avatar: selfInfo.Avatar, + IsBot: false, + }, + }, + }) + // 将LagrangeGo的消息存入消息缓存 + err := Cache.Set(strconv.Itoa(int(event.InternalId)), event) + if err != nil { + return + } + }) + // 注册群聊事件 + Manager.RegisterGroupMessageHandler(func(client *client.QQClient, event *message.GroupMessage) { + // 尝试从LagrangeGo的内置缓存中获取群信息 + groupInfo := client.GetCachedGroupInfo(event.GroupUin) + // 尝试从适配器的缓存中获取自身信息 + selfInfo := GetSelfInfoInCache(client) + // 尝试从适配器的缓存中获取群成员映射 + groupMemberData := GetGroupMembersDataInCache(client, event.GroupUin) + ice.Publish(&ice.IceinuEvent{ + Type: 10, + From: ice.GetSelfNodeId(), + Target: ice.GetMasterNodeId(), + Timestamp: time.Now().Unix(), + Summary: "GroupMessageEvent", + Event: &satori.EventSatori{ + Id: uint64(event.Id), + Type: "GroupMessageEvent", + Platform: "QQNT", + SelfId: strconv.Itoa(int(client.Uin)), + Timestamp: int64(event.Time), + Argv: nil, + Button: nil, + Channel: &satori.Channel{ + Id: strconv.Itoa(int(event.GroupUin)), + Type: 0, + Name: event.GroupName, + ParentId: "", + }, + Group: &satori.Group{ + Id: strconv.Itoa(int(event.GroupUin)), + Name: event.GroupName, + Avatar: groupInfo.Avatar, + Maxcount: groupInfo.MaxMember, + MemberCount: groupInfo.MemberCount, + }, + Login: nil, + Member: nil, + Message: &satori.Message{ + Id: strconv.Itoa(int(event.InternalId)), + Content: event.ToString(), + Channel: nil, + Group: nil, + Member: nil, + User: nil, + CreatedAt: int64(event.Time), + UpdatedAt: int64(event.Time), + MessageElements: ToSatoriElements(event.Elements), + }, + Operator: &satori.User{ + Id: strconv.Itoa(int(event.Sender.Uin)), + Name: event.Sender.CardName, + Nickname: event.Sender.Nickname, + Avatar: groupMemberData[event.Sender.Uin].Avatar, + IsBot: false, + }, + Role: nil, + User: &satori.User{ + Id: strconv.Itoa(int(client.Uin)), + Name: client.NickName(), + Nickname: client.NickName(), + Avatar: selfInfo.Avatar, + IsBot: false, + }, + }, + }) + }) +} diff --git a/adapters/lagrange/lagrange.go b/adapters/lagrange/lagrange.go new file mode 100644 index 0000000..5ba9795 --- /dev/null +++ b/adapters/lagrange/lagrange.go @@ -0,0 +1,138 @@ +package lagrange + +import ( + "github.com/Iceinu-Project/Iceinu/adapters" + "github.com/Iceinu-Project/Iceinu/cache" + "github.com/Iceinu-Project/Iceinu/ice" + "github.com/Iceinu-Project/Iceinu/log" + "github.com/LagrangeDev/LagrangeGo/client" + "github.com/LagrangeDev/LagrangeGo/client/auth" + "os" + "os/signal" + "strconv" + "syscall" +) + +// InfosLagrangeAdapter LagrangeGo适配器元信息 +var InfosLagrangeAdapter = adapters.AdapterInfo{ + Name: "LagrangeGo适配器", + Version: "1.0.0", + Model: "Satori", + Platform: []string{"NTQQ"}, + Author: []string{"Kyoku"}, + License: []string{"MIT License"}, + Repo: "https://github.com/Iceinu-Project/Iceinu", + Introduce: "内置LagrangeGo的NTQQ适配器,无需外置协议端程序", +} + +// Client LagrangeGo客户端实例 +var Client *client.QQClient + +// Cache 消息缓存 +var Cache *cache.IceCacheManager + +// AdapterLagrangeGo LagrangeGo适配器 +type AdapterLagrangeGo struct{} + +func (a *AdapterLagrangeGo) Init() error { + // 读取配置文件 + AdapterConfigInit() + // 初始化消息缓存 + log.Debug("正在初始化LagrangeGo的消息缓存...") + Cache = cache.NewIceCacheManager(AdapterLagrangeConf.CacheSize, AdapterLagrangeConf.CacheExpire) + // 日志输出 + appInfo := auth.AppList["linux"]["3.2.10-25765"] + deviceInfo := auth.NewDeviceInfo(AdapterLagrangeConf.Lagrange.Account) + qqClientInstance := client.NewClient(uint32(AdapterLagrangeConf.Lagrange.Account), appInfo, AdapterLagrangeConf.Lagrange.SignServer) + qqClientInstance.SetLogger(GetProtocolLogger()) + qqClientInstance.UseDevice(deviceInfo) + + // 尝试读取签名文件 + data, err := os.ReadFile("signature.bin") + if err != nil { + log.Warn("读取签名文件时发生错误:", err) + } else { + sig, err := auth.UnmarshalSigInfo(data, true) + if err != nil { + log.Warn("加载签名文件时发生错误:", err) + } else { + qqClientInstance.UseSig(sig) + } + } + // 保存Client实例 + Client = qqClientInstance + return nil +} + +func (a *AdapterLagrangeGo) SubscribeEvents() error { + BindEvents() + return nil +} + +func (a *AdapterLagrangeGo) Start() error { + // 在函数结束时释放Client并尝试保存签名 + defer Client.Release() + defer SaveSignature() + // 事件订阅 + err := a.SubscribeEvents() + if err != nil { + return err + } + SetAllSubscribes() + // 登录 + err = Login() + if err != nil { + return err + } + // 推送适配器连接事件 + ice.MakeAdapterConnectEvent(InfosLagrangeAdapter.Name, InfosLagrangeAdapter.Model, strconv.Itoa(int(Client.Uin)), Client.NickName()) + + // 主协程关闭通道 + mc := make(chan os.Signal, 2) + signal.Notify(mc, os.Interrupt, syscall.SIGTERM) + for { + switch <-mc { + case os.Interrupt, syscall.SIGTERM: + return nil + } + } +} + +func (a *AdapterLagrangeGo) GetAdapterInfo() *adapters.AdapterInfo { + return &InfosLagrangeAdapter +} + +func (a *AdapterLagrangeGo) GetUserTree() *adapters.UserTree { + //TODO implement me + panic("implement me") +} + +// Login 登录 +func Login() error { + // 声明 err 变量并进行错误处理 + err := Client.Login("", "qrcode.png") + if err != nil { + log.Error("登录时发生错误:", err) + return err + } + return nil +} + +// SaveSignature 保存sign信息 +func SaveSignature() { + data, err := Client.Sig().Marshal() + if err != nil { + log.Error("生成签名文件时发生错误err:", err) + return + } + err = os.WriteFile("signature.bin", data, 0644) + if err != nil { + log.Error("写入签名文件时发生错误 err:", err) + return + } + log.Info("签名已被写入签名文件") +} + +func GetAdapter() adapters.IceinuAdapter { + return &AdapterLagrangeGo{} +} diff --git a/adapters/lagrange/lagrange_config.go b/adapters/lagrange/lagrange_config.go new file mode 100644 index 0000000..d832f26 --- /dev/null +++ b/adapters/lagrange/lagrange_config.go @@ -0,0 +1,41 @@ +package lagrange + +import ( + "github.com/Iceinu-Project/Iceinu/config" + "github.com/Iceinu-Project/Iceinu/log" +) + +var AdapterLagrangeConf *AdapterConfig + +// AdapterConfig 适配器配置 +type AdapterConfig struct { + CacheSize uint32 `toml:"message_cache_size"` // 消息缓存大小 + CacheExpire uint32 `toml:"message_cache_expire"` // 消息缓存过期时间 + Lagrange LgrConfig `toml:"lagrange"` +} + +type LgrConfig struct { + SignServer string `toml:"sign_server"` + MusicSignServer string `toml:"music_sign_server"` + Account int `toml:"account"` + Password string `toml:"password"` +} + +// AdapterConfigInit 初始化适配器配置 +func AdapterConfigInit() { + AdapterLagrangeConf = &AdapterConfig{ + CacheSize: 200, + CacheExpire: 600, + Lagrange: LgrConfig{ + SignServer: "https://sign.lagrangecore.org/api/sign/25765", + MusicSignServer: "", + Account: 0, + Password: "", + }, + } + err := config.ProcessConfig(AdapterLagrangeConf, "lagrange_config.toml") + if err != nil { + log.Errorf("初始化LagrangeGo适配器配置文件失败: %v", err) + return + } +} diff --git a/logger/protocol_logger.go b/adapters/lagrange/protocol_logger.go similarity index 63% rename from logger/protocol_logger.go rename to adapters/lagrange/protocol_logger.go index 94709d8..e0f6cd0 100644 --- a/logger/protocol_logger.go +++ b/adapters/lagrange/protocol_logger.go @@ -1,7 +1,9 @@ -package logger +package lagrange import ( "fmt" + gradient "github.com/Iceinu-Project/IceGradient" + "github.com/Iceinu-Project/Iceinu/log" "os" "path" "time" @@ -12,22 +14,22 @@ type ProtocolLogger struct{} var dumpspath = "dump" -const fromProtocol = "LGR | " +const fromProtocol = "LGR | " + gradient.DarkGray func (p ProtocolLogger) Info(format string, arg ...any) { - Infof(fromProtocol+format, arg...) + log.Infof(fromProtocol+format, arg...) } func (p ProtocolLogger) Warning(format string, arg ...any) { - Warnf(fromProtocol+format, arg...) + log.Warnf(fromProtocol+format, arg...) } func (p ProtocolLogger) Debug(format string, arg ...any) { - Debugf(fromProtocol+format, arg...) + log.Debugf(fromProtocol+format, arg...) } func (p ProtocolLogger) Error(format string, arg ...any) { - Errorf(fromProtocol+format, arg...) + log.Errorf(fromProtocol+format, arg...) } func (p ProtocolLogger) Dump(data []byte, format string, arg ...any) { @@ -35,12 +37,12 @@ func (p ProtocolLogger) Dump(data []byte, format string, arg ...any) { if _, err := os.Stat(dumpspath); err != nil { err = os.MkdirAll(dumpspath, 0o755) if err != nil { - Errorf("出现错误 %v. 详细信息转储失败", message) + log.Errorf("出现错误 %v. 详细信息转储失败", message) return } } dumpFile := path.Join(dumpspath, fmt.Sprintf("%v.dump", time.Now().Unix())) - Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile) + log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile) _ = os.WriteFile(dumpFile, data, 0o644) } diff --git a/adapter/lagrange/lagrange_subscriber.go b/adapters/lagrange/subscribe_manager.go similarity index 83% rename from adapter/lagrange/lagrange_subscriber.go rename to adapters/lagrange/subscribe_manager.go index a922704..2436768 100644 --- a/adapter/lagrange/lagrange_subscriber.go +++ b/adapters/lagrange/subscribe_manager.go @@ -178,133 +178,133 @@ func (sm *SubscribeManager) RegisterGroupNotifyEventHandler(handler GroupNotifyE // SetAllSubscribes 设置所有订阅处理 func SetAllSubscribes() { - LgrClient.QQClient.PrivateMessageEvent.Subscribe(func(client *client.QQClient, event *message.PrivateMessage) { + Client.PrivateMessageEvent.Subscribe(func(client *client.QQClient, event *message.PrivateMessage) { for _, handler := range Manager.privateMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMessageEvent.Subscribe(func(client *client.QQClient, event *message.GroupMessage) { + Client.GroupMessageEvent.Subscribe(func(client *client.QQClient, event *message.GroupMessage) { for _, handler := range Manager.groupMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.TempMessageEvent.Subscribe(func(client *client.QQClient, event *message.TempMessage) { + Client.TempMessageEvent.Subscribe(func(client *client.QQClient, event *message.TempMessage) { for _, handler := range Manager.tempMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.SelfPrivateMessageEvent.Subscribe(func(client *client.QQClient, event *message.PrivateMessage) { + Client.SelfPrivateMessageEvent.Subscribe(func(client *client.QQClient, event *message.PrivateMessage) { for _, handler := range Manager.selfPrivateMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.SelfGroupMessageEvent.Subscribe(func(client *client.QQClient, event *message.GroupMessage) { + Client.SelfGroupMessageEvent.Subscribe(func(client *client.QQClient, event *message.GroupMessage) { for _, handler := range Manager.selfGroupMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.SelfTempMessageEvent.Subscribe(func(client *client.QQClient, event *message.TempMessage) { + Client.SelfTempMessageEvent.Subscribe(func(client *client.QQClient, event *message.TempMessage) { for _, handler := range Manager.selfTempMessageHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupJoinEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberIncrease) { + Client.GroupJoinEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberIncrease) { for _, handler := range Manager.groupJoinEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupLeaveEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberDecrease) { + Client.GroupLeaveEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberDecrease) { for _, handler := range Manager.groupLeaveEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupInvitedEvent.Subscribe(func(client *client.QQClient, event *event.GroupInvite) { + Client.GroupInvitedEvent.Subscribe(func(client *client.QQClient, event *event.GroupInvite) { for _, handler := range Manager.groupInviteEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMemberJoinRequestEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberJoinRequest) { + Client.GroupMemberJoinRequestEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberJoinRequest) { for _, handler := range Manager.groupMemberJoinRequestEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMemberJoinEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberIncrease) { + Client.GroupMemberJoinEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberIncrease) { for _, handler := range Manager.groupMemberJoinEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMemberLeaveEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberDecrease) { + Client.GroupMemberLeaveEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberDecrease) { for _, handler := range Manager.groupMemberLeaveEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMuteEvent.Subscribe(func(client *client.QQClient, event *event.GroupMute) { + Client.GroupMuteEvent.Subscribe(func(client *client.QQClient, event *event.GroupMute) { for _, handler := range Manager.groupMuteEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupRecallEvent.Subscribe(func(client *client.QQClient, event *event.GroupRecall) { + Client.GroupRecallEvent.Subscribe(func(client *client.QQClient, event *event.GroupRecall) { for _, handler := range Manager.groupRecallEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupMemberPermissionChangedEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberPermissionChanged) { + Client.GroupMemberPermissionChangedEvent.Subscribe(func(client *client.QQClient, event *event.GroupMemberPermissionChanged) { for _, handler := range Manager.groupMemberPermissionChangedEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupNameUpdatedEvent.Subscribe(func(client *client.QQClient, event *event.GroupNameUpdated) { + Client.GroupNameUpdatedEvent.Subscribe(func(client *client.QQClient, event *event.GroupNameUpdated) { for _, handler := range Manager.groupNameUpdatedEventHandlers { handler(client, event) } }) - LgrClient.QQClient.MemberSpecialTitleUpdatedEvent.Subscribe(func(client *client.QQClient, event *event.MemberSpecialTitleUpdated) { + Client.MemberSpecialTitleUpdatedEvent.Subscribe(func(client *client.QQClient, event *event.MemberSpecialTitleUpdated) { for _, handler := range Manager.memberSpecialTitleUpdatedEventHandlers { handler(client, event) } }) - LgrClient.QQClient.NewFriendRequestEvent.Subscribe(func(client *client.QQClient, event *event.NewFriendRequest) { + Client.NewFriendRequestEvent.Subscribe(func(client *client.QQClient, event *event.NewFriendRequest) { for _, handler := range Manager.newFriendRequestHandlers { handler(client, event) } }) - LgrClient.QQClient.FriendRecallEvent.Subscribe(func(client *client.QQClient, event *event.FriendRecall) { + Client.FriendRecallEvent.Subscribe(func(client *client.QQClient, event *event.FriendRecall) { for _, handler := range Manager.friendRecallEventHandlers { handler(client, event) } }) - LgrClient.QQClient.RenameEvent.Subscribe(func(client *client.QQClient, event *event.Rename) { + Client.RenameEvent.Subscribe(func(client *client.QQClient, event *event.Rename) { for _, handler := range Manager.renameEventHandlers { handler(client, event) } }) - LgrClient.QQClient.FriendNotifyEvent.Subscribe(func(client *client.QQClient, event event.INotifyEvent) { + Client.FriendNotifyEvent.Subscribe(func(client *client.QQClient, event event.INotifyEvent) { for _, handler := range Manager.friendNotifyEventHandlers { handler(client, event) } }) - LgrClient.QQClient.GroupNotifyEvent.Subscribe(func(client *client.QQClient, event event.INotifyEvent) { + Client.GroupNotifyEvent.Subscribe(func(client *client.QQClient, event event.INotifyEvent) { for _, handler := range Manager.groupNotifyEventHandlers { handler(client, event) } diff --git a/adapters/lagrange/tools.go b/adapters/lagrange/tools.go new file mode 100644 index 0000000..ada0b32 --- /dev/null +++ b/adapters/lagrange/tools.go @@ -0,0 +1,174 @@ +package lagrange + +import ( + "fmt" + "github.com/Iceinu-Project/Iceinu/log" + "github.com/Iceinu-Project/Iceinu/models/satori" + "github.com/LagrangeDev/LagrangeGo/client" + "github.com/LagrangeDev/LagrangeGo/client/entity" + "github.com/LagrangeDev/LagrangeGo/message" + "strconv" + "strings" +) + +// ToSatoriElements 将LagrangeGo的消息元素切片转换为Satori的消息元素切片 +func ToSatoriElements(elements []message.IMessageElement) *[]satori.ElementSatori { + // 创建存储Satori消息切片的变量 + var result []satori.ElementSatori + // 遍历传入的元素 + for _, ele := range elements { + // 通过消息元素的Type方法来确定消息类型 + switch ele.Type() { + // 将元素依次对应转换并传入 + case message.Text: + ele := ele.(*message.TextElement) + // 检测文本中是否包含换行符 + if strings.Contains(ele.Content, "\n") { + // 如果包含换行符,进行拆分和处理 + textParts := strings.Split(ele.Content, "\n") + for i, part := range textParts { + // 将每段文本添加到 result + result = append(result, &satori.TextElement{Text: part}) + // 如果不是最后一段文本,则插入 BrElement + if i < len(textParts)-1 { + result = append(result, &satori.BrElement{}) + } + } + } else { + // 如果不包含换行符,直接添加文本元素 + result = append(result, &satori.TextElement{Text: ele.Content}) + } + case message.At: + ele := ele.(*message.AtElement) + result = append(result, &satori.AtElement{ + Id: strconv.Itoa(int(ele.TargetUin)), + Name: ele.Display, + Role: "", + Type: strconv.Itoa(int(ele.Type())), + }) + case message.Face: + ele := ele.(*message.FaceElement) + result = append(result, &satori.FaceElement{ + Id: ele.FaceID, + }) + case message.Voice: + ele := ele.(*message.VoiceElement) + result = append(result, &satori.AudioElement{ + Src: ele.Url, + Title: ele.Name, + Duration: ele.Size, + Poster: "", + Cache: false, + Timeout: 0, + }) + case message.Image: + ele := ele.(*message.ImageElement) + result = append(result, &satori.ImgElement{ + Src: ele.Url, + Width: ele.Width, + Height: ele.Height, + Title: ele.ImageId, + Cache: false, + Timeout: 0, + }) + case message.File: + ele := ele.(*message.FileElement) + result = append(result, &satori.FileElement{ + Src: ele.FileUrl, + Title: ele.FileName, + Poster: "", + Cache: false, + Timeout: 0, + }) + case message.Reply: + ele := ele.(*message.ReplyElement) + result = append(result, &satori.QuoteElement{ + Id: strconv.Itoa(int(ele.SenderUin)), + Name: ele.SenderUid, + ChannelId: strconv.Itoa(int(ele.GroupUin)), + GroupId: strconv.Itoa(int(ele.GroupUin)), + Timestamp: int64(ele.Time), + Elements: ToSatoriElements(ele.Elements), + }) + case message.Forward: + ele := ele.(*message.ForwardMessage) + result = append(result, &satori.MessageElement{ + Forward: true, + Elements: UnzipNodes(ele.Nodes), + }) + + default: + result = append(result, &satori.UnsupportedElement{Type: strconv.Itoa(int(ele.Type()))}) + + } + } + return &result +} + +func UnzipNodes(n []*message.ForwardNode) *[]satori.ElementSatori { + var result []satori.ElementSatori + for _, node := range n { + result = append(result, &satori.NodeElement{ + GroupId: node.GroupId, + SenderId: node.SenderId, + SenderName: node.SenderName, + Time: node.Time, + Message: ToSatoriElements(node.Message), + }) + } + return &result +} + +// GetGroupMembersDataInCache 从缓存中获取群成员映射,如果缓存中没有则拉取并存入缓存 +func GetGroupMembersDataInCache(client *client.QQClient, groupId uint32) map[uint32]*entity.GroupMember { + // 尝试在内置缓存中查找群成员映射 + var groupMemberMap map[uint32]*entity.GroupMember + err := Cache.Get(fmt.Sprintf("group_member_data_%d", groupId), &groupMemberMap) + if err != nil { + // 如果缓存中没有群成员映射,则创建一个 + groupMembersData, err := client.GetGroupMembersData(groupId) + if err != nil { + log.Warn("无法获取群成员列表数据:", err) + } + err = Cache.Set(fmt.Sprintf("group_member_data_%d", groupId), groupMembersData) + if err != nil { + log.Warn("无法缓存群成员映射数据:", err) + } + groupMemberMap = groupMembersData + log.Debugf("%v群成员缓存数据更新完成,共%d个成员", groupId, len(groupMemberMap)) + } + return groupMemberMap +} + +// GetFriendsDataInCache 从缓存中获取好友映射,如果缓存中没有则拉取并存入缓存 +func GetFriendsDataInCache(client *client.QQClient) map[uint32]*entity.Friend { + // 尝试在内置缓存中查找好友映射 + var friendMap map[uint32]*entity.Friend + err := Cache.Get("friend_data", &friendMap) + if err != nil { + // 如果缓存中没有好友映射,则创建一个 + friendData, err := client.GetFriendsData() + if err != nil { + log.Warn("无法获取好友列表数据:", err) + } + err = Cache.Set("friend_data", friendData) + if err != nil { + log.Warn("无法缓存好友映射数据:", err) + } + friendMap = friendData + log.Debugf("好友缓存数据更新完成,共%d个好友", len(friendMap)) + } + return friendMap +} + +// GetSelfInfoInCache 从缓存中获取自身信息,如果缓存中没有则拉取并存入缓存 +func GetSelfInfoInCache(client *client.QQClient) *entity.Friend { + // 尝试在内置缓存中查找自身信息 + friendData := GetFriendsDataInCache(client) + selfInfo, ok := friendData[client.Uin] + if !ok { + log.Warn("无法获取自身信息") + return nil + } + return selfInfo +} diff --git a/adapters/onebot/README.md b/adapters/onebot/README.md new file mode 100644 index 0000000..e6612f8 --- /dev/null +++ b/adapters/onebot/README.md @@ -0,0 +1 @@ +饼先画上再说,暂时先主要支持LagrangeGo适配器和Satori适配器的接入,后续再逐步完善其他适配器的接入。 diff --git a/adapters/satori/README.md b/adapters/satori/README.md new file mode 100644 index 0000000..e6612f8 --- /dev/null +++ b/adapters/satori/README.md @@ -0,0 +1 @@ +饼先画上再说,暂时先主要支持LagrangeGo适配器和Satori适配器的接入,后续再逐步完善其他适配器的接入。 diff --git a/cache/cache_manager.go b/cache/cache_manager.go new file mode 100644 index 0000000..9a456d2 --- /dev/null +++ b/cache/cache_manager.go @@ -0,0 +1,105 @@ +package cache + +import ( + "bytes" + "encoding/gob" + "github.com/Iceinu-Project/Iceinu/log" + "github.com/coocood/freecache" +) + +// IceCacheManager Iceinu的缓存管理器实例,实现了一系列的缓存管理方法,便于直接使用 +type IceCacheManager struct { + MaxSize uint32 // 缓存最大容量,单位为MB + ExpireTime uint32 // 缓存过期时间,单位为秒 + Cache *freecache.Cache // 缓存实例 +} + +// NewIceCacheManager 创建新的缓存管理器实例 +func NewIceCacheManager(maxSize, expireTime uint32) *IceCacheManager { + log.Debugf("缓存管理器初始化完成,最大容量:%dMB,过期时间:%d秒", maxSize, expireTime) + return &IceCacheManager{ + MaxSize: maxSize * 1024 * 1024, + ExpireTime: expireTime, + Cache: freecache.NewCache(int(maxSize * 1024 * 1024)), + } +} + +// GetCache 直接获取缓存实例 +func (icm *IceCacheManager) GetCache() *freecache.Cache { + return icm.Cache +} + +// Set 设置缓存数据 +func (icm *IceCacheManager) Set(key string, value interface{}) error { + // 创建一个字节缓冲区 + var buf bytes.Buffer + + // 创建一个新的编码器,并将值编码到缓冲区中 + enc := gob.NewEncoder(&buf) + err := enc.Encode(value) + if err != nil { + return err + } + + // 将序列化后的数据存储到缓存中 + return icm.Cache.Set([]byte(key), buf.Bytes(), int(icm.ExpireTime)) +} + +// Get 获取缓存数据,value 必须是指针类型以便解码后进行填充 +func (icm *IceCacheManager) Get(key string, value interface{}) error { + // 从缓存中获取数据 + data, err := icm.Cache.Get([]byte(key)) + if err != nil { + return err + } + + // 创建一个字节缓冲区,并使用解码器解码数据到提供的 value 中 + buf := bytes.NewBuffer(data) + dec := gob.NewDecoder(buf) + return dec.Decode(value) +} + +// Del 删除缓存数据 +func (icm *IceCacheManager) Del(key string) { + icm.Cache.Del([]byte(key)) +} + +// Clear 清空缓存 +func (icm *IceCacheManager) Clear() { + icm.Cache.Clear() +} + +// Update 更新缓存数据 +func (icm *IceCacheManager) Update(key string, value interface{}) error { + // 先删除旧数据 + icm.Del(key) + + // 再设置新数据 + return icm.Set(key, value) +} + +// GetMaxSize 获取缓存最大容量 +func (icm *IceCacheManager) GetMaxSize() uint32 { + return icm.MaxSize +} + +// GetExpireTime 获取缓存过期时间 +func (icm *IceCacheManager) GetExpireTime() uint32 { + return icm.ExpireTime +} + +// SetWithExpire 设置缓存数据并同时指定过期时间 +func (icm *IceCacheManager) SetWithExpire(key string, value interface{}, expire int) error { + // 创建一个字节缓冲区 + var buf bytes.Buffer + + // 创建一个新的编码器,并将值编码到缓冲区中 + enc := gob.NewEncoder(&buf) + err := enc.Encode(value) + if err != nil { + return err + } + + // 将序列化后的数据存储到缓存中 + return icm.Cache.Set([]byte(key), buf.Bytes(), expire) +} diff --git a/config/config_manager.go b/config/config_manager.go deleted file mode 100644 index a2fae5e..0000000 --- a/config/config_manager.go +++ /dev/null @@ -1,233 +0,0 @@ -package config - -import ( - "github.com/Iceinu-Project/iceinu/logger" - "github.com/pelletier/go-toml" - "os" - "path/filepath" - "reflect" - "sync" -) - -// Iceinu的配置文件处理器,除了iceinu自带的配置文件之外也负责处理适配器和插件的配置文件设置 -// 这配置文件管理器设计一大半是ChatGPT帮我优化的,o1模型真的是太斯巴拉西了 -// 总之是实现了动态的配置文件注册加载解析修正等等功能 - -// ConfManager 管理配置文件的结构体 -type ConfManager struct { - configs map[string]interface{} - defaults map[string]interface{} - mutex sync.RWMutex - configChanged bool // 标志位,指示配置文件是否有生成或修改 -} - -// manager 是全局的配置管理器实例 -var manager = NewManager() - -// NewManager 创建一个新的配置管理器 -func NewManager() *ConfManager { - logger.Debugf("正在初始化配置文件管理器...") - return &ConfManager{ - configs: make(map[string]interface{}), - defaults: make(map[string]interface{}), - } -} - -// InitConfig 注册一个配置文件和对应的结构体 -func (cm *ConfManager) InitConfig(filename string, configStruct interface{}) { - cm.mutex.Lock() - defer cm.mutex.Unlock() - cm.configs[filename] = configStruct - - // 保存默认配置的深拷贝 - defaultConfig := deepCopy(configStruct) - cm.defaults[filename] = defaultConfig - - logger.Debugf("已注册配置文件:%s", filename) -} - -// LoadConfigs 加载并解析所有已注册的配置文件,如果不存在则自动生成 -// 如果已有的配置文件缺少字段,则补全并写回,并在覆盖前备份原始文件 -func (cm *ConfManager) LoadConfigs() error { - cm.mutex.Lock() - defer cm.mutex.Unlock() - - // 获取当前工作目录 - dir, err := os.Getwd() - if err != nil { - logger.Errorf("获取工作目录失败:%v", err) - return err - } - - // 遍历已注册的配置文件 - for filename, configStruct := range cm.configs { - fullPath := filepath.Join(dir, filename) - - // 检查文件是否存在 - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - // 如果文件不存在,生成默认配置文件 - data, err := toml.Marshal(configStruct) - if err != nil { - logger.Errorf("序列化默认配置失败:%v", err) - return err - } - - err = os.WriteFile(fullPath, data, 0644) - if err != nil { - logger.Errorf("写入配置文件失败:%v", err) - return err - } - - logger.Infof("已生成配置文件:%s", fullPath) - cm.configChanged = true // 设置标志位 - } else { - // 如果文件存在,读取并解析配置文件 - data, err := os.ReadFile(fullPath) - if err != nil { - logger.Errorf("读取配置文件失败:%v", err) - return err - } - - // 创建一个新的配置实例,用于加载文件内容 - newConfig := deepCopy(cm.defaults[filename]) - - err = toml.Unmarshal(data, newConfig) - if err != nil { - logger.Errorf("解析配置文件失败:%v", err) - return err - } - - // 比较新配置和默认配置,补全缺失的字段 - changed := mergeConfig(newConfig, cm.defaults[filename]) - - // 将补全后的配置赋值回 configs - cm.configs[filename] = newConfig - - if changed { - // 在覆盖前备份原始配置文件 - backupPath := fullPath + ".backup.toml" - err = backupFile(fullPath, backupPath) - if err != nil { - logger.Errorf("备份配置文件失败:%v", err) - return err - } - - // 将完整的配置(包含默认值和新解析的值)写回配置文件 - data, err = toml.Marshal(newConfig) - if err != nil { - logger.Errorf("序列化配置文件失败:%v", err) - return err - } - - err = os.WriteFile(fullPath, data, 0644) - if err != nil { - logger.Errorf("写入配置文件失败:%v", err) - return err - } - - logger.Infof("配置文件已更新并备份:%s", fullPath) - cm.configChanged = true // 设置标志位 - } else { - logger.Debugf("配置文件已加载,无需更新:%s", fullPath) - } - } - } - - // 如果配置文件有变动,提示用户并退出程序 - if cm.configChanged { - logger.Warnf("配置文件已生成或更新,请检查配置文件后重新启动程序。") - os.Exit(0) - } - - return nil -} - -// Get 获取指定名称的配置 -func (cm *ConfManager) Get(filename string) interface{} { - cm.mutex.RLock() - defer cm.mutex.RUnlock() - return cm.configs[filename] -} - -// GetManager 获取配置管理器实例 -func GetManager() *ConfManager { - return manager -} - -// deepCopy 深拷贝一个配置结构体 -func deepCopy(src interface{}) interface{} { - // 将 src 序列化为 TOML - data, err := toml.Marshal(src) - if err != nil { - logger.Errorf("深拷贝失败:%v", err) - return nil - } - - // 创建一个新的与 src 类型相同的实例 - dst := reflect.New(reflect.TypeOf(src).Elem()).Interface() - - // 将 TOML 反序列化到 dst - err = toml.Unmarshal(data, dst) - if err != nil { - logger.Errorf("深拷贝失败:%v", err) - return nil - } - - return dst -} - -// mergeConfig 递归地将 src 中的非零值合并到 dst 中,返回是否有修改 -func mergeConfig(dst, src interface{}) bool { - dstVal := reflect.ValueOf(dst).Elem() - srcVal := reflect.ValueOf(src).Elem() - - changed := false - - for i := 0; i < dstVal.NumField(); i++ { - dstField := dstVal.Field(i) - srcField := srcVal.Field(i) - - switch dstField.Kind() { - case reflect.Struct: - if mergeConfig(dstField.Addr().Interface(), srcField.Addr().Interface()) { - changed = true - } - case reflect.Slice, reflect.Map: - if dstField.IsNil() && !srcField.IsNil() { - dstField.Set(srcField) - changed = true - } - default: - // 如果 dstField 是零值,则使用 srcField 的值 - if isZeroValue(dstField) && !isZeroValue(srcField) { - dstField.Set(srcField) - changed = true - } - } - } - - return changed -} - -// isZeroValue 判断一个值是否是零值 -func isZeroValue(v reflect.Value) bool { - return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) -} - -// backupFile 备份原始配置文件 -func backupFile(originalPath, backupPath string) error { - // 读取原始文件内容 - data, err := os.ReadFile(originalPath) - if err != nil { - return err - } - - // 写入备份文件 - err = os.WriteFile(backupPath, data, 0644) - if err != nil { - return err - } - - logger.Infof("已备份配置文件:%s", backupPath) - return nil -} diff --git a/config/iceinu_config.go b/config/iceinu_config.go index 9825991..12aa85f 100644 --- a/config/iceinu_config.go +++ b/config/iceinu_config.go @@ -1,22 +1,65 @@ package config +import "github.com/Iceinu-Project/Iceinu/log" + +var IceConf *IceinuConfig + type IceinuConfig struct { - BotName string `toml:"bot_name"` - LogLevel string `toml:"log_level"` - Database DatabaseConfig `toml:"database"` + LogLevel string `toml:"log_level"` // 日志级别 + Node NodeConfig `toml:"node"` // 节点配置 + Database DatabaseConfig `toml:"database"` // 数据库配置 + Cache CacheConfig `toml:"cache"` // 缓存配置 +} + +type NodeConfig struct { + IsEnableNode bool `toml:"is_enable_node"` // 是否启用节点连接 + Mode string `toml:"node_mode"` // 运行模式,可选值为Dist(分布式),Static(静态集群),Dynamic(动态集群) + IsMaster bool `toml:"is_master"` // 是否为主节点 + MasterURL string `toml:"master_url"` // 主节点地址 } type DatabaseConfig struct { - URL string `toml:"url"` + MaxIdleConns int `toml:"max_idle_conns"` // 数据库连接池最大空闲连接数 + MaxOpenConns int `toml:"max_open_conns"` // 数据库连接池最大连接数 + ConnMaxLifetime int `toml:"conn_max_lifetime"` // 数据库连接最大生命周期,单位为分钟 + IsEnableRemoteDatabase bool `toml:"is_enable_remote_database"` // 是否启用远程数据库 + DatabaseType string `toml:"database_type"` // 数据库类型,可选值为MySQL,PostgreSQL + DatabaseURL string `toml:"database_url"` // 数据库连接地址 } -func init() { - // 注册Iceinu的配置文件 - manager.InitConfig("iceinu.toml", &IceinuConfig{ - BotName: "Iceinu", +type CacheConfig struct { + MaxCacheSize uint32 `toml:"max_cache_size"` // 内置缓存最大容量 + CacheExpire uint32 `toml:"cache_expire"` // 内置缓存过期时间 +} + +// IceConfigInit 初始化内置配置文件 +func IceConfigInit() { + log.Debugf("正在初始化内置配置文件...") + // 初始化内置配置文件 + IceConf = &IceinuConfig{ LogLevel: "INFO", + Node: NodeConfig{ + IsEnableNode: false, + Mode: "Dist", + IsMaster: true, + MasterURL: "", + }, Database: DatabaseConfig{ - URL: "sqlite://iceinu.db", + MaxIdleConns: 10, + MaxOpenConns: 100, + ConnMaxLifetime: 60, + IsEnableRemoteDatabase: false, + DatabaseType: "PostgreSQL", + DatabaseURL: "", + }, + Cache: CacheConfig{ + MaxCacheSize: 100, + CacheExpire: 600, }, - }) + } + err := ProcessConfig(IceConf, "ice_config.toml") + if err != nil { + log.Errorf("初始化内置配置文件失败: %v", err) + return + } } diff --git a/config/manager.go b/config/manager.go new file mode 100644 index 0000000..0f58db1 --- /dev/null +++ b/config/manager.go @@ -0,0 +1,170 @@ +package config + +import ( + "encoding/json" + "github.com/Iceinu-Project/Iceinu/log" + "os" + "reflect" + "strings" + + "github.com/pelletier/go-toml" +) + +// 将结构体转换为 map +func structToMap(v interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + var m map[string]interface{} + err = json.Unmarshal(data, &m) + return m, err +} + +// structToMapWithTags converts a struct to a map with tag keys, handling nested structs +func structToMapWithTags(data interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + val := reflect.ValueOf(data).Elem() + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + tag := fieldType.Tag.Get("toml") + if tag == "" { + tag = strings.ToLower(fieldType.Name) + } + + if field.Kind() == reflect.Struct { + nestedMap, err := structToMapWithTags(field.Addr().Interface()) + if err != nil { + return nil, err + } + result[tag] = nestedMap + } else { + result[tag] = field.Interface() + } + } + + return result, nil +} + +// 读取 TOML 文件到 map +func readTomlFileToMap(filename string) (map[string]interface{}, error) { + tree, err := toml.LoadFile(filename) + if err != nil { + return nil, err + } + return tree.ToMap(), nil +} + +// 将 map 写入 TOML 文件 +func writeMapToTomlFile(m map[string]interface{}, filename string) error { + tree, err := toml.TreeFromMap(m) + if err != nil { + return err + } + tomlString := tree.String() + return os.WriteFile(filename, []byte(tomlString), 0644) +} + +// 备份文件 +func backupFile(filename string) error { + input, err := os.ReadFile(filename) + if err != nil { + return err + } + backupFilename := filename + ".backup.toml" + return os.WriteFile(backupFilename, input, 0644) +} + +// 合并两个 map,并检测是否有缺失的键 +func mergeMaps(defaultMap, fileMap map[string]interface{}) (map[string]interface{}, bool) { + mergedMap := make(map[string]interface{}) + missingKeys := false + + // 复制 defaultMap 的所有键值对到 mergedMap + for key, defVal := range defaultMap { + mergedMap[key] = defVal + } + + // 用 fileMap 的值覆盖 mergedMap,并检测缺失的键 + for key, fileVal := range fileMap { + if defVal, ok := defaultMap[key]; ok { + defSubMap, defIsMap := defVal.(map[string]interface{}) + fileSubMap, fileIsMap := fileVal.(map[string]interface{}) + if defIsMap && fileIsMap { + subMergedMap, subMissing := mergeMaps(defSubMap, fileSubMap) + mergedMap[key] = subMergedMap + if subMissing { + missingKeys = true + } + } else { + mergedMap[key] = fileVal + } + } else { + mergedMap[key] = fileVal + } + } + + // 检查是否有缺失的键 + for key := range defaultMap { + if _, ok := fileMap[key]; !ok { + missingKeys = true + break + } + } + + return mergedMap, missingKeys +} + +// ProcessConfig 处理配置文件,需要传入预先配置了默认值的结构体和配置文件名 +func ProcessConfig(cfg interface{}, filename string) error { + defaultMap, err := structToMapWithTags(cfg) + if err != nil { + return err + } + + fileMap, err := readTomlFileToMap(filename) + if err != nil { + if os.IsNotExist(err) { + // 文件不存在,生成新的配置文件 + log.Warnf("配置文件 %s 不存在,将生成新的配置文件", filename) + err = writeMapToTomlFile(defaultMap, filename) + if err != nil { + return err + } + } else { + return err + } + // 文件已生成,使用默认配置 + return nil + } + + mergedMap, missingKeys := mergeMaps(defaultMap, fileMap) + if missingKeys { + // 备份原始配置文件 + log.Warnf("配置文件 %s 缺失了键,将自动备份原始配置文件并写入新的配置文件", filename) + err = backupFile(filename) + if err != nil { + return err + } + // 写入更新后的配置文件 + err = writeMapToTomlFile(mergedMap, filename) + if err != nil { + return err + } + } + + // 将 TOML 文件解析到配置结构体 + data, err := os.ReadFile(filename) + if err != nil { + return err + } + // 打印读取到的 TOML 数据 + // println("读取到的 TOML 数据:", string(data)) + + err = toml.Unmarshal(data, cfg) + + return err +} diff --git "a/doc/draft/\346\225\260\346\215\256\345\272\223.md" "b/doc/draft/\346\225\260\346\215\256\345\272\223.md" new file mode 100644 index 0000000..83ac347 --- /dev/null +++ "b/doc/draft/\346\225\260\346\215\256\345\272\223.md" @@ -0,0 +1,7 @@ +考虑到对Bot进行二次开发时往往需要涉及到数据存储的问题,Iceinu直接提供了公用的数据库连接池,其中包含一个SQLite连接池和可选的PostgreSQL连接池。 + +由于不想因为引入cgo而导致代码编译流程变得复杂,Iceinu使用的SQLite驱动是github.com/glebarez/sqlite,一个纯Go实现的SQLite驱动,但这可能导致无法预料的特性缺失以及相对原生SQLite的性能下降。 + +虽然直接提供了SQLite连接但并不建议将其作为主要的数据存储,SQLite连接本身主要被框架用来存储元数据,在实际进行数据存储(尤其是启用了分布/集群模式时)应该启用Iceinu的PostgreSQL/MySQL连接作为插件的数据存储。 + +同时Iceinu还维护了基于FreeCache的缓存数据存储池,一般有一个被用于适配器的消息数据缓存,还有一个作为框架使用的缓存。 \ No newline at end of file diff --git "a/doc/draft/\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\346\200\273\347\272\277\344\270\255\351\227\264\344\273\266.md" "b/doc/draft/\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\346\200\273\347\272\277\344\270\255\351\227\264\344\273\266.md" new file mode 100644 index 0000000..f1d9880 --- /dev/null +++ "b/doc/draft/\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\346\200\273\347\272\277\344\270\255\351\227\264\344\273\266.md" @@ -0,0 +1,30 @@ +Iceinu的事件总线具备自定义中间件支持,允许开发者自行在事件处理前后进行一些操作,比如日志记录、性能监控等。 + +中间件函数可以在如下情景被触发: + +1. 任意事件发布 +2. 指定类型事件发布 +3. 指定摘要事件发布 +4. 事件被订阅者处理 + +Iceinu的中间件函数遵循洋葱模型,即事件发布时,中间件函数的执行顺序为: + +1. 事件发布前的中间件函数 +2. 事件发布者发布事件 +3. 事件发布后的中间件函数 + +以下是一个中间件函数封包的示例: +```go +customPublishLogger := func(event *ice.IceinuEvent, next func(event *ice.IceinuEvent)) { + log.Infof("Publish event: %s", event) + next(event) + log.Infof("Event published: %s", event) +} +``` + +在中间件函数中,可以通过event参数获取事件的详细信息,通过next参数控制中间件函数的执行顺序。 + +然后可以通过对应阶段的注册函数来将中间件添加到事件总线中: +```go +ice.UseGlobalPublishMiddleware(customPublishLogger) +``` \ No newline at end of file diff --git "a/doc/draft/\350\207\252\345\256\232\344\271\211\346\227\245\345\277\227\346\240\274\345\274\217.md" "b/doc/draft/\350\207\252\345\256\232\344\271\211\346\227\245\345\277\227\346\240\274\345\274\217.md" new file mode 100644 index 0000000..56f1785 --- /dev/null +++ "b/doc/draft/\350\207\252\345\256\232\344\271\211\346\227\245\345\277\227\346\240\274\345\274\217.md" @@ -0,0 +1,73 @@ +Iceinu框架内置了Logrus作为日志库,从而也自然支持了Logrus的日志格式化功能。 + +在log包中,已经预先定义了一些常用的快捷方式,无需再手动引入Logrus。 + +在`main`包的`log_format.go`中,是Iceinu的默认日志格式化配置,如下所示: + +```go +package main + +import ( + "fmt" + "github.com/KyokuKong/gradients" + "github.com/sirupsen/logrus" +) + +// LogFormatter 可以通过修改这个结构体的Format方法来设置你想要的日志格式 +type LogFormatter struct{} + +func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { + // 根据日志级别设置不同的颜色 + var textColor string + var levelColor string + var levelText string + switch entry.Level { + case logrus.DebugLevel, logrus.TraceLevel: + levelColor = gradients.DarkGreen + textColor = gradients.DarkGreen + levelText = "DEBUG" + case logrus.InfoLevel: + levelColor = gradients.DarkCyan + textColor = gradients.White + levelText = "_INFO" + case logrus.WarnLevel: + levelColor = gradients.Orange + textColor = gradients.DarkYellow + levelText = "_WARN" + case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: + levelColor = gradients.Red + textColor = gradients.Red + levelText = "ERROR" + default: + levelColor = gradients.White + textColor = gradients.White + levelText = "UNKNOWN" + } + + // 构建日志格式,可以按需修改 + logMsg := fmt.Sprintf( + "%s• %s %s[%s%s%s] %s%s\n", + gradients.Gray, + entry.Time.Format("2006-01-02 15:04:05"), + textColor, + levelColor, + levelText, + textColor, + entry.Message, + gradients.ResetColor, + ) + + return []byte(logMsg), nil +} +``` + +你可以根据自己的需求修改这个结构体的`Format`方法来设置你想要的日志格式,然后在`main`包的`main`函数中设置为全局的日志格式化器: + + +```go +// 定义日志格式 +formatter := &LogFormatter{} +log.SetFormatter(formatter) +``` + +这里的`log.SetFormatter`方法是Logrus的日志格式化Formatter的一个快捷方式,在`log`包下的`logger.go`中进行定义。 \ No newline at end of file diff --git a/elements/elements.go b/elements/elements.go deleted file mode 100644 index 4618a76..0000000 --- a/elements/elements.go +++ /dev/null @@ -1,650 +0,0 @@ -package elements - -import ( - "encoding/base64" - "fmt" - "io" - "strings" - "time" -) - -var DefaultThumb, _ = base64.StdEncoding.DecodeString("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") - -// IceinuMessageElement Iceinu的通用消息元素接口,参考了Satori的标准消息元素设计 -// https://satori.js.org/zh-CN/protocol/elements.html -// 基于对不同平台的支持,Iceinu对标准消息元素进行扩展和调整来方便一些平台特殊设计的实现 -// 带*号的消息元素是Iceinu自定义的消息元素,方便实现一些针对平台设计的特殊功能 -type IceinuMessageElement interface { - GetType() string // 获取消息元素类型 - ToSatori() string // 转换为Satori消息元素字符串 -} - -// TextElement 文本消息元素 -type TextElement struct { - Text string -} - -func (t *TextElement) GetType() string { - return "text" -} - -func (t *TextElement) ToSatori() string { - return t.Text -} - -// AtElement At提及消息元素 -type AtElement struct { - Id string // 目标用户ID - Name string // 目标用户名称 - Role string // 目标用户角色 - Type string // At请求类型,0为全体成员,1为指定成员 -} - -func (a *AtElement) GetType() string { - return "at" -} - -func (a *AtElement) ToSatori() string { - var attributes []string - if a.Id != "" { - attributes = append(attributes, fmt.Sprintf("id=\"%s\"", a.Id)) - } - if a.Name != "" { - attributes = append(attributes, fmt.Sprintf("name=\"%s\"", a.Name)) - } - if a.Role != "" { - attributes = append(attributes, fmt.Sprintf("role=\"%s\"", a.Role)) - } - if a.Type != "" { - attributes = append(attributes, fmt.Sprintf("type=\"%s\"", a.Type)) - } - return fmt.Sprintf("", strings.Join(attributes, " ")) -} - -// SharpElement Sharp提及频道消息元素 -type SharpElement struct { - Id string // 目标频道ID - Name string // 目标频道名称 -} - -func (s *SharpElement) GetType() string { - return "sharp" -} - -func (s *SharpElement) ToSatori() string { - var attributes []string - if s.Id != "" { - attributes = append(attributes, fmt.Sprintf("id=\"%s\"", s.Id)) - } - if s.Name != "" { - attributes = append(attributes, fmt.Sprintf("name=\"%s\"", s.Name)) - } - return fmt.Sprintf("", strings.Join(attributes, " ")) -} - -// LinkElement A超链接消息元素 -type LinkElement struct { - Href string // 链接地址 -} - -func (a *LinkElement) GetType() string { - return "link" -} - -func (a *LinkElement) ToSatori() string { - if a.Href != "" { - return fmt.Sprintf("", a.Href) - } - return "" -} - -// ImageElement 图片消息元素 -type ImageElement struct { - // 用于接收图片 - - ImageId string // 图片ID - Src string // 图片源地址 - Title string // 图片标题 - Width uint32 // 图片宽度 - Height uint32 // 图片高度 - - EffectId int // 图片特效ID - IsFlash bool // 是否是闪图 - - // 用于发送图片 - Summary string // 图片描述 - Path string // 图片路径或URL - Stream io.ReadSeeker // 图片流 -} - -func (i *ImageElement) GetType() string { - return "image" -} - -func (i *ImageElement) ToSatori() string { - var attributes []string - if i.ImageId != "" { - attributes = append(attributes, fmt.Sprintf("id=\"%s\"", i.ImageId)) - } - if i.Src != "" { - attributes = append(attributes, fmt.Sprintf("src=\"%s\"", i.Src)) - } - if i.Title != "" { - attributes = append(attributes, fmt.Sprintf("title=\"%s\"", i.Title)) - } - if i.Width != 0 { - attributes = append(attributes, fmt.Sprintf("width=\"%d\"", i.Width)) - } - if i.Height != 0 { - attributes = append(attributes, fmt.Sprintf("height=\"%d\"", i.Height)) - } - if i.EffectId != 0 { - attributes = append(attributes, fmt.Sprintf("effect=\"%d\"", i.EffectId)) - } - if i.IsFlash { - attributes = append(attributes, fmt.Sprintf("flash=\"%t\"", i.IsFlash)) - } - if i.Summary != "" { - attributes = append(attributes, fmt.Sprintf("summary=\"%s\"", i.Summary)) - } - if i.Path != "" { - attributes = append(attributes, fmt.Sprintf("path=\"%s\"", i.Path)) - } - return fmt.Sprintf("", strings.Join(attributes, " ")) -} - -// AudioElement 音频消息元素 -type AudioElement struct { - Src string - Title string - Duration uint32 - Poster string - Stream io.ReadSeeker - Summary string - Path string -} - -func (a *AudioElement) GetType() string { - return "audio" -} - -func (a *AudioElement) ToSatori() string { - var attributes []string - if a.Src != "" { - attributes = append(attributes, fmt.Sprintf("src=\"%s\"", a.Src)) - } - if a.Title != "" { - attributes = append(attributes, fmt.Sprintf("title=\"%s\"", a.Title)) - } - if a.Duration != 0 { - attributes = append(attributes, fmt.Sprintf("duration=\"%d\"", a.Duration)) - } - if a.Poster != "" { - attributes = append(attributes, fmt.Sprintf("poster=\"%s\"", a.Poster)) - } - if a.Summary != "" { - attributes = append(attributes, fmt.Sprintf("summary=\"%s\"", a.Summary)) - } - if a.Path != "" { - attributes = append(attributes, fmt.Sprintf("path=\"%s\"", a.Path)) - } - return fmt.Sprintf("