This commit is contained in:
Shen Junzheng
2025-03-12 01:19:35 +08:00
parent 160040f3e1
commit 78cce92ce3
70 changed files with 10134 additions and 1 deletions

148
pkg/config/config.go Normal file
View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2023 shenjunzheng@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package config
import (
"errors"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
DefaultConfigType = "json"
)
var (
// ConfigName holds the name of the configuration file.
ConfigName = ""
// ConfigType specifies the type/format of the configuration file.
ConfigType = ""
// ConfigPath denotes the path to the configuration file.
ConfigPath = ""
// ERROR
ErrInvalidDirectory = errors.New("invalid directory path")
ErrMissingConfigName = errors.New("config name not specified")
)
// Init initializes the configuration settings.
// It sets up the name, type, and path for the configuration file.
func Init(name, _type, path string) error {
if len(name) == 0 {
return ErrMissingConfigName
}
if len(_type) == 0 {
_type = DefaultConfigType
}
var err error
if len(path) == 0 {
path, err = os.UserHomeDir()
if err != nil {
path = os.TempDir()
}
path += string(os.PathSeparator) + "." + name
}
if err := PrepareDir(path); err != nil {
return err
}
ConfigName = name
ConfigType = _type
ConfigPath = path
return nil
}
// Load loads the configuration from the previously initialized file.
// It unmarshals the configuration into the provided conf interface.
func Load(conf interface{}) error {
viper.SetConfigName(ConfigName)
viper.SetConfigType(ConfigType)
viper.AddConfigPath(ConfigPath)
if err := viper.ReadInConfig(); err != nil {
if err := viper.SafeWriteConfig(); err != nil {
return err
}
}
if err := viper.Unmarshal(conf); err != nil {
return err
}
SetDefault(conf)
return nil
}
// LoadFile loads the configuration from a specified file.
// It unmarshals the configuration into the provided conf interface.
func LoadFile(file string, conf interface{}) error {
viper.SetConfigFile(file)
if err := viper.ReadInConfig(); err != nil {
return err
}
if err := viper.Unmarshal(conf); err != nil {
return err
}
SetDefault(conf)
return nil
}
// SetConfig sets a configuration key to a specified value.
// It also writes the updated configuration back to the file.
func SetConfig(key string, value interface{}) error {
viper.Set(key, value)
if err := viper.WriteConfig(); err != nil {
return err
}
return nil
}
// ResetConfig resets the configuration to empty.
func ResetConfig() error {
viper.Reset()
viper.SetConfigName(ConfigName)
viper.SetConfigType(ConfigType)
viper.AddConfigPath(ConfigPath)
return viper.WriteConfig()
}
// GetConfig retrieves all configuration settings as a map.
func GetConfig() map[string]interface{} {
return viper.AllSettings()
}
// PrepareDir ensures that the specified directory path exists.
// If the directory does not exist, it attempts to create it.
func PrepareDir(path string) error {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
} else {
return err
}
} else if !stat.IsDir() {
log.Debugf("%s is not a directory", path)
return ErrInvalidDirectory
}
return nil
}

251
pkg/config/default.go Normal file
View File

