Implement wildmat support

This commit is contained in:
ChronosX88 2022-01-21 00:29:58 +03:00
parent dda82b7916
commit 7ae9a7af35
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
7 changed files with 140 additions and 6 deletions

View File

@ -6,6 +6,7 @@
### Features ### Features
- :heavy_check_mark: Wildmat support
- :heavy_check_mark: Database (SQLite) - :heavy_check_mark: Database (SQLite)
- :construction: Articles posting - :construction: Articles posting
- :x: Transit mode - :x: Transit mode

5
go.mod
View File

@ -10,4 +10,7 @@ require (
github.com/pressly/goose/v3 v3.5.0 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
)

2
go.sum
View File

@ -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/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/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/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/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/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=

View File

@ -1,10 +1,14 @@
package sqlite package sqlite
import ( import (
"database/sql"
"embed" "embed"
"github.com/ChronosX88/yans/internal/config" "github.com/ChronosX88/yans/internal/config"
"github.com/ChronosX88/yans/internal/models" "github.com/ChronosX88/yans/internal/models"
"github.com/ChronosX88/yans/internal/utils"
"github.com/dlclark/regexp2"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
) )
@ -16,8 +20,19 @@ type SQLiteBackend struct {
db *sqlx.DB 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -41,6 +56,19 @@ func (sb *SQLiteBackend) ListGroups() ([]models.Group, error) {
return groups, sb.db.Select(&groups, "SELECT * FROM groups") 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) { func (sb *SQLiteBackend) GetArticlesCount(g models.Group) (int, error) {
var count int var count int
return count, sb.db.Get(&count, "SELECT COUNT(*) FROM articles_to_groups WHERE group_id = ?", g.ID) return count, sb.db.Get(&count, "SELECT COUNT(*) FROM articles_to_groups WHERE group_id = ?", g.ID)

View File

@ -8,6 +8,7 @@ const (
type StorageBackend interface { type StorageBackend interface {
ListGroups() ([]models.Group, error) ListGroups() ([]models.Group, error)
ListGroupsByPattern(pattern string) ([]models.Group, error)
GetArticlesCount(g models.Group) (int, error) GetArticlesCount(g models.Group) (int, error)
GetGroupLowWaterMark(g models.Group) (int, error) GetGroupLowWaterMark(g models.Group) (int, error)
GetGroupHighWaterMark(g models.Group) (int, error) GetGroupHighWaterMark(g models.Group) (int, error)

View File

@ -3,6 +3,7 @@ package server
import ( import (
"fmt" "fmt"
"github.com/ChronosX88/yans/internal/backend" "github.com/ChronosX88/yans/internal/backend"
"github.com/ChronosX88/yans/internal/models"
"github.com/ChronosX88/yans/internal/protocol" "github.com/ChronosX88/yans/internal/protocol"
"strings" "strings"
"time" "time"
@ -60,13 +61,20 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error {
fallthrough fallthrough
case "ACTIVE": 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 { if err != nil {
return err return err
} }
sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF)) sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF))
for _, v := range groups { 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) c, err := h.backend.GetArticlesCount(v)
if err != nil { if err != nil {
return err 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))) sb.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark)))
} else { } 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": 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 { if err != nil {
return err return err
} }
sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF))
for _, v := range groups { for _, v := range groups {
desc := "" desc := ""
if v.Description == nil { if v.Description == nil {

83
internal/utils/wildmat.go Normal file
View File

@ -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)
}