Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

消息模板匹配器 #91

Merged
merged 54 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
27353f5
add new Matcher `PatternMatcher`
RikaCelery Oct 7, 2024
e175810
replace with const value
RikaCelery Oct 9, 2024
816234b
remove unused function
RikaCelery Oct 9, 2024
392941b
remove `OnPattern`
RikaCelery Oct 10, 2024
22c4ec8
remove unused const
RikaCelery Oct 10, 2024
ba285e3
add `PatternModel`
RikaCelery Oct 10, 2024
0635fc1
type safe PatternRule state
RikaCelery Oct 10, 2024
ece50eb
add `PatternReply`
RikaCelery Oct 10, 2024
66f9382
remove redundant `at` element after `reply` element
RikaCelery Oct 10, 2024
32b5226
fix wrong key && catch error
RikaCelery Oct 10, 2024
24877ed
Optional PatternSegment
RikaCelery Oct 10, 2024
e810fbf
use pointer
RikaCelery Oct 10, 2024
e4950df
fix nil
RikaCelery Oct 10, 2024
7101db5
move patterns to pattern.go
RikaCelery Oct 11, 2024
88963dc
set State in `patternMatch`
RikaCelery Oct 12, 2024
ab9dbeb
match tests
RikaCelery Oct 12, 2024
3752551
fix optional not work if len(msg) < len(pattern)
RikaCelery Oct 12, 2024
63b4e1f
match tests
RikaCelery Oct 12, 2024
cc54177
Merge remote-tracking branch 'origin/feat/pattern-matcher' into feat/…
RikaCelery Oct 12, 2024
9ecfc51
return empty value if not Valid
RikaCelery Oct 12, 2024
d501a63
fix test failed
RikaCelery Oct 12, 2024
07a5fcb
ignore empty message
RikaCelery Oct 12, 2024
039a7ed
change `At` parsed value to string
RikaCelery Oct 12, 2024
2e7bdfb
Merge remote-tracking branch 'upstream/main' into feat/pattern-matcher
RikaCelery Oct 12, 2024
9403051
Merge remote-tracking branch 'upstream/main' into feat/pattern-matcher
RikaCelery Oct 12, 2024
7526ee8
chore: make lint happy
RikaCelery Oct 12, 2024
6050559
optimize
RikaCelery Oct 12, 2024
0f30441
chore: make lint happy
RikaCelery Oct 12, 2024
b3366ef
move PatternRule to pattern.go
RikaCelery Oct 13, 2024
8dadba4
move KeyPattern to pattern.go
RikaCelery Oct 13, 2024
4524132
chained PatternRule builder
RikaCelery Oct 13, 2024
09e5c79
rename value getter
RikaCelery Oct 13, 2024
2c5cfea
optimize
RikaCelery Oct 13, 2024
2c51768
rename `containsOptional` to `mustMatchAllPatterns`
RikaCelery Oct 13, 2024
c1bd251
make `PatternParse` fields private
RikaCelery Oct 13, 2024
5fd8dd4
Merge remote-tracking branch 'origin/feat/pattern-matcher' into feat/…
RikaCelery Oct 13, 2024
8b77550
optimize
RikaCelery Oct 13, 2024
0f48fe7
optimize
RikaCelery Oct 13, 2024
4b95413
PatternSegment: make `Type` and `Parse` private
RikaCelery Oct 13, 2024
4e89e07
Merge remote-tracking branch 'origin/feat/pattern-matcher' into feat/…
RikaCelery Oct 13, 2024
78ce239
fixup! rename `containsOptional` to `mustMatchAllPatterns`
RikaCelery Oct 13, 2024
455afde
make lint happy
RikaCelery Oct 13, 2024
f606a75
At Pattern use `message.ID` parameters
RikaCelery Oct 13, 2024
6dfbd27
PatternSegment: make `optional` private
RikaCelery Oct 13, 2024
370fda4
`Raw` gatter
RikaCelery Oct 13, 2024
18dbda7
`PatternSegment` builder
RikaCelery Oct 13, 2024
2a96084
`Any` PatternSegment
RikaCelery Oct 13, 2024
6ac4209
tests for `Any`
RikaCelery Oct 13, 2024
7bbb37b
option for cleaning useless sibling `at` after `reply`
RikaCelery Oct 13, 2024
6b486c1
fix wrong clean logic
RikaCelery Oct 13, 2024
2077580
refactor: extract parser, custom parser support
RikaCelery Oct 14, 2024
dbe0b4d
refactor: make `cleanRedundantAt` an option of `Pattern`
RikaCelery Oct 14, 2024
df7234b
fix: fix stupid bugs
RikaCelery Oct 14, 2024
43ae5cd
use for range loop
RikaCelery Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extension/model.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package extension

import zero "github.com/wdvxdr1123/ZeroBot"

// PrefixModel is model of zero.PrefixRule
type PrefixModel struct {
Prefix string `zero:"prefix"`
Expand Down Expand Up @@ -32,3 +34,8 @@ type FullMatchModel struct {
type RegexModel struct {
Matched []string `zero:"regex_matched"`
}

// PatternModel is model of zero.PatternRule
type PatternModel struct {
Matched []*zero.PatternParsed `zero:"pattern_matched"`
}
2 changes: 1 addition & 1 deletion extension/rate/rate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func NewManager[K comparable](interval time.Duration, burst int) *LimiterManager
}

// Delete 删除对应限速器
func (l *LimiterManager[K]) Delete(key K) {
func (l *LimiterManager[K]) Delete(key K) {
l.limiters.Delete(key)
}

Expand Down
1 change: 1 addition & 0 deletions extension/single/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"runtime"

"github.com/RomiChan/syncx"

zero "github.com/wdvxdr1123/ZeroBot"
)

Expand Down
268 changes: 268 additions & 0 deletions pattern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package zero

import (
"regexp"
"strconv"
"strings"

"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 {
return patternMatch(ctx, *p, ctx.Event.Message)
}

// copy messages
msgs := make([]message.Segment, 0, len(ctx.Event.Message))
msgs = append(msgs, ctx.Event.Message[0])
for i := 1; i < len(ctx.Event.Message); i++ {
fumiama marked this conversation as resolved.
Show resolved Hide resolved
if 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
}
}
msgs = append(msgs, ctx.Event.Message[i])
}
return patternMatch(ctx, *p, msgs)
}
}

type Pattern struct {
cleanRedundantAt bool
segments []PatternSegment
}

func NewPattern(cleanRedundantAt ...bool) *Pattern {
clean := true
if len(cleanRedundantAt) > 0 {
clean = cleanRedundantAt[0]
}
pattern := Pattern{
cleanRedundantAt: clean,
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
}
Loading
Loading