@@ -0,0 +1,251 @@
/*
* Copyright (c) 2023 shenjunzheng@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package config
import (
"encoding/json"
"reflect"
"strconv"
)
// DefaultTag is the default tag used to identify default values for struct fields.
var DefaultTag string
func init() {
DefaultTag = "default"
}
// SetDefaultTag updates the tag used to identify default values.
func SetDefaultTag(tag string) {
DefaultTag = tag
}
// SetDefault sets the default values for a given interface{}.
// The interface{} must be a pointer to a struct, bcs the default values are SET on the struct fields.
func SetDefault(v interface{}) {
if v == nil {
return
}
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
setDefault(val, "")
}
// setDefault recursively sets default values based on the struct's tags.
func setDefault(val reflect.Value, tag string) {
if !val.CanSet() {
return
}
switch val.Kind() {
case reflect.Struct:
handleStruct(val, tag)
case reflect.Ptr:
handlePtr(val, tag)
case reflect.Interface:
handleInterface(val, tag)
case reflect.Map:
handleMap(val, tag)
case reflect.Slice, reflect.Array:
handleSliceArray(val, tag)
default:
handleSimpleType(val, tag)
}
}
// handleSimpleType handles the assignment of default values for simple data types.
func handleSimpleType(val reflect.Value, tag string) {
if len(tag) == 0 || !val.IsZero() || !val.CanSet() {
return
}
// Assign appropriate default values based on the data type.
switch val.Kind() {
case reflect.String:
val.SetString(tag)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if intValue, err := strconv.ParseInt(tag, 10, 64); err == nil {
val.SetInt(intValue)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if uintVal, err := strconv.ParseUint(tag, 10, 64); err == nil {
val.SetUint(uintVal)
}
case reflect.Float32, reflect.Float64:
if floatValue, err := strconv.ParseFloat(tag, 64); err == nil {
val.SetFloat(floatValue)
}
case reflect.Bool:
if boolVal, err := strconv.ParseBool(tag); err == nil {
val.SetBool(boolVal)
}
}
}
// handleStruct processes struct type fields and sets default values based on their tags.
func handleStruct(val reflect.Value, tag string) {
if val.Kind() != reflect.Struct {
return
}
if !val.IsZero() || len(tag) == 0 {
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
setDefault(field, fieldType.Tag.Get(DefaultTag))
}
return
}
if !val.CanSet() {
return
}
// If tag is provided, unmarshal the default value and set it.
newStruct := reflect.New(val.Type()).Interface()
if err := json.Unmarshal([]byte(tag), newStruct); err == nil {
newStructVal := reflect.ValueOf(newStruct).Elem()
for i := 0; i < newStructVal.NumField(); i++ {
field := newStructVal.Field(i)
fieldType := newStructVal.Type().Field(i)
setDefault(field, fieldType.Tag.Get(DefaultTag))
}
val.Set(newStructVal)
}
}
// handleSliceArray sets default values for slice or array types.
func handleSliceArray(val reflect.Value, tag string) {
if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
return
}
if !val.IsZero() || len(tag) == 0 {
for i := 0; i < val.Len(); i++ {
setDefault(val.Index(i), "")
}
return
}
if !val.CanSet() {
return
}
// array 提前初始化子结构体,解决 default 长度小于 array 长度的问题
if val.Kind() == reflect.Array {
for i := 0; i < val.Len(); i++ {
setDefault(val.Index(i), "")
}
}
// If tag is provided, unmarshal the default value and set it.
newSlice := reflect.New(reflect.SliceOf(val.Type().Elem())).Interface()
if err := json.Unmarshal([]byte(tag), newSlice); err == nil {
sliceValue := reflect.ValueOf(newSlice).Elem()
for j := 0; j < sliceValue.Len(); j++ {
v := sliceValue.Index(j)
setDefault(v, "")
if val.Kind() == reflect.Array {
if j >= val.Len() {
return
}
val.Index(j).Set(v)
} else {
val.Set(reflect.Append(val, v))
}
}
}
}
// handleMap sets default values for map types.
func handleMap(val reflect.Value, tag string) {
if val.Kind() != reflect.Map {
return
}
if !val.IsZero() || len(tag) == 0 {
for _, key := range val.MapKeys() {
setDefault(val.MapIndex(key), "")
}
return
}
if !val.CanSet() {
return
}
// If tag is provided, unmarshal the default value and set it.
newMap := reflect.New(val.Type()).Interface()
if err := json.Unmarshal([]byte(tag), newMap); err == nil {
newMapVal := reflect.ValueOf(newMap).Elem()
for _, k := range newMapVal.MapKeys() {
v := newMapVal.MapIndex(k)
setDefault(v, "")
}
val.Set(newMapVal)
}
}
// handlePtr handles pointer types and sets default values based on their tags.
func handlePtr(val reflect.Value, tag string) {
if val.Kind() != reflect.Ptr {
return
}
if !val.IsZero() || len(tag) == 0 || !val.CanSet() {
return
}
// If tag is provided, unmarshal the default value and set it.
newPtr := reflect.New(val.Type()).Interface()
if err := json.Unmarshal([]byte(tag), newPtr); err == nil {
newPtrVal := reflect.ValueOf(newPtr).Elem()
setDefault(newPtrVal, "")
val.Set(newPtrVal)
}
}
// handleInterface processes interface types and sets default values based on their tags.
// support pointer interface only, bcs the default values are SET on the struct fields.
func handleInterface(val reflect.Value, tag string) {
if val.Kind() != reflect.Interface {
return
}
if val.IsNil() {
return
}
if val.Elem().Kind() != reflect.Ptr {
return
}
// Recursively set the default for the inner value of the interface.
setDefault(reflect.ValueOf(val.Elem().Interface()).Elem(), tag)
}

25
pkg/dllver/version.go Normal file
View File

@@ -0,0 +1,25 @@
package dllver
type Info struct {
FilePath string `json:"file_path"`
CompanyName string `json:"company_name"`
FileDescription string `json:"file_description"`
FileVersion string `json:"file_version"`
FileMajorVersion int `json:"file_major_version"`
LegalCopyright string `json:"legal_copyright"`
ProductName string `json:"product_name"`
ProductVersion string `json:"product_version"`
}
func New(filePath string) (*Info, error) {
i := &Info{
FilePath: filePath,
}
err := i.initialize()
if err != nil {
return nil, err
}
return i, nil
}

View File

@@ -0,0 +1,7 @@
//go:build !windows
package dllver
func (i *Info) initialize() error {
return nil
}

View File

@@ -0,0 +1,142 @@
package dllver
import (
"fmt"
"syscall"
"unsafe"
)
var (
modversion = syscall.NewLazyDLL("version.dll")
procGetFileVersionInfoSize = modversion.NewProc("GetFileVersionInfoSizeW")
procGetFileVersionInfo = modversion.NewProc("GetFileVersionInfoW")
procVerQueryValue = modversion.NewProc("VerQueryValueW")
)
// VS_FIXEDFILEINFO 结构体
type VS_FIXEDFILEINFO struct {
Signature uint32
StrucVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}
// initialize 初始化版本信息
func (i *Info) initialize() error {
// 转换路径为 UTF16
pathPtr, err := syscall.UTF16PtrFromString(i.FilePath)
if err != nil {
return err
}
// 获取版本信息大小
var handle uintptr
size, _, err := procGetFileVersionInfoSize.Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&handle)),
)
if size == 0 {
return fmt.Errorf("GetFileVersionInfoSize failed: %v", err)
}
// 分配内存
verInfo := make([]byte, size)
ret, _, err := procGetFileVersionInfo.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
size,
uintptr(unsafe.Pointer(&verInfo[0])),
)
if ret == 0 {
return fmt.Errorf("GetFileVersionInfo failed: %v", err)
}
// 获取固定的文件信息
var fixedFileInfo *VS_FIXEDFILEINFO
var uLen uint32
rootPtr, _ := syscall.UTF16PtrFromString("\\")
ret, _, err = procVerQueryValue.Call(
uintptr(unsafe.Pointer(&verInfo[0])),
uintptr(unsafe.Pointer(rootPtr)),
uintptr(unsafe.Pointer(&fixedFileInfo)),
uintptr(unsafe.Pointer(&uLen)),
)
if ret == 0 {
return fmt.Errorf("VerQueryValue failed: %v", err)
}
// 解析文件版本
i.FileVersion = fmt.Sprintf("%d.%d.%d.%d",
(fixedFileInfo.FileVersionMS>>16)&0xffff,
(fixedFileInfo.FileVersionMS>>0)&0xffff,
(fixedFileInfo.FileVersionLS>>16)&0xffff,
(fixedFileInfo.FileVersionLS>>0)&0xffff,
)
i.FileMajorVersion = int((fixedFileInfo.FileVersionMS >> 16) & 0xffff)
i.ProductVersion = fmt.Sprintf("%d.%d.%d.%d",
(fixedFileInfo.ProductVersionMS>>16)&0xffff,
(fixedFileInfo.ProductVersionMS>>0)&0xffff,
(fixedFileInfo.ProductVersionLS>>16)&0xffff,
(fixedFileInfo.ProductVersionLS>>0)&0xffff,
)
// 获取翻译信息
type langAndCodePage struct {
language uint16
codePage uint16
}
var lpTranslate *langAndCodePage
var cbTranslate uint32
transPtr, _ := syscall.UTF16PtrFromString("\\VarFileInfo\\Translation")
ret, _, _ = procVerQueryValue.Call(
uintptr(unsafe.Pointer(&verInfo[0])),
uintptr(unsafe.Pointer(transPtr)),
uintptr(unsafe.Pointer(&lpTranslate)),
uintptr(unsafe.Pointer(&cbTranslate)),
)
if ret != 0 && cbTranslate > 0 {
// 获取所有需要的字符串信息
stringInfos := map[string]*string{
"CompanyName": &i.CompanyName,
"FileDescription": &i.FileDescription,
"FileVersion": &i.FileVersion,
"LegalCopyright": &i.LegalCopyright,
"ProductName": &i.ProductName,
"ProductVersion": &i.ProductVersion,
}
for name, ptr := range stringInfos {
subBlock := fmt.Sprintf("\\StringFileInfo\\%04x%04x\\%s",
lpTranslate.language, lpTranslate.codePage, name)
subBlockPtr, _ := syscall.UTF16PtrFromString(subBlock)
var buffer *uint16
var bufLen uint32
ret, _, _ = procVerQueryValue.Call(
uintptr(unsafe.Pointer(&verInfo[0])),
uintptr(unsafe.Pointer(subBlockPtr)),
uintptr(unsafe.Pointer(&buffer)),
uintptr(unsafe.Pointer(&bufLen)),
)
if ret != 0 && bufLen > 0 {
*ptr = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(buffer))[:bufLen:bufLen])
}
}
}
return nil
}

142
pkg/model/chatroom.go Normal file
View File

@@ -0,0 +1,142 @@
package model
import (
"github.com/sjzar/chatlog/pkg/model/wxproto"
"google.golang.org/protobuf/proto"
)
type ChatRoom struct {
Name string `json:"name"`
Owner string `json:"owner"`
Users []ChatRoomUser `json:"users"`
// Extra From Contact
Remark string `json:"remark"`
NickName string `json:"nickName"`
User2DisplayName map[string]string `json:"-"`
}
type ChatRoomUser struct {
UserName string `json:"userName"`
DisplayName string `json:"displayName"`
}
// CREATE TABLE ChatRoom(
// ChatRoomName TEXT PRIMARY KEY,
// UserNameList TEXT,
// DisplayNameList TEXT,
// ChatRoomFlag int Default 0,
// Owner INTEGER DEFAULT 0,
// IsShowName INTEGER DEFAULT 0,
// SelfDisplayName TEXT,
// Reserved1 INTEGER DEFAULT 0,
// Reserved2 TEXT,
// Reserved3 INTEGER DEFAULT 0,
// Reserved4 TEXT,
// Reserved5 INTEGER DEFAULT 0,
// Reserved6 TEXT,
// RoomData BLOB,
// Reserved7 INTEGER DEFAULT 0,
// Reserved8 TEXT
// )
type ChatRoomV3 struct {
ChatRoomName string `json:"ChatRoomName"`
Reserved2 string `json:"Reserved2"` // Creator
RoomData []byte `json:"RoomData"`
// // 非关键信息,暂时忽略
// UserNameList string `json:"UserNameList"`
// DisplayNameList string `json:"DisplayNameList"`
// ChatRoomFlag int `json:"ChatRoomFlag"`
// Owner int `json:"Owner"`
// IsShowName int `json:"IsShowName"`
// SelfDisplayName string `json:"SelfDisplayName"`
// Reserved1 int `json:"Reserved1"`
// Reserved3 int `json:"Reserved3"`
// Reserved4 string `json:"Reserved4"`
// Reserved5 int `json:"Reserved5"`
// Reserved6 string `json:"Reserved6"`
// Reserved7 int `json:"Reserved7"`
// Reserved8 string `json:"Reserved8"`
}
func (c *ChatRoomV3) Wrap() *ChatRoom {
var users []ChatRoomUser
if len(c.RoomData) != 0 {
users = ParseRoomData(c.RoomData)
}
user2DisplayName := make(map[string]string, len(users))
for _, user := range users {
if user.DisplayName != "" {
user2DisplayName[user.UserName] = user.DisplayName
}
}
return &ChatRoom{
Name: c.ChatRoomName,
Owner: c.Reserved2,
Users: users,
User2DisplayName: user2DisplayName,
}
}
// CREATE TABLE chat_room(
// id INTEGER PRIMARY KEY,
// username TEXT,
// owner TEXT,
// ext_buffer BLOB
// )
type ChatRoomV4 struct {
ID int `json:"id"`
UserName string `json:"username"`
Owner string `json:"owner"`
ExtBuffer []byte `json:"ext_buffer"`
}
func (c *ChatRoomV4) Wrap() *ChatRoom {
var users []ChatRoomUser
if len(c.ExtBuffer) != 0 {
users = ParseRoomData(c.ExtBuffer)
}
return &ChatRoom{
Name: c.UserName,
Owner: c.Owner,
Users: users,
}
}
func ParseRoomData(b []byte) (users []ChatRoomUser) {
var pbMsg wxproto.RoomData
if err := proto.Unmarshal(b, &pbMsg); err != nil {
return
}
if pbMsg.Users == nil {
return
}
users = make([]ChatRoomUser, 0, len(pbMsg.Users))
for _, user := range pbMsg.Users {
u := ChatRoomUser{UserName: user.UserName}
if user.DisplayName != nil {
u.DisplayName = *user.DisplayName
}
users = append(users, u)
}
return users
}
func (c *ChatRoom) DisplayName() string {
switch {
case c.Remark != "":
return c.Remark
case c.NickName != "":
return c.NickName
}
return ""
}

158
pkg/model/contact.go Normal file
View File

@@ -0,0 +1,158 @@
package model
type Contact struct {
UserName string `json:"userName"`
Alias string `json:"alias"`
Remark string `json:"remark"`
NickName string `json:"nickName"`
IsFriend bool `json:"isFriend"`
}
// CREATE TABLE Contact(
// UserName TEXT PRIMARY KEY ,
// Alias TEXT,
// EncryptUserName TEXT,
// DelFlag INTEGER DEFAULT 0,
// Type INTEGER DEFAULT 0,
// VerifyFlag INTEGER DEFAULT 0,
// Reserved1 INTEGER DEFAULT 0,
// Reserved2 INTEGER DEFAULT 0,
// Reserved3 TEXT,
// Reserved4 TEXT,
// Remark TEXT,
// NickName TEXT,
// LabelIDList TEXT,
// DomainList TEXT,
// ChatRoomType int,
// PYInitial TEXT,
// QuanPin TEXT,
// RemarkPYInitial TEXT,
// RemarkQuanPin TEXT,
// BigHeadImgUrl TEXT,
// SmallHeadImgUrl TEXT,
// HeadImgMd5 TEXT,
// ChatRoomNotify INTEGER DEFAULT 0,
// Reserved5 INTEGER DEFAULT 0,
// Reserved6 TEXT,
// Reserved7 TEXT,
// ExtraBuf BLOB,
// Reserved8 INTEGER DEFAULT 0,
// Reserved9 INTEGER DEFAULT 0,
// Reserved10 TEXT,
// Reserved11 TEXT
// )
type ContactV3 struct {
UserName string `json:"UserName"`
Alias string `json:"Alias"`
Remark string `json:"Remark"`
NickName string `json:"NickName"`
Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
// EncryptUserName string `json:"EncryptUserName"`
// DelFlag int `json:"DelFlag"`
// Type int `json:"Type"`
// VerifyFlag int `json:"VerifyFlag"`
// Reserved2 int `json:"Reserved2"`
// Reserved3 string `json:"Reserved3"`
// Reserved4 string `json:"Reserved4"`
// LabelIDList string `json:"LabelIDList"`
// DomainList string `json:"DomainList"`
// ChatRoomType int `json:"ChatRoomType"`
// PYInitial string `json:"PYInitial"`
// QuanPin string `json:"QuanPin"`
// RemarkPYInitial string `json:"RemarkPYInitial"`
// RemarkQuanPin string `json:"RemarkQuanPin"`
// BigHeadImgUrl string `json:"BigHeadImgUrl"`
// SmallHeadImgUrl string `json:"SmallHeadImgUrl"`
// HeadImgMd5 string `json:"HeadImgMd5"`
// ChatRoomNotify int `json:"ChatRoomNotify"`
// Reserved5 int `json:"Reserved5"`
// Reserved6 string `json:"Reserved6"`
// Reserved7 string `json:"Reserved7"`
// ExtraBuf []byte `json:"ExtraBuf"`
// Reserved8 int `json:"Reserved8"`
// Reserved9 int `json:"Reserved9"`
// Reserved10 string `json:"Reserved10"`
// Reserved11 string `json:"Reserved11"`
}
func (c *ContactV3) Wrap() *Contact {
return &Contact{
UserName: c.UserName,
Alias: c.Alias,
Remark: c.Remark,
NickName: c.NickName,
IsFriend: c.Reserved1 == 1,
}
}
// CREATE TABLE contact(
// id INTEGER PRIMARY KEY,
// username TEXT,
// local_type INTEGER,
// alias TEXT,
// encrypt_username TEXT,
// flag INTEGER,
// delete_flag INTEGER,
// verify_flag INTEGER,
// remark TEXT,
// remark_quan_pin TEXT,
// remark_pin_yin_initial TEXT,
// nick_name TEXT,
// pin_yin_initial TEXT,
// quan_pin TEXT,
// big_head_url TEXT,
// small_head_url TEXT,
// head_img_md5 TEXT,
// chat_room_notify INTEGER,
// is_in_chat_room INTEGER,
// description TEXT,
// extra_buffer BLOB,
// chat_room_type INTEGER
// )
type ContactV4 struct {
UserName string `json:"username"`
Alias string `json:"alias"`
Remark string `json:"remark"`
NickName string `json:"nick_name"`
LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
// ID int `json:"id"`
// EncryptUserName string `json:"encrypt_username"`
// Flag int `json:"flag"`
// DeleteFlag int `json:"delete_flag"`
// VerifyFlag int `json:"verify_flag"`
// RemarkQuanPin string `json:"remark_quan_pin"`
// RemarkPinYinInitial string `json:"remark_pin_yin_initial"`
// PinYinInitial string `json:"pin_yin_initial"`
// QuanPin string `json:"quan_pin"`
// BigHeadUrl string `json:"big_head_url"`
// SmallHeadUrl string `json:"small_head_url"`
// HeadImgMd5 string `json:"head_img_md5"`
// ChatRoomNotify int `json:"chat_room_notify"`
// IsInChatRoom int `json:"is_in_chat_room"`
// Description string `json:"description"`
// ExtraBuffer []byte `json:"extra_buffer"`
// ChatRoomType int `json:"chat_room_type"`
}
func (c *ContactV4) Wrap() *Contact {
return &Contact{
UserName: c.UserName,
Alias: c.Alias,
Remark: c.Remark,
NickName: c.NickName,
IsFriend: c.LocalType != 3,
}
}
func (c *Contact) DisplayName() string {
switch {
case c.Remark != "":
return c.Remark
case c.NickName != "":
return c.NickName
}
return ""
}

301
pkg/model/message.go Normal file
View File

@@ -0,0 +1,301 @@
package model
import (
"fmt"
"strings"
"time"
"github.com/sjzar/chatlog/pkg/model/wxproto"
"github.com/sjzar/chatlog/pkg/util"
"google.golang.org/protobuf/proto"
)
const (
// Source
WeChatV3 = "wechatv3"
WeChatV4 = "wechatv4"
)
type Message struct {
Sequence int64 `json:"sequence"` // 消息序号10位时间戳 + 3位序号
CreateTime time.Time `json:"createTime"` // 消息创建时间10位时间戳
TalkerID int `json:"talkerID"` // 聊天对象Name2ID 表序号,索引值
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
IsSender int `json:"isSender"` // 是否为发送消息0 接收消息1 发送消息
Type int `json:"type"` // 消息类型
SubType int `json:"subType"` // 消息子类型
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人
// Fill Info
// 从联系人等信息中填充
DisplayName string `json:"-"` // 显示名称
CharRoomName string `json:"-"` // 群聊名称
Version string `json:"-"` // 消息版本,内部判断
}
// CREATE TABLE MSG (
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
// TalkerId INT DEFAULT 0,
// MsgSvrID INT,
// Type INT,
// SubType INT,
// IsSender INT,
// CreateTime INT,
// Sequence INT DEFAULT 0,
// StatusEx INT DEFAULT 0,
// FlagEx INT,
// Status INT,
// MsgServerSeq INT,
// MsgSequence INT,
// StrTalker TEXT,
// StrContent TEXT,
// DisplayContent TEXT,
// Reserved0 INT DEFAULT 0,
// Reserved1 INT DEFAULT 0,
// Reserved2 INT DEFAULT 0,
// Reserved3 INT DEFAULT 0,
// Reserved4 TEXT,
// Reserved5 TEXT,
// Reserved6 TEXT,
// CompressContent BLOB,
// BytesExtra BLOB,
// BytesTrans BLOB
// )
type MessageV3 struct {
Sequence int64 `json:"Sequence"` // 消息序号10位时间戳 + 3位序号
CreateTime int64 `json:"CreateTime"` // 消息创建时间10位时间戳
TalkerID int `json:"TalkerId"` // 聊天对象Name2ID 表序号,索引值
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
IsSender int `json:"IsSender"` // 是否为发送消息0 接收消息1 发送消息
Type int `json:"Type"` // 消息类型
SubType int `json:"SubType"` // 消息子类型
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
// 非关键信息,后续有需要再加入
// LocalID int64 `json:"localId"`
// MsgSvrID int64 `json:"MsgSvrID"`
// StatusEx int `json:"StatusEx"`
// FlagEx int `json:"FlagEx"`
// Status int `json:"Status"`
// MsgServerSeq int64 `json:"MsgServerSeq"`
// MsgSequence int64 `json:"MsgSequence"`
// DisplayContent string `json:"DisplayContent"`
// Reserved0 int `json:"Reserved0"`
// Reserved1 int `json:"Reserved1"`
// Reserved2 int `json:"Reserved2"`
// Reserved3 int `json:"Reserved3"`
// Reserved4 string `json:"Reserved4"`
// Reserved5 string `json:"Reserved5"`
// Reserved6 string `json:"Reserved6"`
// BytesTrans []byte `json:"BytesTrans"`
}
func (m *MessageV3) Wrap() *Message {
isChatRoom := strings.HasSuffix(m.StrTalker, "@chatroom")
var chatRoomSender string
if len(m.BytesExtra) != 0 && isChatRoom {
chatRoomSender = ParseBytesExtra(m.BytesExtra)
}
return &Message{
Sequence: m.Sequence,
CreateTime: time.Unix(m.CreateTime, 0),
TalkerID: m.TalkerID,
Talker: m.StrTalker,
IsSender: m.IsSender,
Type: m.Type,
SubType: m.SubType,
Content: m.StrContent,
CompressContent: m.CompressContent,
IsChatRoom: isChatRoom,
ChatRoomSender: chatRoomSender,
Version: WeChatV3,
}
}
// CREATE TABLE Msg_xxxxxxxxxxxx(
// local_id INTEGER PRIMARY KEY AUTOINCREMENT,
// server_id INTEGER,
// local_type INTEGER,
// sort_seq INTEGER,
// real_sender_id INTEGER,
// create_time INTEGER,
// status INTEGER,
// upload_status INTEGER,
// download_status INTEGER,
// server_seq INTEGER,
// origin_source INTEGER,
// source TEXT,
// message_content TEXT,
// compress_content TEXT,
// packed_info_data BLOB,
// WCDB_CT_message_content INTEGER DEFAULT NULL,
// WCDB_CT_source INTEGER DEFAULT NULL
// )
type MessageV4 struct {
SortSeq int64 `json:"sort_seq"` // 消息序号10位时间戳 + 3位序号
LocalType int `json:"local_type"` // 消息类型
RealSenderID int `json:"real_sender_id"` // 发送人 ID对应 Name2Id 表序号
CreateTime int64 `json:"create_time"` // 消息创建时间10位时间戳
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto格式与 v3 有差异
Status int `json:"status"` // 消息状态2 是已发送4 是已接收,可以用于判断 IsSender猜测
// 非关键信息,后续有需要再加入
// LocalID int `json:"local_id"`
// ServerID int64 `json:"server_id"`
// UploadStatus int `json:"upload_status"`
// DownloadStatus int `json:"download_status"`
// ServerSeq int `json:"server_seq"`
// OriginSource int `json:"origin_source"`
// Source string `json:"source"`
// CompressContent string `json:"compress_content"`
}
func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
_m := &Message{
Sequence: m.SortSeq,
CreateTime: time.Unix(m.CreateTime, 0),
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
CompressContent: m.PackedInfoData,
Type: m.LocalType,
Version: WeChatV4,
}
if name, ok := id2Name[m.RealSenderID]; ok {
_m.Talker = name
}
if m.Status == 2 {
_m.IsSender = 1
}
if util.IsNormalString(m.MessageContent) {
_m.Content = string(m.MessageContent)
} else {
_m.CompressContent = m.MessageContent
}
if isChatRoom {
_m.IsChatRoom = true
split := strings.Split(_m.Content, "\n")
if len(split) > 1 {
_m.Content = split[1]
_m.ChatRoomSender = strings.TrimSuffix(split[0], ":")
}
}
return _m
}
// ParseBytesExtra 解析额外数据
// 按需解析
func ParseBytesExtra(b []byte) (chatRoomSender string) {
var pbMsg wxproto.BytesExtra
if err := proto.Unmarshal(b, &pbMsg); err != nil {
return
}
if pbMsg.Items == nil {
return
}
for _, item := range pbMsg.Items {
if item.Type == 1 {
return item.Value
}
}
return
}
func (m *Message) PlainText(showChatRoom bool) string {
buf := strings.Builder{}
talker := m.Talker
if m.IsSender == 1 {
talker = "我"
} else if m.IsChatRoom {
talker = m.ChatRoomSender
}
if m.DisplayName != "" {
buf.WriteString(m.DisplayName)
buf.WriteString("(")
buf.WriteString(talker)
buf.WriteString(")")
} else {
buf.WriteString(talker)
}
buf.WriteString(" ")
if m.IsChatRoom && showChatRoom {
buf.WriteString("[")
if m.CharRoomName != "" {
buf.WriteString(m.CharRoomName)
buf.WriteString("(")
buf.WriteString(m.Talker)
buf.WriteString(")")
} else {
buf.WriteString(m.Talker)
}
buf.WriteString("] ")
}
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
buf.WriteString("\n")
switch m.Type {
case 1:
buf.WriteString(m.Content)
case 3:
buf.WriteString("[图片]")
case 34:
buf.WriteString("[语音]")
case 43:
buf.WriteString("[视频]")
case 47:
buf.WriteString("[动画表情]")
case 49:
switch m.SubType {
case 6:
buf.WriteString("[文件]")
case 8:
buf.WriteString("[GIF表情]")
case 19:
buf.WriteString("[合并转发]")
case 33, 36:
buf.WriteString("[小程序]")
case 57:
buf.WriteString("[引用]")
case 63:
buf.WriteString("[视频号]")
case 87:
buf.WriteString("[群公告]")
case 2000:
buf.WriteString("[转账]")
case 2003:
buf.WriteString("[红包封面]")
default:
buf.WriteString("[分享]")
}
case 50:
buf.WriteString("[语音通话]")
case 10000:
buf.WriteString("[系统消息]")
default:
buf.WriteString(fmt.Sprintf("Type: %d Content: %s", m.Type, m.Content))
}
buf.WriteString("\n")
return buf.String()
}

133
pkg/model/session.go Normal file
View File

@@ -0,0 +1,133 @@
package model
import (
"strings"
"time"
)
type Session struct {
UserName string `json:"userName"`
NOrder int `json:"nOrder"`
NickName string `json:"nickName"`
Content string `json:"content"`
NTime time.Time `json:"nTime"`
}
// CREATE TABLE Session(
// strUsrName TEXT PRIMARY KEY,
// nOrder INT DEFAULT 0,
// nUnReadCount INTEGER DEFAULT 0,
// parentRef TEXT,
// Reserved0 INTEGER DEFAULT 0,
// Reserved1 TEXT,
// strNickName TEXT,
// nStatus INTEGER,
// nIsSend INTEGER,
// strContent TEXT,
// nMsgType INTEGER,
// nMsgLocalID INTEGER,
// nMsgStatus INTEGER,
// nTime INTEGER,
// editContent TEXT,
// othersAtMe INT,
// Reserved2 INTEGER DEFAULT 0,
// Reserved3 TEXT,
// Reserved4 INTEGER DEFAULT 0,
// Reserved5 TEXT,
// bytesXml BLOB
// )
type SessionV3 struct {
StrUsrName string `json:"strUsrName"`
NOrder int `json:"nOrder"`
StrNickName string `json:"strNickName"`
StrContent string `json:"strContent"`
NTime int64 `json:"nTime"`
// NUnReadCount int `json:"nUnReadCount"`
// ParentRef string `json:"parentRef"`
// Reserved0 int `json:"Reserved0"`
// Reserved1 string `json:"Reserved1"`
// NStatus int `json:"nStatus"`
// NIsSend int `json:"nIsSend"`
// NMsgType int `json:"nMsgType"`
// NMsgLocalID int `json:"nMsgLocalID"`
// NMsgStatus int `json:"nMsgStatus"`
// EditContent string `json:"editContent"`
// OthersAtMe int `json:"othersAtMe"`
// Reserved2 int `json:"Reserved2"`
// Reserved3 string `json:"Reserved3"`
// Reserved4 int `json:"Reserved4"`
// Reserved5 string `json:"Reserved5"`
// BytesXml string `json:"bytesXml"`
}
func (s *SessionV3) Wrap() *Session {
return &Session{
UserName: s.StrUsrName,
NOrder: s.NOrder,
NickName: s.StrNickName,
Content: s.StrContent,
NTime: time.Unix(int64(s.NTime), 0),
}
}
// 注意v4 session 是独立数据库文件
// CREATE TABLE SessionTable(
// username TEXT PRIMARY KEY,
// type INTEGER,
// unread_count INTEGER,
// unread_first_msg_srv_id INTEGER,
// is_hidden INTEGER,
// summary TEXT,
// draft TEXT,
// status INTEGER,
// last_timestamp INTEGER,
// sort_timestamp INTEGER,
// last_clear_unread_timestamp INTEGER,
// last_msg_locald_id INTEGER,
// last_msg_type INTEGER,
// last_msg_sub_type INTEGER,
// last_msg_sender TEXT,
// last_sender_display_name TEXT,
// last_msg_ext_type INTEGER
// )
type SessionV4 struct {
Username string `json:"username"`
Summary string `json:"summary"`
LastTimestamp int `json:"last_timestamp"`
LastMsgSender string `json:"last_msg_sender"`
LastSenderDisplayName string `json:"last_sender_display_name"`
// Type int `json:"type"`
// UnreadCount int `json:"unread_count"`
// UnreadFirstMsgSrvID int `json:"unread_first_msg_srv_id"`
// IsHidden int `json:"is_hidden"`
// Draft string `json:"draft"`
// Status int `json:"status"`
// SortTimestamp int `json:"sort_timestamp"`
// LastClearUnreadTimestamp int `json:"last_clear_unread_timestamp"`
// LastMsgLocaldID int `json:"last_msg_locald_id"`
// LastMsgType int `json:"last_msg_type"`
// LastMsgSubType int `json:"last_msg_sub_type"`
// LastMsgExtType int `json:"last_msg_ext_type"`
}
func (s *Session) PlainText(limit int) string {
buf := strings.Builder{}
buf.WriteString(s.NickName)
buf.WriteString("(")
buf.WriteString(s.UserName)
buf.WriteString(") ")
buf.WriteString(s.NTime.Format("2006-01-02 15:04:05"))
buf.WriteString("\n")
if limit > 0 {
if len(s.Content) > limit {
buf.WriteString(s.Content[:limit])
buf.WriteString(" <...>")
} else {
buf.WriteString(s.Content)
}
}
buf.WriteString("\n")
return buf.String()
}

View File

@@ -0,0 +1,254 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v5.29.3
// source: bytesextra.proto
package wxproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type BytesExtraHeader struct {
state protoimpl.MessageState `protogen:"open.v1"`
Field1 int32 `protobuf:"varint,1,opt,name=field1,proto3" json:"field1,omitempty"`
Field2 int32 `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BytesExtraHeader) Reset() {
*x = BytesExtraHeader{}
mi := &file_bytesextra_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BytesExtraHeader) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BytesExtraHeader) ProtoMessage() {}
func (x *BytesExtraHeader) ProtoReflect() protoreflect.Message {
mi := &file_bytesextra_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BytesExtraHeader.ProtoReflect.Descriptor instead.
func (*BytesExtraHeader) Descriptor() ([]byte, []int) {
return file_bytesextra_proto_rawDescGZIP(), []int{0}
}
func (x *BytesExtraHeader) GetField1() int32 {
if x != nil {
return x.Field1
}
return 0
}
func (x *BytesExtraHeader) GetField2() int32 {
if x != nil {
return x.Field2
}
return 0
}
type BytesExtraItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type int32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BytesExtraItem) Reset() {
*x = BytesExtraItem{}
mi := &file_bytesextra_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BytesExtraItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BytesExtraItem) ProtoMessage() {}
func (x *BytesExtraItem) ProtoReflect() protoreflect.Message {
mi := &file_bytesextra_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BytesExtraItem.ProtoReflect.Descriptor instead.
func (*BytesExtraItem) Descriptor() ([]byte, []int) {
return file_bytesextra_proto_rawDescGZIP(), []int{1}
}
func (x *BytesExtraItem) GetType() int32 {
if x != nil {
return x.Type
}
return 0
}
func (x *BytesExtraItem) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
type BytesExtra struct {
state protoimpl.MessageState `protogen:"open.v1"`
Header *BytesExtraHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
Items []*BytesExtraItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BytesExtra) Reset() {
*x = BytesExtra{}
mi := &file_bytesextra_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BytesExtra) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BytesExtra) ProtoMessage() {}
func (x *BytesExtra) ProtoReflect() protoreflect.Message {
mi := &file_bytesextra_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BytesExtra.ProtoReflect.Descriptor instead.
func (*BytesExtra) Descriptor() ([]byte, []int) {
return file_bytesextra_proto_rawDescGZIP(), []int{2}
}
func (x *BytesExtra) GetHeader() *BytesExtraHeader {
if x != nil {
return x.Header
}
return nil
}
func (x *BytesExtra) GetItems() []*BytesExtraItem {
if x != nil {
return x.Items
}
return nil
}
var File_bytesextra_proto protoreflect.FileDescriptor
var file_bytesextra_proto_rawDesc = string([]byte{
0x0a, 0x10, 0x62, 0x79, 0x74, 0x65, 0x73, 0x65, 0x78, 0x74, 0x72, 0x61, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x22, 0x42, 0x0a, 0x10, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65,
0x61, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x12, 0x16, 0x0a, 0x06,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x32, 0x22, 0x3a, 0x0a, 0x0e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74,
0x72, 0x61, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x22, 0x78, 0x0a, 0x0a, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x12, 0x36,
0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e,
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79,
0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18,
0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49,
0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b,
0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
file_bytesextra_proto_rawDescOnce sync.Once
file_bytesextra_proto_rawDescData []byte
)
func file_bytesextra_proto_rawDescGZIP() []byte {
file_bytesextra_proto_rawDescOnce.Do(func() {
file_bytesextra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)))
})
return file_bytesextra_proto_rawDescData
}
var file_bytesextra_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_bytesextra_proto_goTypes = []any{
(*BytesExtraHeader)(nil), // 0: app.protobuf.BytesExtraHeader
(*BytesExtraItem)(nil), // 1: app.protobuf.BytesExtraItem
(*BytesExtra)(nil), // 2: app.protobuf.BytesExtra
}
var file_bytesextra_proto_depIdxs = []int32{
0, // 0: app.protobuf.BytesExtra.header:type_name -> app.protobuf.BytesExtraHeader
1, // 1: app.protobuf.BytesExtra.items:type_name -> app.protobuf.BytesExtraItem
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_bytesextra_proto_init() }
func file_bytesextra_proto_init() {
if File_bytesextra_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_bytesextra_proto_rawDesc), len(file_bytesextra_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_bytesextra_proto_goTypes,
DependencyIndexes: file_bytesextra_proto_depIdxs,
MessageInfos: file_bytesextra_proto_msgTypes,
}.Build()
File_bytesextra_proto = out.File
file_bytesextra_proto_goTypes = nil
file_bytesextra_proto_depIdxs = nil
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
package app.protobuf;
option go_package=".;wxproto";
message BytesExtraHeader {
int32 field1 = 1;
int32 field2 = 2;
}
message BytesExtraItem {
int32 type = 1;
string value = 2;
}
message BytesExtra {
BytesExtraHeader header = 1;
repeated BytesExtraItem items = 3;
}

View File

@@ -0,0 +1,222 @@
// v3 & v4 通用,可能会有部分字段差异
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v5.29.3
// source: roomdata.proto
package wxproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type RoomData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*RoomDataUser `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
RoomCap *int32 `protobuf:"varint,5,opt,name=roomCap,proto3,oneof" json:"roomCap,omitempty"` // 只在第一份数据中出现值为500
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomData) Reset() {
*x = RoomData{}
mi := &file_roomdata_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomData) ProtoMessage() {}
func (x *RoomData) ProtoReflect() protoreflect.Message {
mi := &file_roomdata_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoomData.ProtoReflect.Descriptor instead.
func (*RoomData) Descriptor() ([]byte, []int) {
return file_roomdata_proto_rawDescGZIP(), []int{0}
}
func (x *RoomData) GetUsers() []*RoomDataUser {
if x != nil {
return x.Users
}
return nil
}
func (x *RoomData) GetRoomCap() int32 {
if x != nil && x.RoomCap != nil {
return *x.RoomCap
}
return 0
}
type RoomDataUser struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserName string `protobuf:"bytes,1,opt,name=userName,proto3" json:"userName,omitempty"` // 用户ID或名称
DisplayName *string `protobuf:"bytes,2,opt,name=displayName,proto3,oneof" json:"displayName,omitempty"` // 显示名称可能是UTF-8编码的中文部分记录可能为空
Status int32 `protobuf:"varint,3,opt,name=status,proto3" json:"status,omitempty"` // 状态码值范围0-9
Inviter *string `protobuf:"bytes,4,opt,name=inviter,proto3,oneof" json:"inviter,omitempty"` // 邀请人
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomDataUser) Reset() {
*x = RoomDataUser{}
mi := &file_roomdata_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomDataUser) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomDataUser) ProtoMessage() {}
func (x *RoomDataUser) ProtoReflect() protoreflect.Message {
mi := &file_roomdata_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoomDataUser.ProtoReflect.Descriptor instead.
func (*RoomDataUser) Descriptor() ([]byte, []int) {
return file_roomdata_proto_rawDescGZIP(), []int{1}
}
func (x *RoomDataUser) GetUserName() string {
if x != nil {
return x.UserName
}
return ""
}
func (x *RoomDataUser) GetDisplayName() string {
if x != nil && x.DisplayName != nil {
return *x.DisplayName
}
return ""
}
func (x *RoomDataUser) GetStatus() int32 {
if x != nil {
return x.Status
}
return 0
}
func (x *RoomDataUser) GetInviter() string {
if x != nil && x.Inviter != nil {
return *x.Inviter
}
return ""
}
var File_roomdata_proto protoreflect.FileDescriptor
var file_roomdata_proto_rawDesc = string([]byte{
0x0a, 0x0e, 0x72, 0x6f, 0x6f, 0x6d, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x67,
0x0a, 0x08, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x73,
0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x70, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x61, 0x74,
0x61, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x07,
0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52,
0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
0x72, 0x6f, 0x6f, 0x6d, 0x43, 0x61, 0x70, 0x22, 0xa4, 0x01, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d,
0x44, 0x61, 0x74, 0x61, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72,
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e,
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x69, 0x73,
0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x16, 0x0a, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x88,
0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61,
0x6d, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x42, 0x0b,
0x5a, 0x09, 0x2e, 0x3b, 0x77, 0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
})
var (
file_roomdata_proto_rawDescOnce sync.Once
file_roomdata_proto_rawDescData []byte
)
func file_roomdata_proto_rawDescGZIP() []byte {
file_roomdata_proto_rawDescOnce.Do(func() {
file_roomdata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)))
})
return file_roomdata_proto_rawDescData
}
var file_roomdata_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_roomdata_proto_goTypes = []any{
(*RoomData)(nil), // 0: app.protobuf.RoomData
(*RoomDataUser)(nil), // 1: app.protobuf.RoomDataUser
}
var file_roomdata_proto_depIdxs = []int32{
1, // 0: app.protobuf.RoomData.users:type_name -> app.protobuf.RoomDataUser
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_roomdata_proto_init() }
func file_roomdata_proto_init() {
if File_roomdata_proto != nil {
return
}
file_roomdata_proto_msgTypes[0].OneofWrappers = []any{}
file_roomdata_proto_msgTypes[1].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_roomdata_proto_rawDesc), len(file_roomdata_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_roomdata_proto_goTypes,
DependencyIndexes: file_roomdata_proto_depIdxs,
MessageInfos: file_roomdata_proto_msgTypes,
}.Build()
File_roomdata_proto = out.File
file_roomdata_proto_goTypes = nil
file_roomdata_proto_depIdxs = nil
}

View File

@@ -0,0 +1,16 @@
// v3 & v4 通用,可能会有部分字段差异
syntax = "proto3";
package app.protobuf;
option go_package=".;wxproto";
message RoomData {
repeated RoomDataUser users = 1;
optional int32 roomCap = 5; // 只在第一份数据中出现值为500
}
message RoomDataUser {
string userName = 1; // 用户ID或名称
optional string displayName = 2; // 显示名称可能是UTF-8编码的中文部分记录可能为空
int32 status = 3; // 状态码值范围0-9
optional string inviter = 4; // 邀请人
}

135
pkg/util/os.go Normal file
View File

@@ -0,0 +1,135 @@
package util
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"runtime"
log "github.com/sirupsen/logrus"
)
// FindFilesWithPatterns 在指定目录下查找匹配多个正则表达式的文件
// directory: 要搜索的目录路径
// patterns: 正则表达式模式列表
// recursive: 是否递归搜索子目录
// 返回匹配的文件路径列表和可能的错误
func FindFilesWithPatterns(directory string, pattern string, recursive bool) ([]string, error) {
// 编译所有正则表达式
re, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("无效的正则表达式 '%s': %v", pattern, err)
}
// 检查目录是否存在
dirInfo, err := os.Stat(directory)
if err != nil {
return nil, fmt.Errorf("无法访问目录 '%s': %v", directory, err)
}
if !dirInfo.IsDir() {
return nil, fmt.Errorf("'%s' 不是一个目录", directory)
}
// 存储匹配的文件路径
var matchedFiles []string
// 创建文件系统
fsys := os.DirFS(directory)
// 遍历文件系统
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// 如果是目录且不递归,则跳过子目录
if d.IsDir() {
if !recursive && path != "." {
return fs.SkipDir
}
return nil
}
// 检查文件名是否匹配任何一个正则表达式
if re.MatchString(d.Name()) {
// 添加完整路径到结果列表
fullPath := filepath.Join(directory, path)
matchedFiles = append(matchedFiles, fullPath)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("遍历目录时出错: %v", err)
}
return matchedFiles, nil
}
func DefaultWorkDir(account string) string {
if len(account) == 0 {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.ExpandEnv("${USERPROFILE}"), "Documents", "chatlog")
case "darwin":
return filepath.Join(os.ExpandEnv("${HOME}"), "Documents", "chatlog")
default:
return filepath.Join(os.ExpandEnv("${HOME}"), "chatlog")
}
}
switch runtime.GOOS {
case "windows":
return filepath.Join(os.ExpandEnv("${USERPROFILE}"), "Documents", "chatlog", account)
case "darwin":
return filepath.Join(os.ExpandEnv("${HOME}"), "Documents", "chatlog", account)
default:
return filepath.Join(os.ExpandEnv("${HOME}"), "chatlog", account)
}
}
func GetDirSize(dir string) string {
var size int64
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err == nil {
size += info.Size()
}
return nil
})
return ByteCountSI(size)
}
func ByteCountSI(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
// PrepareDir ensures that the specified directory path exists.
// If the directory does not exist, it attempts to create it.
func PrepareDir(path string) error {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
} else {
return err
}
} else if !stat.IsDir() {
log.Debugf("%s is not a directory", path)
return fmt.Errorf("%s is not a directory", path)
}
return nil
}

15
pkg/util/os_windows.go Normal file
View File

@@ -0,0 +1,15 @@
package util
import (
"fmt"
"golang.org/x/sys/windows"
)
func Is64Bit(handle windows.Handle) (bool, error) {
var is32Bit bool
if err := windows.IsWow64Process(handle, &is32Bit); err != nil {
return false, fmt.Errorf("检查进程位数失败: %w", err)
}
return !is32Bit, nil
}

34
pkg/util/strings.go Normal file
View File

@@ -0,0 +1,34 @@
package util
import (
"fmt"
"strconv"
"unicode"
"unicode/utf8"
)
func IsNormalString(b []byte) bool {
str := string(b)
// 检查是否为有效的 UTF-8
if !utf8.ValidString(str) {
return false
}
// 检查是否全部为可打印字符
for _, r := range str {
if !unicode.IsPrint(r) {
return false
}
}
return true
}
func MustAnyToInt(v interface{}) int {
str := fmt.Sprintf("%v", v)
if i, err := strconv.Atoi(str); err == nil {
return i
}
return 0
}

636
pkg/util/time.go Normal file
View File

@@ -0,0 +1,636 @@
package util
import (
"regexp"
"strconv"
"strings"
"time"
)
var zoneStr = time.Now().Format("-0700")
// 时间粒度常量
type TimeGranularity int
const (
GranularityUnknown TimeGranularity = iota // 未知粒度
GranularitySecond // 精确到秒
GranularityMinute // 精确到分钟
GranularityHour // 精确到小时
GranularityDay // 精确到天
GranularityMonth // 精确到月
GranularityQuarter // 精确到季度
GranularityYear // 精确到年
)
// timeOf 内部函数,解析各种格式的时间点,并返回时间粒度
// 支持以下格式:
// 1. 时间戳(秒): 1609459200 (GranularitySecond)
// 2. 标准日期: 20060102, 2006-01-02 (GranularityDay)
// 3. 带时间的日期: 20060102/15:04, 2006-01-02/15:04 (GranularityMinute)
// 4. 完整时间: 20060102150405 (GranularitySecond)
// 5. RFC3339: 2006-01-02T15:04:05Z07:00 (GranularitySecond)
// 6. 相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago (根据单位确定粒度)
// 7. 自然语言: now (GranularitySecond), today, yesterday (GranularityDay)
// 8. 年份: 2006 (GranularityYear)
// 9. 月份: 200601, 2006-01 (GranularityMonth)
// 10. 季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4 (GranularityQuarter)
// 11. 年月日时分: 200601021504 (GranularityMinute)
func timeOf(str string) (t time.Time, g TimeGranularity, ok bool) {
if str == "" {
return time.Time{}, GranularityUnknown, false
}
str = strings.TrimSpace(str)
// 处理自然语言时间
switch strings.ToLower(str) {
case "now":
return time.Now(), GranularitySecond, true
case "today":
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
case "yesterday":
now := time.Now().AddDate(0, 0, -1)
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
case "this-week":
now := time.Now()
weekday := int(now.Weekday())
if weekday == 0 { // 周日
weekday = 7
}
// 本周一
monday := now.AddDate(0, 0, -(weekday - 1))
return time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
case "last-week":
now := time.Now()
weekday := int(now.Weekday())
if weekday == 0 { // 周日
weekday = 7
}
// 上周一
lastMonday := now.AddDate(0, 0, -(weekday-1)-7)
return time.Date(lastMonday.Year(), lastMonday.Month(), lastMonday.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
case "this-month":
now := time.Now()
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()), GranularityMonth, true
case "last-month":
now := time.Now()
return time.Date(now.Year(), now.Month()-1, 1, 0, 0, 0, 0, now.Location()), GranularityMonth, true
case "this-year":
now := time.Now()
return time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location()), GranularityYear, true
case "last-year":
now := time.Now()
return time.Date(now.Year()-1, 1, 1, 0, 0, 0, 0, now.Location()), GranularityYear, true
case "all":
// 返回零值时间
return time.Time{}, GranularityYear, true
}
// 处理相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago
if strings.HasSuffix(str, "-ago") {
str = strings.TrimSuffix(str, "-ago")
// 特殊处理 0d-ago 为当天开始
if str == "0d" {
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), GranularityDay, true
}
// 解析数字和单位
re := regexp.MustCompile(`^(\d+)([hdwmy])$`)
matches := re.FindStringSubmatch(str)
if len(matches) == 3 {
num, err := strconv.Atoi(matches[1])
if err != nil {
return time.Time{}, GranularityUnknown, false
}
// 确保数字是正数
if num <= 0 {
// 对于0d-ago已经特殊处理其他0或负数都是无效的
if num == 0 && matches[2] != "d" {
return time.Time{}, GranularityUnknown, false
}
return time.Time{}, GranularityUnknown, false
}
now := time.Now()
var resultTime time.Time
var granularity TimeGranularity
switch matches[2] {
case "h": // 小时
resultTime = now.Add(-time.Duration(num) * time.Hour)
granularity = GranularityHour
case "d": // 天
resultTime = now.AddDate(0, 0, -num)
granularity = GranularityDay
case "w": // 周
resultTime = now.AddDate(0, 0, -num*7)
granularity = GranularityDay
case "m": // 月
resultTime = now.AddDate(0, -num, 0)
granularity = GranularityMonth
case "y": // 年
resultTime = now.AddDate(-num, 0, 0)
granularity = GranularityYear
default:
return time.Time{}, GranularityUnknown, false
}
return resultTime, granularity, true
}
// 尝试标准 duration 解析
dur, err := time.ParseDuration(str)
if err == nil {
// 根据duration单位确定粒度
hours := dur.Hours()
if hours < 1 {
return time.Now().Add(-dur), GranularitySecond, true
} else if hours < 24 {
return time.Now().Add(-dur), GranularityHour, true
} else {
return time.Now().Add(-dur), GranularityDay, true
}
}
return time.Time{}, GranularityUnknown, false
}
// 处理季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4
if matched, _ := regexp.MatchString(`^\d{4}Q[1-4]$`, str); matched {
re := regexp.MustCompile(`^(\d{4})Q([1-4])$`)
matches := re.FindStringSubmatch(str)
if len(matches) == 3 {
year, _ := strconv.Atoi(matches[1])
quarter, _ := strconv.Atoi(matches[2])
// 验证年份范围
if year < 1970 || year > 9999 {
return time.Time{}, GranularityUnknown, false
}
// 计算季度的开始月份
startMonth := time.Month((quarter-1)*3 + 1)
return time.Date(year, startMonth, 1, 0, 0, 0, 0, time.Local), GranularityQuarter, true
}
}
// 处理年份: 2006
if len(str) == 4 && isDigitsOnly(str) {
year, err := strconv.Atoi(str)
if err == nil && year >= 1970 && year <= 9999 {
return time.Date(year, 1, 1, 0, 0, 0, 0, time.Local), GranularityYear, true
}
return time.Time{}, GranularityUnknown, false
}
// 处理月份: 200601 或 2006-01
if (len(str) == 6 && isDigitsOnly(str)) || (len(str) == 7 && strings.Count(str, "-") == 1) {
var year, month int
var err error
if len(str) == 6 && isDigitsOnly(str) {
year, err = strconv.Atoi(str[0:4])
if err != nil {
return time.Time{}, GranularityUnknown, false
}
month, err = strconv.Atoi(str[4:6])
if err != nil {
return time.Time{}, GranularityUnknown, false
}
} else { // 2006-01
parts := strings.Split(str, "-")
if len(parts) != 2 {
return time.Time{}, GranularityUnknown, false
}
year, err = strconv.Atoi(parts[0])
if err != nil {
return time.Time{}, GranularityUnknown, false
}
month, err = strconv.Atoi(parts[1])
if err != nil {
return time.Time{}, GranularityUnknown, false
}
}
if year < 1970 || year > 9999 || month < 1 || month > 12 {
return time.Time{}, GranularityUnknown, false
}
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local), GranularityMonth, true
}
// 处理日期格式: 20060102 或 2006-01-02
if len(str) == 8 && isDigitsOnly(str) {
// 验证年月日
year, _ := strconv.Atoi(str[0:4])
month, _ := strconv.Atoi(str[4:6])
day, _ := strconv.Atoi(str[6:8])
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
return time.Time{}, GranularityUnknown, false
}
// 进一步验证日期是否有效
if !isValidDate(year, month, day) {
return time.Time{}, GranularityUnknown, false
}
// 直接构造时间
result := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
return result, GranularityDay, true
} else if len(str) == 10 && strings.Count(str, "-") == 2 {
// 验证年月日
parts := strings.Split(str, "-")
if len(parts) != 3 {
return time.Time{}, GranularityUnknown, false
}
year, err1 := strconv.Atoi(parts[0])
month, err2 := strconv.Atoi(parts[1])
day, err3 := strconv.Atoi(parts[2])
if err1 != nil || err2 != nil || err3 != nil {
return time.Time{}, GranularityUnknown, false
}
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
return time.Time{}, GranularityUnknown, false
}
// 进一步验证日期是否有效
if !isValidDate(year, month, day) {
return time.Time{}, GranularityUnknown, false
}
// 直接构造时间
result := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
return result, GranularityDay, true
}
// 处理年月日时分: 200601021504
if len(str) == 12 && isDigitsOnly(str) {
year, _ := strconv.Atoi(str[0:4])
month, _ := strconv.Atoi(str[4:6])
day, _ := strconv.Atoi(str[6:8])
hour, _ := strconv.Atoi(str[8:10])
minute, _ := strconv.Atoi(str[10:12])
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 ||
hour < 0 || hour > 23 || minute < 0 || minute > 59 {
return time.Time{}, GranularityUnknown, false
}
// 进一步验证日期是否有效
if !isValidDate(year, month, day) {
return time.Time{}, GranularityUnknown, false
}
// 直接构造时间
result := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local)
return result, GranularityMinute, true
}
// 处理带时间的日期: 20060102/15:04 或 2006-01-02/15:04
if strings.Contains(str, "/") {
parts := strings.Split(str, "/")
if len(parts) != 2 {
return time.Time{}, GranularityUnknown, false
}
datePart := parts[0]
timePart := parts[1]
// 验证日期部分
var year, month, day int
var err1, err2, err3 error
if len(datePart) == 8 && isDigitsOnly(datePart) {
year, err1 = strconv.Atoi(datePart[0:4])
month, err2 = strconv.Atoi(datePart[4:6])
day, err3 = strconv.Atoi(datePart[6:8])
} else if len(datePart) == 10 && strings.Count(datePart, "-") == 2 {
dateParts := strings.Split(datePart, "-")
if len(dateParts) != 3 {
return time.Time{}, GranularityUnknown, false
}
year, err1 = strconv.Atoi(dateParts[0])
month, err2 = strconv.Atoi(dateParts[1])
day, err3 = strconv.Atoi(dateParts[2])
} else {
return time.Time{}, GranularityUnknown, false
}
if err1 != nil || err2 != nil || err3 != nil {
return time.Time{}, GranularityUnknown, false
}
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 {
return time.Time{}, GranularityUnknown, false
}
// 进一步验证日期是否有效
if !isValidDate(year, month, day) {
return time.Time{}, GranularityUnknown, false
}
// 验证时间部分
if !regexp.MustCompile(`^\d{2}:\d{2}$`).MatchString(timePart) {
return time.Time{}, GranularityUnknown, false
}
timeParts := strings.Split(timePart, ":")
hour, err1 := strconv.Atoi(timeParts[0])
minute, err2 := strconv.Atoi(timeParts[1])
if err1 != nil || err2 != nil {
return time.Time{}, GranularityUnknown, false
}
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
return time.Time{}, GranularityUnknown, false
}
// 直接构造时间
result := time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.Local)
return result, GranularityMinute, true
}
// 处理完整时间: 20060102150405
if len(str) == 14 && isDigitsOnly(str) {
year, _ := strconv.Atoi(str[0:4])
month, _ := strconv.Atoi(str[4:6])
day, _ := strconv.Atoi(str[6:8])
hour, _ := strconv.Atoi(str[8:10])
minute, _ := strconv.Atoi(str[10:12])
second, _ := strconv.Atoi(str[12:14])
if year < 1970 || year > 9999 || month < 1 || month > 12 || day < 1 || day > 31 ||
hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return time.Time{}, GranularityUnknown, false
}
// 进一步验证日期是否有效
if !isValidDate(year, month, day) {
return time.Time{}, GranularityUnknown, false
}
// 直接构造时间
result := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
return result, GranularitySecond, true
}
// 处理时间戳(秒)
if isDigitsOnly(str) {
n, err := strconv.ParseInt(str, 10, 64)
if err == nil {
// 检查是否是合理的时间戳范围
if n >= 1000000000 && n <= 253402300799 { // 2001年到2286年的秒级时间戳
return time.Unix(n, 0), GranularitySecond, true
}
}
return time.Time{}, GranularityUnknown, false
}
// 处理 RFC3339: 2006-01-02T15:04:05Z07:00
if strings.Contains(str, "T") && (strings.Contains(str, "Z") || strings.Contains(str, "+") || strings.Contains(str, "-")) {
t, err := time.Parse(time.RFC3339, str)
if err != nil {
// 尝试不带秒的格式
t, err = time.Parse("2006-01-02T15:04Z07:00", str)
}
if err == nil {
return t, GranularitySecond, true
}
}
// 排除所有其他不支持的格式
return time.Time{}, GranularityUnknown, false
}
// TimeOf 解析各种格式的时间点
// 支持以下格式:
// 1. 时间戳(秒): 1609459200
// 2. 标准日期: 20060102, 2006-01-02
// 3. 带时间的日期: 20060102/15:04, 2006-01-02/15:04
// 4. 完整时间: 20060102150405
// 5. RFC3339: 2006-01-02T15:04:05Z07:00
// 6. 相对时间: 5h-ago, 3d-ago, 1w-ago, 1m-ago, 1y-ago (小时、天、周、月、年)
// 7. 自然语言: now, today, yesterday
// 8. 年份: 2006
// 9. 月份: 200601, 2006-01
// 10. 季度: 2006Q1, 2006Q2, 2006Q3, 2006Q4
// 11. 年月日时分: 200601021504
func TimeOf(str string) (t time.Time, ok bool) {
t, _, ok = timeOf(str)
return
}
// TimeRangeOf 解析各种格式的时间范围
// 支持以下格式:
// 1. 单个时间点: 根据时间粒度确定合适的时间范围
// - 精确到秒/分钟/小时: 扩展为当天范围
// - 精确到天: 当天 00:00:00 ~ 23:59:59
// - 精确到月: 当月第一天 ~ 最后一天
// - 精确到季度: 季度第一天 ~ 最后一天
// - 精确到年: 当年第一天 ~ 最后一天
//
// 2. 时间区间: 2006-01-01~2006-01-31, 2006-01-01,2006-01-31, 2006-01-01 to 2006-01-31
// 3. 相对时间: last-7d, last-30d, last-3m, last-1y (最近7天、30天、3个月、1年)
// 4. 特定时间段: today, yesterday, this-week, last-week, this-month, last-month, this-year, last-year
// 5. all: 表示所有时间
func TimeRangeOf(str string) (start, end time.Time, ok bool) {
if str == "" {
return time.Time{}, time.Time{}, false
}
str = strings.TrimSpace(str)
// 处理 all 特殊情况
if strings.ToLower(str) == "all" {
start = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC)
return start, end, true
}
// 处理相对时间范围: last-7d, last-30d, last-3m, last-1y
if matched, _ := regexp.MatchString(`^last-\d+[dwmy]$`, str); matched {
re := regexp.MustCompile(`^last-(\d+)([dwmy])$`)
matches := re.FindStringSubmatch(str)
if len(matches) == 3 {
num, err := strconv.Atoi(matches[1])
if err != nil || num <= 0 {
return time.Time{}, time.Time{}, false
}
now := time.Now()
end = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
switch matches[2] {
case "d": // 天
start = now.AddDate(0, 0, -num)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
return start, end, true
case "w": // 周
start = now.AddDate(0, 0, -num*7)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
return start, end, true
case "m": // 月
start = now.AddDate(0, -num, 0)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
return start, end, true
case "y": // 年
start = now.AddDate(-num, 0, 0)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
return start, end, true
}
}
}
// 处理时间区间: 2006-01-01~2006-01-31, 2006-01-01,2006-01-31, 2006-01-01 to 2006-01-31
separators := []string{"~", ",", " to "}
for _, sep := range separators {
if strings.Contains(str, sep) {
parts := strings.Split(str, sep)
if len(parts) == 2 {
startTime, startGran, startOk := timeOf(strings.TrimSpace(parts[0]))
endTime, endGran, endOk := timeOf(strings.TrimSpace(parts[1]))
if startOk && endOk {
// 根据粒度调整时间范围
start = adjustStartTime(startTime, startGran)
end = adjustEndTime(endTime, endGran)
// 确保开始时间早于结束时间
if start.After(end) {
// 正确交换开始和结束时间
start, end = adjustStartTime(endTime, endGran), adjustEndTime(startTime, startGran)
}
return start, end, true
}
}
}
}
// 处理单个时间点,根据粒度确定合适的时间范围
t, g, ok := timeOf(str)
if ok {
switch g {
case GranularitySecond, GranularityMinute, GranularityHour:
// 精确到秒/分钟/小时的时间点,扩展为当天范围
start = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
end = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
case GranularityDay:
// 精确到天的时间点
start = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
end = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
case GranularityMonth:
// 精确到月的时间点
start = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
end = time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 999999999, t.Location())
case GranularityQuarter:
// 精确到季度的时间点
quarter := (t.Month()-1)/3 + 1
startMonth := time.Month((int(quarter)-1)*3 + 1)
endMonth := startMonth + 2
start = time.Date(t.Year(), startMonth, 1, 0, 0, 0, 0, t.Location())
end = time.Date(t.Year(), endMonth+1, 0, 23, 59, 59, 999999999, t.Location())
case GranularityYear:
// 精确到年的时间点
start = time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
end = time.Date(t.Year(), 12, 31, 23, 59, 59, 999999999, t.Location())
}
return start, end, true
}
return time.Time{}, time.Time{}, false
}
// adjustStartTime 根据时间粒度调整开始时间
func adjustStartTime(t time.Time, g TimeGranularity) time.Time {
switch g {
case GranularitySecond, GranularityMinute, GranularityHour:
// 对于精确到秒/分钟/小时的时间,保持原样
return t
case GranularityDay:
// 精确到天,设置为当天开始
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
case GranularityMonth:
// 精确到月,设置为当月第一天
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
case GranularityQuarter:
// 精确到季度,设置为季度第一天
quarter := (t.Month()-1)/3 + 1
startMonth := time.Month((int(quarter)-1)*3 + 1)
return time.Date(t.Year(), startMonth, 1, 0, 0, 0, 0, t.Location())
case GranularityYear:
// 精确到年,设置为当年第一天
return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location())
default:
// 未知粒度,默认为当天开始
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
}
// adjustEndTime 根据时间粒度调整结束时间
func adjustEndTime(t time.Time, g TimeGranularity) time.Time {
switch g {
case GranularitySecond, GranularityMinute, GranularityHour:
// 对于精确到秒/分钟/小时的时间,设置为当天结束
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
case GranularityDay:
// 精确到天,设置为当天结束
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
case GranularityMonth:
// 精确到月,设置为当月最后一天
return time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 999999999, t.Location())
case GranularityQuarter:
// 精确到季度,设置为季度最后一天
quarter := (t.Month()-1)/3 + 1
startMonth := time.Month((int(quarter)-1)*3 + 1)
endMonth := startMonth + 2
return time.Date(t.Year(), endMonth+1, 0, 23, 59, 59, 999999999, t.Location())
case GranularityYear:
// 精确到年,设置为当年最后一天
return time.Date(t.Year(), 12, 31, 23, 59, 59, 999999999, t.Location())
default:
// 未知粒度,默认为当天结束
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, t.Location())
}
}
// isDigitsOnly 检查字符串是否只包含数字
func isDigitsOnly(s string) bool {
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return len(s) > 0
}
// isValidDate 检查日期是否有效
func isValidDate(year, month, day int) bool {
// 检查月份的天数
daysInMonth := 31
switch month {
case 4, 6, 9, 11:
daysInMonth = 30
case 2:
// 闰年判断
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
daysInMonth = 29
} else {
daysInMonth = 28
}
}
return day <= daysInMonth
}

1004
pkg/util/time_test.go Normal file

File diff suppressed because it is too large Load Diff

32
pkg/version/version.go Normal file
View File

@@ -0,0 +1,32 @@
package version
import (
"fmt"
"runtime"
"runtime/debug"
"strings"
)
var (
Version = "(dev)"
buildInfo = debug.BuildInfo{}
)
func init() {
if bi, ok := debug.ReadBuildInfo(); ok {
buildInfo = *bi
if len(bi.Main.Version) > 0 {
Version = bi.Main.Version
}
}
}
func GetMore(mod bool) string {
if mod {
mod := buildInfo.String()
if len(mod) > 0 {
return fmt.Sprintf("\t%s\n", strings.ReplaceAll(mod[:len(mod)-1], "\n", "\n\t"))
}
}
return fmt.Sprintf("version %s %s %s/%s\n", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}