Files
Dmitry VerkhoturovandGitHub ba7c3aed94 refactor: modernise Go code with go fix and manual improvements (#2027)
Apply go fix ./... analysers (Go 1.26) across backend and examples:
- interface{} → any (type alias, no behaviour change)
- for i := 0; i < N; i++ → for range N / for i := range N
- slices.Contains / slices.ContainsFunc replacing manual loops
- strings.SplitSeq replacing strings.Split in range (avoids allocation)
- strings.CutPrefix replacing HasPrefix+TrimPrefix
- min() replacing manual if/else
- fmt.Appendf replacing []byte(fmt.Sprintf(...))
- strings.Builder replacing string += concatenation
- wg.Go(func(){}) replacing wg.Add(1)/go/wg.Done() pattern
- removed redundant ii := i loop variable copies (unnecessary since Go 1.22)

omitempty on struct-typed JSON fields: go fix removed omitempty from
struct-typed fields (time.Time, PostInfo, UserDetailEntry) because
encoding/json's omitempty never applied to struct types — it was always
a no-op. Kept as bare tags (no omitzero replacement) to preserve the
existing serialisation behaviour.
2026-03-25 16:42:37 -05:00

173 lines
4.2 KiB
Go

package service
import (
"slices"
"strings"
"unicode"
"unicode/utf8"
log "github.com/go-pkgz/lgr"
)
// RestrictedWordsLister provides restricted words in comments per site
type RestrictedWordsLister interface {
List(siteID string) (restricted []string, err error)
}
// StaticRestrictedWordsLister provides same restricted words in comments for every site
type StaticRestrictedWordsLister struct {
Words []string
}
// List provides restricted words in comments (ignores siteID)
func (l StaticRestrictedWordsLister) List(_ string) (restricted []string, err error) {
return l.Words, nil
}
// RestrictedWordsMatcher matches comment text against restricted words
type RestrictedWordsMatcher struct {
lister RestrictedWordsLister
}
// NewRestrictedWordsMatcher creates new RestrictedWordsMatcher using provided RestrictedWordsLister
func NewRestrictedWordsMatcher(lister RestrictedWordsLister) *RestrictedWordsMatcher {
return &RestrictedWordsMatcher{lister: lister}
}
// Match matches comment text against restricted words for specified site
func (m *RestrictedWordsMatcher) Match(siteID, text string) bool {
restrictedWords, err := m.lister.List(siteID)
if err != nil {
log.Printf("[WARN] failed to get restricted patterns for site %s: %v", siteID, err)
return false
}
if len(restrictedWords) == 0 {
return false
}
tokens := m.tokenize(text)
trie := newWildcardTrie(restrictedWords...)
return slices.ContainsFunc(tokens, trie.check)
}
func (m *RestrictedWordsMatcher) tokenize(text string) []string {
tokens := make([]string, 0, 10) // accumulator for tokens
word := false // flag shows if current range is word
start := 0 // beginning of the current range
for pos, r := range text {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
if !word {
// everything from start to pos - 1 is not a word, so reset start and start word tracking
start = pos
word = true
}
continue
}
if word && start < pos {
// everything from start to pos - 1 is a word, so add it as a token and reset start
tokens = append(tokens, strings.ToLower(text[start:pos]))
start = pos
}
// exited the word
word = false
}
// since we append tokens when we already left the word (on next iteration),
// we need to do it manually for the last iteration
if word {
tokens = append(tokens, strings.ToLower(text[start:]))
}
return tokens
}
type wildcardTrie struct {
terminal bool
children map[rune]*wildcardTrie
}
func newWildcardTrie(patterns ...string) *wildcardTrie {
trie := &wildcardTrie{terminal: false, children: make(map[rune]*wildcardTrie)}
for _, p := range patterns {
trie.addPattern(p)
}
return trie
}
func (trie *wildcardTrie) addPattern(pattern string) {
// since pattern matching algorithm is recursive we do not allow long patterns
if utf8.RuneCountInString(pattern) < 1 || utf8.RuneCountInString(pattern) > 64 {
log.Printf("[WARN] invalid pattern length '%s': actual - %d, min allowed - 1, max allowed - 64", pattern, utf8.RuneCountInString(pattern))
return
}
node := trie
for _, r := range strings.ToLower(strings.TrimSpace(pattern)) {
if childNode, exists := node.children[r]; exists {
node = childNode
continue
}
childNode := newWildcardTrie()
node.children[r] = childNode
node = childNode
}
node.terminal = true
}
// check tests if any pattern stored in trie matches the token. Recursive. Max depth is longest pattern in trie.
func (trie *wildcardTrie) check(token string) bool {
if token == "" {
if trie.terminal {
return true
}
if childNode, exists := trie.children['*']; exists && childNode.terminal {
return true
}
return false
}
r, width := utf8.DecodeRuneInString(token)
if childNode, exists := trie.children[r]; exists {
if childNode.check(token[width:]) {
return true
}
}
if childNode, exists := trie.children['*']; exists {
if childNode.terminal {
return true
}
if childNode.checkAllSuffixes(token) {
return true
}
}
return false
}
func (trie *wildcardTrie) checkAllSuffixes(token string) bool {
suffix := token
for {
if suffix == "" {
return false
}
if trie.check(suffix) {
return true
}
_, width := utf8.DecodeRuneInString(suffix)
suffix = suffix[width:]
}
}