diff --git a/README.md b/README.md index 6073502..c96181f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ### Features +- :heavy_check_mark: Wildmat support - :heavy_check_mark: Database (SQLite) - :construction: Articles posting - :x: Transit mode diff --git a/go.mod b/go.mod index 2db322b..9b604df 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,7 @@ require ( github.com/pressly/goose/v3 v3.5.0 ) -require github.com/pkg/errors v0.9.1 // indirect +require ( + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/pkg/errors v0.9.1 // indirect +) diff --git a/go.sum b/go.sum index a3cd37d..cf565ba 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= diff --git a/internal/backend/sqlite/sqlite.go b/internal/backend/sqlite/sqlite.go index a77b280..e4eb9c0 100644 --- a/internal/backend/sqlite/sqlite.go +++ b/internal/backend/sqlite/sqlite.go @@ -1,10 +1,14 @@ package sqlite import ( + "database/sql" "embed" "github.com/ChronosX88/yans/internal/config" "github.com/ChronosX88/yans/internal/models" + "github.com/ChronosX88/yans/internal/utils" + "github.com/dlclark/regexp2" "github.com/jmoiron/sqlx" + "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3" "github.com/pressly/goose/v3" ) @@ -16,8 +20,19 @@ type SQLiteBackend struct { db *sqlx.DB } +func regexHelper(re, s string) (bool, error) { + return regexp2.MustCompile(re, regexp2.None).MatchString(s) +} + func NewSQLiteBackend(cfg config.SQLiteBackendConfig) (*SQLiteBackend, error) { - db, err := sqlx.Open("sqlite3", cfg.Path) + sql.Register("sqlite3_with_regexp", + &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.RegisterFunc("regexp", regexHelper, true) + }, + }) + + db, err := sqlx.Open("sqlite3_with_regexp", cfg.Path) if err != nil { return nil, err } @@ -41,6 +56,19 @@ func (sb *SQLiteBackend) ListGroups() ([]models.Group, error) { return groups, sb.db.Select(&groups, "SELECT * FROM groups") } +func (sb *SQLiteBackend) ListGroupsByPattern(pattern string) ([]models.Group, error) { + var groups []models.Group + w, err := utils.ParseWildmat(pattern) + if err != nil { + return nil, err + } + r, err := w.ToRegex() + if err != nil { + return nil, err + } + return groups, sb.db.Select(&groups, "SELECT * FROM groups WHERE group_name REGEXP ?", r.String()) +} + func (sb *SQLiteBackend) GetArticlesCount(g models.Group) (int, error) { var count int return count, sb.db.Get(&count, "SELECT COUNT(*) FROM articles_to_groups WHERE group_id = ?", g.ID) diff --git a/internal/backend/storage_backend.go b/internal/backend/storage_backend.go index d1cda95..6fe0a4a 100644 --- a/internal/backend/storage_backend.go +++ b/internal/backend/storage_backend.go @@ -8,6 +8,7 @@ const ( type StorageBackend interface { ListGroups() ([]models.Group, error) + ListGroupsByPattern(pattern string) ([]models.Group, error) GetArticlesCount(g models.Group) (int, error) GetGroupLowWaterMark(g models.Group) (int, error) GetGroupHighWaterMark(g models.Group) (int, error) diff --git a/internal/server/handler.go b/internal/server/handler.go index a8435f1..30c340f 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -3,6 +3,7 @@ package server import ( "fmt" "github.com/ChronosX88/yans/internal/backend" + "github.com/ChronosX88/yans/internal/models" "github.com/ChronosX88/yans/internal/protocol" "strings" "time" @@ -60,13 +61,20 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error { fallthrough case "ACTIVE": { - groups, err := h.backend.ListGroups() + var groups []models.Group + var err error + if len(arguments) == 2 { + groups, err = h.backend.ListGroupsByPattern(arguments[1]) + } else { + groups, err = h.backend.ListGroups() + } + if err != nil { return err } sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF)) for _, v := range groups { - // TODO set high/low mark and posting status to actual values + // TODO set actual post permission status c, err := h.backend.GetArticlesCount(v) if err != nil { return err @@ -82,16 +90,24 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error { } sb.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark))) } else { - sb.Write([]byte(fmt.Sprintf("%s 0 0 n"+protocol.CRLF, v.GroupName))) + sb.Write([]byte(fmt.Sprintf("%s 0 1 n"+protocol.CRLF, v.GroupName))) } } } case "NEWSGROUPS": { - groups, err := h.backend.ListGroups() + var groups []models.Group + var err error + if len(arguments) == 2 { + groups, err = h.backend.ListGroupsByPattern(arguments[1]) + } else { + groups, err = h.backend.ListGroups() + } if err != nil { return err } + + sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF)) for _, v := range groups { desc := "" if v.Description == nil { diff --git a/internal/utils/wildmat.go b/internal/utils/wildmat.go new file mode 100644 index 0000000..11d755d --- /dev/null +++ b/internal/utils/wildmat.go @@ -0,0 +1,83 @@ +package utils + +import ( + "fmt" + "github.com/dlclark/regexp2" + "strings" +) + +type Wildmat struct { + patterns []*WildmatPattern +} + +type WildmatPattern struct { + negated bool + pattern string + regex *regexp2.Regexp +} + +func regexpEscape(str string) string { + restrictedChars := []string{"(\\", "+", "|", "{", "[", "(", ")", "^", "$", ".", "#"} + for _, v := range restrictedChars { + str = strings.ReplaceAll(str, v, "\\"+v) + } + return str +} + +func convertWildmatToRegex(pat string) (*regexp2.Regexp, error) { + regex := "" + pat = regexpEscape(pat) + patRunes := []rune(pat) + for _, v := range patRunes { + switch v { + case '?': + regex += "." + case '*': + regex += ".?" + default: + { + regex += string(v) + } + } + } + return regexp2.Compile(regex, regexp2.None) +} + +func ParseWildmat(wildmat string) (*Wildmat, error) { + res := &Wildmat{} + for _, v := range strings.Split(wildmat, ",") { + if len(v) > 0 && v[0] == '!' { + r, err := convertWildmatToRegex(v[1:]) + if err != nil { + return nil, err + } + res.patterns = append(res.patterns, &WildmatPattern{pattern: v[1:], negated: true, regex: r}) + } else { + r, err := convertWildmatToRegex(v) + if err != nil { + return nil, err + } + res.patterns = append(res.patterns, &WildmatPattern{pattern: v, negated: false, regex: r}) + } + } + return res, nil +} + +func (w *Wildmat) ToRegex() (*regexp2.Regexp, error) { + res := "(%s)%s" + include := "" + exclude := "" + for _, v := range w.patterns { + if v.negated { + exclude += fmt.Sprintf("(?!%s)", v.regex.String()) + } else { + if len(include) != 0 { + include += fmt.Sprintf("|(%s)", v.regex.String()) + } else { + include += fmt.Sprintf("(%s)", v.regex.String()) + } + } + } + res = fmt.Sprintf(res, include, exclude) + return regexp2.Compile(res, regexp2.None) +}