Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 12 additions & 4 deletions command_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ import (
"github.com/SevereCloud/vksdk/v3/api/params"
"github.com/SevereCloud/vksdk/v3/events"
"github.com/SevereCloud/vksdk/v3/object"
"txts.su/vkc/filters"
)

// Контекст команды. Передается в каждый обработчик.
type CommandContext[DEPS any] struct {
VK *api.VK
Message object.MessagesMessage
Arguments []string
RawEvent events.MessageNewObject
// Объект API VK.
VK *api.VK
// Сообщение, вызвавшее команду.
Message object.MessagesMessage
// Список аргументов команды. Если аргументы не удалось получить, срез будет пуст.
Arguments []string
// Результаты извлечений из фильтра команды (остаток строки, группы regex и т.д.).
FilterMatch filters.MatchResult
// Исходное событие message_new.
RawEvent events.MessageNewObject
// Объект с зависимостями всей системы команд.
Dependency DEPS
}

Expand Down
38 changes: 34 additions & 4 deletions command_handler.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package vkc

import (
"txts.su/vkc/filters"
vkfilters "txts.su/vkc/filters/vk_filters"
)

// Функция обработчика команды. Получает контекст и возвращает ошибку или nil.
type HandlerFunc[DEPS any] func(ctx CommandContext[DEPS]) error

