github.com/fumiama/NanoBot@v0.0.0-20231122134259-c22d8183efca/context.go (about)

     1  package nano
     2  
     3  import (
     4  	"encoding/base64"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  
    13  	base14 "github.com/fumiama/go-base16384"
    14  	"github.com/fumiama/imoto"
    15  	"github.com/sirupsen/logrus"
    16  )
    17  
    18  //go:generate go run codegen/context/main.go
    19  
    20  type Ctx struct {
    21  	Event
    22  	State
    23  	Message *Message
    24  	IsToMe  bool
    25  	IsQQ    bool
    26  
    27  	caller *Bot
    28  	ma     *Matcher
    29  }
    30  
    31  // decoder 反射获取的数据
    32  type decoder []dec
    33  
    34  type dec struct {
    35  	index int
    36  	key   string
    37  }
    38  
    39  // decoder 缓存
    40  var decoderCache = sync.Map{}
    41  
    42  // Parse 将 Ctx.State 映射到结构体
    43  func (ctx *Ctx) Parse(model interface{}) (err error) {
    44  	var (
    45  		rv       = reflect.ValueOf(model).Elem()
    46  		t        = rv.Type()
    47  		modelDec decoder
    48  	)
    49  	defer func() {
    50  		if r := recover(); r != nil {
    51  			err = fmt.Errorf("parse state error: %v", r)
    52  		}
    53  	}()
    54  	d, ok := decoderCache.Load(t)
    55  	if ok {
    56  		modelDec = d.(decoder)
    57  	} else {
    58  		modelDec = decoder{}
    59  		for i := 0; i < t.NumField(); i++ {
    60  			t1 := t.Field(i)
    61  			if key, ok := t1.Tag.Lookup("zero"); ok {
    62  				modelDec = append(modelDec, dec{
    63  					index: i,
    64  					key:   key,
    65  				})
    66  			}
    67  		}
    68  		decoderCache.Store(t, modelDec)
    69  	}
    70  	for _, d := range modelDec { // decoder类型非小内存,无法被编译器优化为快速拷贝
    71  		rv.Field(d.index).Set(reflect.ValueOf(ctx.State[d.key]))
    72  	}
    73  	return nil
    74  }
    75  
    76  // CheckSession 判断会话连续性
    77  func (ctx *Ctx) CheckSession() Rule {
    78  	msg := ctx.Value.(*Message)
    79  	return func(ctx2 *Ctx) bool {
    80  		msg2, ok := ctx.Value.(*Message)
    81  		if !ok || msg.Author == nil || msg2.Author == nil { // 确保无空
    82  			return false
    83  		}
    84  		return msg.Author.ID == msg2.Author.ID && msg.ChannelID == msg2.ChannelID
    85  	}
    86  }
    87  
    88  var imotoken = "f7f06a63b8c111df0d4faa256cd5ba35cb98678ee8274923576d0416b52fe768"
    89  
    90  // Send 发送一批消息
    91  func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) {
    92  	isnextreply := false
    93  	textlist := []any{}
    94  	var reply *Message
    95  	for _, msg := range messages {
    96  		switch msg.Type {
    97  		case MessageSegmentTypeText:
    98  			textlist = append(textlist, msg.Data)
    99  		case MessageSegmentTypeImage:
   100  			reply, err = ctx.SendImage(msg.Data, isnextreply, textlist...)
   101  			if isnextreply {
   102  				isnextreply = false
   103  			}
   104  			textlist = textlist[:0]
   105  			m = append(m, reply)
   106  			if err != nil {
   107  				return
   108  			}
   109  		case MessageSegmentTypeImageBytes:
   110  			reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...)
   111  			if isnextreply {
   112  				isnextreply = false
   113  			}
   114  			textlist = textlist[:0]
   115  			m = append(m, reply)
   116  			if err != nil {
   117  				return
   118  			}
   119  		case MessageSegmentTypeReply:
   120  			isnextreply = true
   121  		case MessageSegmentTypeAudio, MessageSegmentTypeVideo:
   122  			if !ctx.IsQQ {
   123  				continue
   124  			}
   125  			fp := &FilePost{
   126  				URL: msg.Data,
   127  			}
   128  			if msg.Type == MessageSegmentTypeAudio {
   129  				fp.Type = FileTypeAudio
   130  			} else if msg.Type == MessageSegmentTypeVideo {
   131  				fp.Type = FileTypeVideo
   132  			}
   133  			if OnlyQQGroup(ctx) {
   134  				reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp)
   135  			} else if OnlyQQPrivate(ctx) {
   136  				reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp)
   137  			}
   138  			if err != nil {
   139  				return
   140  			}
   141  			logrus.Infoln(getLogHeader(), "=> 上传:", reply)
   142  			reply, err = ctx.Post(isnextreply, &MessagePost{
   143  				Content: " ",
   144  				Media:   &MessageMedia{FileInfo: reply.FileInfo},
   145  			})
   146  			m = append(m, reply)
   147  			if err != nil {
   148  				return
   149  			}
   150  		}
   151  	}
   152  	if len(textlist) > 0 {
   153  		reply, err = ctx.SendPlainMessage(isnextreply, textlist...)
   154  		m = append(m, reply)
   155  	}
   156  	return
   157  }
   158  
   159  // SendChain 链式发送
   160  func (ctx *Ctx) SendChain(message ...MessageSegment) (m []*Message, err error) {
   161  	return ctx.Send(message)
   162  }
   163  
   164  // Post 发送消息到对方
   165  func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err error) {
   166  	msg := ctx.Message
   167  	if msg != nil {
   168  		post.ReplyMessageID = msg.ID
   169  		if OnlyGuild(ctx) && replytosender {
   170  			post.MessageReference = &MessageReference{
   171  				MessageID: msg.ID,
   172  			}
   173  		}
   174  	} else {
   175  		post.ReplyMessageID = "MESSAGE_CREATE"
   176  	}
   177  
   178  	if OnlyDirect(ctx) { // dms
   179  		reply, err = ctx.PostMessageToUser(msg.GuildID, post)
   180  	} else if OnlyChannel(ctx) {
   181  		reply, err = ctx.PostMessageToChannel(msg.ChannelID, post)
   182  	} else { // v2
   183  		switch {
   184  		case post.Markdown != nil:
   185  			post.Type = MessageTypeMarkdown
   186  		case post.Ark != nil:
   187  			post.Type = MessageTypeArk
   188  		case post.Embed != nil:
   189  			post.Type = MessageTypeEmbed
   190  		case post.Media != nil:
   191  			post.Type = MessageTypeMedia
   192  		default:
   193  			post.Type = MessageTypeText
   194  		}
   195  		post.Seq = len(GetTriggeredMessages(msg.ID)) + 1
   196  		if OnlyQQGroup(ctx) {
   197  			reply, err = ctx.PostMessageToQQGroup(msg.ChannelID, post)
   198  		} else if OnlyQQPrivate(ctx) {
   199  			reply, err = ctx.PostMessageToQQUser(msg.ChannelID, post)
   200  		}
   201  		if err == nil {
   202  			logtriggeredmessages(msg.ID, "") // only to log message seq
   203  		}
   204  		return
   205  	}
   206  	if err == nil && msg != nil && reply != nil && reply.ID != "" {
   207  		logtriggeredmessages(msg.ID, reply.ID)
   208  	}
   209  	return
   210  }
   211  
   212  // SendPlainMessage 发送纯文本消息到对方
   213  func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message, error) {
   214  	return ctx.Post(replytosender, &MessagePost{
   215  		Content: HideURL(fmt.Sprint(printable...)),
   216  	})
   217  }
   218  
   219  // SendImage 发送带图片消息到对方
   220  func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (reply *Message, err error) {
   221  	post := &MessagePost{
   222  		Content: HideURL(fmt.Sprint(caption...)),
   223  	}
   224  
   225  	if OnlyQQ(ctx) {
   226  		if strings.HasPrefix(file, "file:///") {
   227  			data, err := os.ReadFile(file[8:])
   228  			if err != nil {
   229  				return nil, err
   230  			}
   231  			return ctx.SendImageBytes(data, replytosender, caption...)
   232  		}
   233  		if strings.HasPrefix(file, "base64://") {
   234  			data, err := base64.StdEncoding.DecodeString(file[9:])
   235  			if err != nil {
   236  				return nil, err
   237  			}
   238  			return ctx.SendImageBytes(data, replytosender, caption...)
   239  		}
   240  		if strings.HasPrefix(file, "base16384://") {
   241  			data := base14.DecodeFromString(file[12:])
   242  			if len(data) == 0 {
   243  				return nil, errors.New("invalid base16384 image")
   244  			}
   245  			return ctx.SendImageBytes(data, replytosender, caption...)
   246  		}
   247  		fp := &FilePost{
   248  			Type: FileTypeImage,
   249  			URL:  file,
   250  		}
   251  		/*if len(caption) > 0 {
   252  			_, _ = ctx.SendPlainMessage(replytosender, caption...)
   253  		}*/
   254  		if post.Content == "" {
   255  			post.Content = " "
   256  		}
   257  		if OnlyQQGroup(ctx) {
   258  			reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp)
   259  		} else if OnlyQQPrivate(ctx) {
   260  			reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp)
   261  		}
   262  		if err != nil {
   263  			return
   264  		}
   265  		logrus.Infoln(getLogHeader(), "=> 上传:", reply)
   266  		post.Media = &MessageMedia{FileInfo: reply.FileInfo}
   267  	} else {
   268  		if strings.HasPrefix(file, "http") {
   269  			post.Image = file
   270  		} else {
   271  			post.ImageFile = file
   272  		}
   273  	}
   274  
   275  	return ctx.Post(replytosender, post)
   276  }
   277  
   278  // SendImageBytes 发送带图片消息到对方
   279  func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) {
   280  	if OnlyQQ(ctx) {
   281  		file, _, _, err := imoto.Bed(imotoken, data)
   282  		if err != nil {
   283  			return nil, err
   284  		}
   285  		return ctx.SendImage(file, replytosender, caption...)
   286  	}
   287  
   288  	post := &MessagePost{
   289  		Content: HideURL(fmt.Sprint(caption...)),
   290  	}
   291  
   292  	post.ImageBytes = data
   293  
   294  	return ctx.Post(replytosender, post)
   295  }
   296  
   297  // Echo 向自身分发虚拟事件
   298  func (ctx *Ctx) Echo(payload *WebsocketPayload) {
   299  	ctx.caller.processEvent(payload)
   300  }
   301  
   302  // FutureEvent ...
   303  func (ctx *Ctx) FutureEvent(Type string, rule ...Rule) *FutureEvent {
   304  	return ctx.ma.FutureEvent(Type, rule...)
   305  }
   306  
   307  // Get 从 promt 获得回复
   308  func (ctx *Ctx) Get(prompt string) string {
   309  	if prompt != "" {
   310  		_, _ = ctx.SendPlainMessage(false, prompt)
   311  	}
   312  	return (<-ctx.FutureEvent("Message", ctx.CheckSession()).Next()).Event.Value.(*Message).Content
   313  }
   314  
   315  // ExtractPlainText 提取消息中的纯文本
   316  func (ctx *Ctx) ExtractPlainText() string {
   317  	if ctx == nil || ctx.Value == nil {
   318  		return ""
   319  	}
   320  	if msg, ok := ctx.Value.(*Message); ok {
   321  		return msg.Content
   322  	}
   323  	return ""
   324  }
   325  
   326  // MessageString 字符串消息便于Regex
   327  func (ctx *Ctx) MessageString() string {
   328  	return ctx.ExtractPlainText()
   329  }
   330  
   331  // Block 匹配成功后阻止后续触发
   332  func (ctx *Ctx) Block() {
   333  	ctx.ma.SetBlock(true)
   334  }
   335  
   336  // Block 在 pre, rules, mid 阶段阻止后续触发
   337  func (ctx *Ctx) Break() {
   338  	ctx.ma.Break = true
   339  }
   340  
   341  // GroupID 唯一的发送者所属组 ID
   342  func (ctx *Ctx) GroupID() uint64 {
   343  	grp := uint64(0)
   344  	if ctx.IsQQ {
   345  		if OnlyQQGroup(ctx) {
   346  			grp = DigestID(ctx.Message.ChannelID)
   347  		} else if OnlyQQPrivate(ctx) {
   348  			grp = DigestID(ctx.Message.Author.ID)
   349  		} else {
   350  			return 0
   351  		}
   352  	} else {
   353  		var err error
   354  		grp, err = strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
   355  		if err != nil {
   356  			return 0
   357  		}
   358  	}
   359  	return grp
   360  }
   361  
   362  // GroupID 唯一的发送者 ID
   363  func (ctx *Ctx) UserID() uint64 {
   364  	if ctx.IsQQ {
   365  		return DigestID(ctx.Message.Author.ID)
   366  	}
   367  	grp, _ := strconv.ParseUint(ctx.Message.Author.ID, 10, 64)
   368  	return grp
   369  }