-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathpattern.go
325 lines (291 loc) · 7.13 KB
/
pattern.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package zero
import (
"regexp"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/wdvxdr1123/ZeroBot/message"
)
const (
KeyPattern = "pattern_matched"
)
// AsRule build PatternRule
func (p *Pattern) AsRule() Rule {
return func(ctx *Ctx) bool {
if len(ctx.Event.Message) == 0 {
return false
}
if !p.cleanRedundantAt && !p.fuzzyAt {
return patternMatch(ctx, *p, ctx.Event.Message)
}
// copy messages
msgs := make([]message.Segment, 0, len(ctx.Event.Message))
for i := 0; i < len(ctx.Event.Message); i++ {
if i > 0 && ctx.Event.Message[i-1].Type == "reply" && ctx.Event.Message[i].Type == "at" {
// [reply][at]
reply := ctx.GetMessage(ctx.Event.Message[i-1].Data["id"])
if reply.MessageID.ID() != 0 && reply.Sender != nil && reply.Sender.ID != 0 && strconv.FormatInt(reply.Sender.ID, 10) == ctx.Event.Message[i].Data["qq"] {
continue
}
}
if ctx.Event.Message[i].Type == "text" && atRegexp.MatchString(ctx.Event.Message[i].Data["text"]) {
// xxxx @11232123 xxxxx
msgs = append(msgs, ctx.splitAtInText(i)...)
continue
}
msgs = append(msgs, ctx.Event.Message[i])
}
return patternMatch(ctx, *p, msgs)
}
}
var atRegexp = regexp.MustCompile(`@([\d\S]*)`)
func (ctx *Ctx) splitAtInText(index int) []message.Segment {
msg := ctx.Event.Message[index].String()
splited := atRegexp.Split(msg, -1)
ats := atRegexp.FindAllStringSubmatch(msg, -1)
var tmp = make([]message.Segment, 0, len(splited)+len(ats))
var list []gjson.Result
for i, s := range splited {
if strings.TrimSpace(s) == "" {
continue
}
tmp = append(tmp, message.Text(s))
// append at
if i > len(ats)-1 {
continue
}
uid, err := strconv.ParseInt(ats[i][1], 10, 64)
// TODO numeric username
if err != nil {
// assume is username
if list == nil {
list = ctx.GetThisGroupMemberList().Array()
}
for _, member := range list {
if member.Get("card").Str != ats[i][1] && member.Get("nickname").Str != ats[i][1] {
continue
}
uid = member.Get("user_id").Int()
}
}
tmp = append(tmp, message.At(uid))
}
return tmp
}
type Pattern struct {
cleanRedundantAt bool
fuzzyAt bool
segments []PatternSegment
}
// PatternOption pattern option
type PatternOption struct {
CleanRedundantAt bool
FuzzyAt bool
}
// NewPattern new pattern
// defaults:
//
// CleanRedundantAt: true
// FuzzyAt: false
func NewPattern(option *PatternOption) *Pattern {
if option == nil {
option = &PatternOption{
CleanRedundantAt: true,
FuzzyAt: false,
}
}
pattern := Pattern{
cleanRedundantAt: option.CleanRedundantAt,
fuzzyAt: option.FuzzyAt,
segments: make([]PatternSegment, 0, 4),
}
return &pattern
}
type PatternSegment struct {
typ string
optional bool
parse Parser
}
type Parser func(msg *message.Segment) PatternParsed
// SetOptional set previous segment is optional, is v is empty, optional will be true
// if Pattern is empty, panic
func (p *Pattern) SetOptional(v ...bool) *Pattern {
if len(p.segments) == 0 {
panic("pattern is empty")
}
if len(v) == 1 {
p.segments[len(p.segments)-1].optional = v[0]
} else {
p.segments[len(p.segments)-1].optional = true
}
return p
}
// PatternParsed PatternRule parse result
type PatternParsed struct {
value any
msg *message.Segment
}
// Text 获取正则表达式匹配到的文本数组
func (p PatternParsed) Text() []string {
if p.value == nil {
return nil
}
return p.value.([]string)
}
// At 获取被@者ID
func (p PatternParsed) At() string {
if p.value == nil {
return ""
}
return p.value.(string)
}
// Image 获取图片URL
func (p PatternParsed) Image() string {
if p.value == nil {
return ""
}
return p.value.(string)
}
// Reply 获取被回复的消息ID
func (p PatternParsed) Reply() string {
if p.value == nil {
return ""
}
return p.value.(string)
}
// Raw 获取原始消息
func (p PatternParsed) Raw() *message.Segment {
return p.msg
}
func (p *Pattern) Add(typ string, optional bool, parse Parser) *Pattern {
pattern := &PatternSegment{
typ: typ,
optional: optional,
parse: parse,
}
p.segments = append(p.segments, *pattern)
return p
}
// Text use regex to search a 'text' segment
func (p *Pattern) Text(regex string) *Pattern {
p.Add("text", false, NewTextParser(regex))
return p
}
func NewTextParser(regex string) Parser {
re := regexp.MustCompile(regex)
return func(msg *message.Segment) PatternParsed {
s := msg.Data["text"]
s = strings.Trim(s, " \n\r\t")
matchString := re.MatchString(s)
if matchString {
return PatternParsed{
value: re.FindStringSubmatch(s),
msg: msg,
}
}
return PatternParsed{}
}
}
// At use regex to match an 'at' segment, if id is not empty, only match specific target
func (p *Pattern) At(id ...message.ID) *Pattern {
if len(id) > 1 {
panic("at pattern only support one id")
}
p.Add("at", false, NewAtParser(id...))
return p
}
func NewAtParser(id ...message.ID) Parser {
return func(msg *message.Segment) PatternParsed {
if len(id) == 0 || len(id) == 1 && id[0].String() == msg.Data["qq"] {
return PatternParsed{
value: msg.Data["qq"],
msg: msg,
}
}
return PatternParsed{}
}
}
// Image use regex to match an 'at' segment, if id is not empty, only match specific target
func (p *Pattern) Image() *Pattern {
p.Add("image", false, NewImageParser())
return p
}
func NewImageParser() Parser {
return func(msg *message.Segment) PatternParsed {
return PatternParsed{
value: msg.Data["file"],
msg: msg,
}
}
}
// Reply type zero.PatternReplyMatched
func (p *Pattern) Reply() *Pattern {
p.Add("reply", false, NewReplyParser())
return p
}
func NewReplyParser() Parser {
return func(msg *message.Segment) PatternParsed {
return PatternParsed{
value: msg.Data["id"],
msg: msg,
}
}
}
// Any match any segment
func (p *Pattern) Any() *Pattern {
p.Add("any", false, NewAnyParser())
return p
}
func NewAnyParser() Parser {
return func(msg *message.Segment) PatternParsed {
parsed := PatternParsed{
value: nil,
msg: msg,
}
switch {
case msg.Data["text"] != "":
parsed.value = msg.Data["text"]
case msg.Data["qq"] != "":
parsed.value = msg.Data["qq"]
case msg.Data["file"] != "":
parsed.value = msg.Data["file"]
case msg.Data["id"] != "":
parsed.value = msg.Data["id"]
default:
parsed.value = msg.Data
}
return parsed
}
}
func (s *PatternSegment) matchType(msg message.Segment) bool {
return s.typ == msg.Type || s.typ == "any"
}
func mustMatchAllPatterns(pattern Pattern) bool {
for _, p := range pattern.segments {
if p.optional {
return false
}
}
return true
}
func patternMatch(ctx *Ctx, pattern Pattern, msgs []message.Segment) bool {
if mustMatchAllPatterns(pattern) && len(pattern.segments) != len(msgs) {
return false
}
patternState := make([]PatternParsed, len(pattern.segments))
j := 0
for i := range pattern.segments {
if j < len(msgs) && pattern.segments[i].matchType(msgs[j]) {
patternState[i] = pattern.segments[i].parse(&msgs[j])
}
if patternState[i].value == nil {
if pattern.segments[i].optional {
continue
}
return false
}
j++
}
ctx.State[KeyPattern] = patternState
return true
}