Expand Down Expand Up @@ -33,16 +38,41 @@ type HandlerAccessCheck[DEPS any] struct {
// из дженерика в объекте команд;
// впоследствии в контекст обработчика будет передано значение этого типа в поле Dependency */
// var HandleSomeCommand = CommandHandler[DepsType]{
// Pattern: Text("some"), // шаблон "только строка `some`"
// Filter: filter.PrefixF("some") // шаблон "только строка `some`"
// Help: CommandHelp{ /* помощь по команде */ },
// AccessCheck: &HandlerAccessCheck[DepsType]{ /* проверка доступа */ },
// Executor: func(ctx CommandContext[DepsType]) error { /* логика команды */ },
// }
type CommandHandler[DEPS any] struct {
Pattern CommandPattern
Help CommandHelp
// Фильтр, совпадение с которым запустит команду
Filter filters.Filter
// Deprecated: будет удалено в v2. Используйте [CommandHandler.Filter].
Pattern CommandPattern
// Помощь по команде
Help CommandHelp
// Проверка на доступ к команде
AccessCheck *HandlerAccessCheck[DEPS]
Executor HandlerFunc[DEPS]
// Исполнитель команды
Executor HandlerFunc[DEPS]
}

// Метод для проверки совпадения контекста с фильтром команды.
func (handler *CommandHandler[T]) IsNotFiltered(ctx CommandContext[T], prefixRemainder string) (result bool, sideValues filters.MatchResult) {
input := map[filters.FieldDescriptor]any{
vkfilters.MessageField: ctx.Message,
vkfilters.VkApiField: ctx.VK,
filters.TextField: prefixRemainder,
}
if !handler.Filter.Eval(input) {
return false, nil
}
// по умолчанию grfilt работает с "чистыми" фильтрами.
// а для работы команд необходимо еще и знать контекст срабатывания фильтра,
// поэтому в vkc используются фильтры с извлечениями.
if ex, ok := handler.Filter.(filters.Extractor); ok {
return true, ex.Extract(input)
}
return true, nil
}

// Метод для проверки доступности команды для пользователя.
Expand Down
44 changes: 39 additions & 5 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ import (
"github.com/SevereCloud/vksdk/v3/api"
"github.com/SevereCloud/vksdk/v3/events"
"github.com/SevereCloud/vksdk/v3/longpoll-bot"
"txts.su/vkc/filters"
)

func logDeprecationWarning(feature string) {
log.Printf("обработчик %s скоро перестанет вызываться. См.: https://github.com/EgorBron/vkc/issues/1", feature)
}

type CommandsOptions struct { // TODO: задокументировать в v1.3.0
AllowPrefixless bool
stripSpaces bool
stripPunctuation bool
}

// Структура для управления командами. Содержит обработчики, префикс и зависимости.
// Структуру можно типизировать дженериком для передачи зависимостей в обработчики команд, или передать any в дженерик, если это не требуется.
// Для обработчиков команд опеределено поле Handlers, принимающее массив указателей на обработчики. Рекомендуется получать адрес обработчика прямо при передаче в массив, как здесь:
Expand All @@ -42,7 +49,11 @@ func logDeprecationWarning(feature string) {
type Commands[DEPS any] struct {
cmdMutex sync.RWMutex

// Префикс для команд.
Prefix PrefixMatcher

Options CommandsOptions

// Структура для передачи зависимостей в обработчики команд. Если зависимости не требуются, можно указать any в дженерике.
//
// Deprecated: Начиная с v2 будет удалено. Рекомендуется перейти на [context.Context] (см. https://github.com/EgorBron/vkc/issues/2 для просмотра обсуждения).
Expand All @@ -62,7 +73,7 @@ type Commands[DEPS any] struct {
OnCommandError *func(ctx CommandContext[DEPS], err error)
}

// Поиск команды по строке. Возвращает найденный обработчик и остаток строки (т.е. без названия команды).
// ППоиск команды по строке. Возвращает найденный обработчик и остаток строки (т.е. без названия команды).
//
// Поскольку срезы в Go являются упорядоченными, то при поиске приоритет будут иметь обработчики, расположенные ближе к началу среза.
//
Expand All @@ -75,6 +86,8 @@ type Commands[DEPS any] struct {
// FindCommand("tag me", handlers), есть обработчики Text("tag") и Text("tag me") -> (обработчик для Text("tag"), остаток - "me")
//
// и так далее.
//
// Deprecated: будет удалено в v2. Функция больше не используется.
func FindCommand[DEPS any](rawCmd string, commands []*CommandHandler[DEPS]) (*CommandHandler[DEPS], string) {
for _, handler := range commands {
if handler == nil {
Expand All @@ -86,7 +99,7 @@ func FindCommand[DEPS any](rawCmd string, commands []*CommandHandler[DEPS]) (*Co
for i := len(words); i > 0; i-- {
candidate := strings.Join(words[:i], " ")

if handler.Pattern(candidate) {
if handler.Pattern != nil && handler.Pattern(candidate) {
return handler, strings.Join(words[i:], " ")
}
}
Expand Down Expand Up @@ -130,7 +143,10 @@ func (commands *Commands[any]) ProcessCommands(ctx context.Context, vk *api.VK,

matched, rawCmd := commands.Prefix(text)
if !matched {
return ErrNoPrefix
if !commands.Options.AllowPrefixless {
return ErrNoPrefix
}
rawCmd = text
}

cmdCtx := CommandContext[any]{
Expand All @@ -154,7 +170,20 @@ func (commands *Commands[any]) ProcessCommands(ctx context.Context, vk *api.VK,
copy(handlers, commands.Handlers)
commands.cmdMutex.RUnlock()

handler, remaining := FindCommand(rawCmd, handlers)
var handler *CommandHandler[any]
var matchResult filters.MatchResult
var legacyRemainder string
for _, h := range handlers {
if matched, match := h.IsNotFiltered(cmdCtx, rawCmd); matched {
handler = h
matchResult = match
break
}
}
if handler == nil {
handler, legacyRemainder = FindCommand(rawCmd, handlers)
}

if handler == nil {
if commands.OnUnknownCommand != nil {
logDeprecationWarning("OnUnknownCommand")
Expand All @@ -163,7 +192,12 @@ func (commands *Commands[any]) ProcessCommands(ctx context.Context, vk *api.VK,
return ErrCommandNotFound
}

cmdCtx.Arguments = SplitArgs(remaining)
cmdCtx.FilterMatch = matchResult
if matchResult != nil {
cmdCtx.Arguments = ArgumentsFromMatch(matchResult)
} else {
cmdCtx.Arguments = SplitArgs(legacyRemainder)
}

if !handler.IsAccessAvailable(cmdCtx) {
if commands.OnNoPermissions != nil {
Expand Down
53 changes: 53 additions & 0 deletions filters/composite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package filters

import "txts.su/grfilt"

type CompositeFilter struct {
filters []Filter
negated bool
}

func NewComposite(items ...Filter) *CompositeFilter {
return &CompositeFilter{filters: items}
}

func (f *CompositeFilter) Not() Filter {
f.negated = !f.negated
return f
}

func And(filters ...Filter) *CompositeFilter {
return NewComposite(filters...)
}

func (f *CompositeFilter) And(otherFilters ...Filter) Filter {
return grfilt.CombineFilters(NewComposite, f, otherFilters)
}

func (f *CompositeFilter) Or(otherFilters ...Filter) Filter {
return grfilt.CombineFilters(NewEither, f, otherFilters)
}

func (cf *CompositeFilter) Eval(input map[FieldDescriptor]any) (result bool) {
result = true
for _, f := range cf.filters {
if !f.Eval(input) {
result = false
break
}
}
if cf.negated {
result = !result
}
return
}

func (cf *CompositeFilter) Extract(input map[FieldDescriptor]any) MatchResult {
result := make(MatchResult)
for _, f := range cf.filters {
if ex, ok := f.(Extractor); ok {
mergeResults(result, ex.Extract(input))
}
}
return result
}
56 changes: 56 additions & 0 deletions filters/either.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package filters

import "txts.su/grfilt"

// EitherFilter — композиция фильтров по логическому ИЛИ.
type EitherFilter struct {
filters []Filter
negated bool
}

func NewEither(filters ...Filter) *EitherFilter {
return &EitherFilter{filters: filters}
}

// Or создаёт фильтр с логическим ИЛИ.
func Or(filters ...Filter) *EitherFilter {
return NewEither(filters...)
}

func (f *EitherFilter) And(otherFilters ...Filter) Filter {
return grfilt.CombineFilters(NewComposite, f, otherFilters)
}

func (f *EitherFilter) Or(otherFilters ...Filter) Filter {
return grfilt.CombineFilters(NewEither, f, otherFilters)
}

func (f *EitherFilter) Not() Filter {
return leafFilter{inner: grfilt.Not(f)}
}

func (cf *EitherFilter) Eval(input map[FieldDescriptor]any) (result bool) {
for _, f := range cf.filters {
if !f.Eval(input) {
result = true
break
}
}
if cf.negated {
result = !result
}
return
}

func (f *EitherFilter) Extract(input map[FieldDescriptor]any) MatchResult {
result := make(MatchResult)
for _, f := range f.filters {
if !f.Eval(input) {
continue
}
if ex, ok := f.(Extractor); ok {
mergeResults(result, ex.Extract(input))
}
}
return result
}
45 changes: 45 additions & 0 deletions filters/extractable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package filters

import "txts.su/grfilt"

type extractableFilter[T any] struct {
field FieldDescriptor
eval func(T) bool
extract func(T) MatchResult
}

func (f *extractableFilter[T]) Eval(input map[FieldDescriptor]any) bool {
value, ok := input[f.field].(T)
if !ok {
return false
}
return f.eval(value)
}

func (f *extractableFilter[T]) Extract(input map[FieldDescriptor]any) MatchResult {
value, ok := input[f.field].(T)
if !ok {
return nil
}
return f.extract(value)
}

func (f *extractableFilter[T]) Not() Filter {
return leafFilter{inner: grfilt.Not(f)}
}

func (f *extractableFilter[T]) And(otherFilters ...Filter) Filter {
return And(append([]Filter{f}, otherFilters...)...)
}

func (f *extractableFilter[T]) Or(otherFilters ...Filter) Filter {
return Or(append([]Filter{f}, otherFilters...)...)
}

func NewExtractableFilter[T any](field FieldDescriptor, eval func(T) bool, extract func(T) MatchResult) Filter {
return &extractableFilter[T]{
field: field,
eval: eval,
extract: extract,
}
}
12 changes: 12 additions & 0 deletions filters/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package filters

import "txts.su/grfilt"

type Filter = grfilt.Filter
type FieldDescriptor = grfilt.FieldDescriptor

type MatchResult map[string]any

type Extractor interface {
Extract(input map[FieldDescriptor]any) MatchResult
}
28 changes: 28 additions & 0 deletions filters/leaf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package filters

import "txts.su/grfilt"

type leafFilter struct {
inner Filter
}

func (f leafFilter) Eval(input map[FieldDescriptor]any) bool {
return f.inner.Eval(input)
}

func (f leafFilter) Not() Filter {
return leafFilter{inner: grfilt.Not(f.inner)}
}

func (f leafFilter) And(otherFilters ...Filter) Filter {
return And(append([]Filter{f}, otherFilters...)...)
}

func (f leafFilter) Or(otherFilters ...Filter) Filter {
return Or(append([]Filter{f}, otherFilters...)...)
}

// NewFilter создаёт листовой фильтр по полю и предикату (только bool, без Extractor).
func NewFilter[T any](fieldName FieldDescriptor, fn func(value T) bool) Filter {
return leafFilter{inner: grfilt.NewFilter(fieldName, fn)}
}
Loading