Implement LISTGROUP command

This commit is contained in:
ChronosX88 2022-02-03 20:39:08 +03:00
parent 163f2bcba6
commit 10320d169a
Signed by: ChronosXYZ
GPG Key ID: 085A69A82C8C511A
6 changed files with 135 additions and 18 deletions

View File

@ -27,7 +27,7 @@
- :x: `HELP` - :x: `HELP`
- :x: `IHAVE` - :x: `IHAVE`
- :x: `LAST` - :x: `LAST`
- :x: `LISTGROUP` - :heavy_check_mark: `LISTGROUP`
- :heavy_check_mark: `NEWGROUPS` - :heavy_check_mark: `NEWGROUPS`
- :x: `NEWNEWS` - :x: `NEWNEWS`
- :x: `NEXT` - :x: `NEXT`

View File

@ -72,17 +72,17 @@ func (sb *SQLiteBackend) ListGroupsByPattern(pattern string) ([]models.Group, er
return groups, sb.db.Select(&groups, "SELECT * FROM groups WHERE group_name REGEXP ?", r.String()) 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)
} }
func (sb *SQLiteBackend) GetGroupHighWaterMark(g models.Group) (int, error) { func (sb *SQLiteBackend) GetGroupHighWaterMark(g *models.Group) (int, error) {
var waterMark int var waterMark int
return waterMark, sb.db.Get(&waterMark, "SELECT article_id FROM articles_to_groups WHERE group_id = ? ORDER BY article_id DESC LIMIT 1", g.ID) return waterMark, sb.db.Get(&waterMark, "SELECT article_id FROM articles_to_groups WHERE group_id = ? ORDER BY article_id DESC LIMIT 1", g.ID)
} }
func (sb *SQLiteBackend) GetGroupLowWaterMark(g models.Group) (int, error) { func (sb *SQLiteBackend) GetGroupLowWaterMark(g *models.Group) (int, error) {
var waterMark int var waterMark int
return waterMark, sb.db.Get(&waterMark, "SELECT article_id FROM articles_to_groups WHERE group_id = ? ORDER BY article_id LIMIT 1", g.ID) return waterMark, sb.db.Get(&waterMark, "SELECT article_id FROM articles_to_groups WHERE group_id = ? ORDER BY article_id LIMIT 1", g.ID)
} }
@ -134,3 +134,29 @@ func (sb *SQLiteBackend) GetArticle(messageID string) (models.Article, error) {
} }
return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header) return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header)
} }
func (sb *SQLiteBackend) GetArticleNumbers(g *models.Group, low, high int64) ([]int64, error) {
var numbers []int64
if high == 0 && low == 0 {
if err := sb.db.Select(&numbers, "SELECT article_id FROM articles_to_groups WHERE group_id = ?", g.ID); err != nil {
return nil, err
}
} else if low == -1 && high != 0 {
if err := sb.db.Select(&numbers, "SELECT article_id FROM articles_to_groups WHERE group_id = ? AND article_id = ?", g.ID, high); err != nil {
return nil, err
}
} else if low != 0 && high == -1 {
if err := sb.db.Select(&numbers, "SELECT article_id FROM articles_to_groups WHERE group_id = ? AND article_id > ?", g.ID, low); err != nil {
return nil, err
}
} else if low == -1 && high == -1 {
return nil, nil
} else {
if err := sb.db.Select(&numbers, "SELECT article_id FROM articles_to_groups WHERE group_id = ? AND article_id > ? AND article_id < ?", g.ID, low, high); err != nil {
return nil, err
}
}
return numbers, nil
}

View File

@ -11,9 +11,10 @@ type StorageBackend interface {
ListGroupsByPattern(pattern string) ([]models.Group, error) ListGroupsByPattern(pattern string) ([]models.Group, error)
GetGroup(groupName string) (models.Group, error) GetGroup(groupName string) (models.Group, error)
GetNewGroupsSince(timestamp int64) ([]models.Group, error) GetNewGroupsSince(timestamp int64) ([]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)
SaveArticle(article models.Article, groups []string) error SaveArticle(article models.Article, groups []string) error
GetArticle(messageID string) (models.Article, error) GetArticle(messageID string) (models.Article, error)
GetArticleNumbers(g *models.Group, low, high int64) ([]int64, error)
} }

View File

@ -55,6 +55,7 @@ const (
CommandGroup = "GROUP" CommandGroup = "GROUP"
CommandNewGroups = "NEWGROUPS" CommandNewGroups = "NEWGROUPS"
CommandPost = "POST" CommandPost = "POST"
CommandListGroup = "LISTGROUP"
) )
const ( const (
@ -74,7 +75,6 @@ const (
MessageNNTPServiceReadyPostingProhibited = "201 YANS NNTP Service Ready, posting prohibited" MessageNNTPServiceReadyPostingProhibited = "201 YANS NNTP Service Ready, posting prohibited"
MessageReaderModePostingProhibited = "201 Reader mode, posting prohibited" MessageReaderModePostingProhibited = "201 Reader mode, posting prohibited"
MessageNNTPServiceExitsNormally = "205 NNTP Service exits normally, bye!" MessageNNTPServiceExitsNormally = "205 NNTP Service exits normally, bye!"
MessageUnknownCommand = "500 Unknown command"
MessageErrorHappened = "403 Failed to process command:" MessageErrorHappened = "403 Failed to process command:"
MessageListOfNewsgroupsFollows = "215 list of newsgroups follows" MessageListOfNewsgroupsFollows = "215 list of newsgroups follows"
MessageNoSuchGroup = "411 No such newsgroup" MessageNoSuchGroup = "411 No such newsgroup"

View File

@ -7,9 +7,11 @@ import (
"github.com/ChronosX88/yans/internal/backend" "github.com/ChronosX88/yans/internal/backend"
"github.com/ChronosX88/yans/internal/models" "github.com/ChronosX88/yans/internal/models"
"github.com/ChronosX88/yans/internal/protocol" "github.com/ChronosX88/yans/internal/protocol"
"github.com/ChronosX88/yans/internal/utils"
"github.com/google/uuid" "github.com/google/uuid"
"io" "io"
"net/mail" "net/mail"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -32,6 +34,7 @@ func NewHandler(b backend.StorageBackend, serverDomain string) *Handler {
protocol.CommandGroup: h.handleGroup, protocol.CommandGroup: h.handleGroup,
protocol.CommandNewGroups: h.handleNewGroups, protocol.CommandNewGroups: h.handleNewGroups,
protocol.CommandPost: h.handlePost, protocol.CommandPost: h.handlePost,
protocol.CommandListGroup: h.handleListgroup,
} }
h.serverDomain = serverDomain h.serverDomain = serverDomain
return h return h
@ -85,16 +88,16 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error {
sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF)) sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + protocol.CRLF))
for _, v := range groups { for _, v := range groups {
// TODO set actual post permission status // 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
} }
if c > 0 { if c > 0 {
highWaterMark, err := h.backend.GetGroupHighWaterMark(v) highWaterMark, err := h.backend.GetGroupHighWaterMark(&v)
if err != nil { if err != nil {
return err return err
} }
lowWaterMark, err := h.backend.GetGroupLowWaterMark(v) lowWaterMark, err := h.backend.GetGroupLowWaterMark(&v)
if err != nil { if err != nil {
return err return err
} }
@ -169,15 +172,15 @@ func (h *Handler) handleGroup(s *Session, arguments []string, id uint) error {
return err return err
} }
} }
highWaterMark, err := h.backend.GetGroupHighWaterMark(g) highWaterMark, err := h.backend.GetGroupHighWaterMark(&g)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return err return err
} }
lowWaterMark, err := h.backend.GetGroupLowWaterMark(g) lowWaterMark, err := h.backend.GetGroupLowWaterMark(&g)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return err return err
} }
articlesCount, err := h.backend.GetArticlesCount(g) articlesCount, err := h.backend.GetArticlesCount(&g)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return err return err
} }
@ -230,16 +233,16 @@ func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error
dw.Write([]byte(protocol.NNTPResponse{Code: 231, Message: "list of new newsgroups follows"}.String() + protocol.CRLF)) dw.Write([]byte(protocol.NNTPResponse{Code: 231, Message: "list of new newsgroups follows"}.String() + protocol.CRLF))
for _, v := range g { for _, v := range g {
// TODO set actual post permission status // 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
} }
if c > 0 { if c > 0 {
highWaterMark, err := h.backend.GetGroupHighWaterMark(v) highWaterMark, err := h.backend.GetGroupHighWaterMark(&v)
if err != nil { if err != nil {
return err return err
} }
lowWaterMark, err := h.backend.GetGroupLowWaterMark(v) lowWaterMark, err := h.backend.GetGroupLowWaterMark(&v)
if err != nil { if err != nil {
return err return err
} }
@ -321,6 +324,66 @@ func (h *Handler) handlePost(s *Session, arguments []string, id uint) error {
return s.tconn.PrintfLine(protocol.MessageArticleReceived) return s.tconn.PrintfLine(protocol.MessageArticleReceived)
} }
func (h *Handler) handleListgroup(s *Session, arguments []string, id uint) error {
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
currentGroup := s.currentGroup
var low, high int64
if len(arguments) == 1 {
g, err := h.backend.GetGroup(arguments[0])
if err != nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 411, Message: "No such newsgroup"}.String())
}
currentGroup = &g
} else if len(arguments) == 2 {
g, err := h.backend.GetGroup(arguments[0])
if err != nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 411, Message: "No such newsgroup"}.String())
}
currentGroup = &g
low, high, err = utils.ParseRange(arguments[1])
if err != nil {
low = 0
high = 0
}
if high != -1 && low > high {
low = -1
high = -1
}
}
if currentGroup == nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 412, Message: "No newsgroup selected"}.String())
}
highWaterMark, err := h.backend.GetGroupHighWaterMark(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
lowWaterMark, err := h.backend.GetGroupLowWaterMark(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
articlesCount, err := h.backend.GetArticlesCount(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
nums, err := h.backend.GetArticleNumbers(currentGroup, low, high)
if err != nil && err != sql.ErrNoRows {
return err
}
dw := s.tconn.DotWriter()
dw.Write([]byte(protocol.NNTPResponse{Code: 211, Message: fmt.Sprintf("%d %d %d %s list follows%s", articlesCount, lowWaterMark, highWaterMark, currentGroup.GroupName, protocol.CRLF)}.String()))
for _, v := range nums {
dw.Write([]byte(strconv.FormatInt(v, 10) + protocol.CRLF))
}
return dw.Close()
}
func (h *Handler) Handle(s *Session, message string, id uint) error { func (h *Handler) Handle(s *Session, message string, id uint) error {
splittedMessage := strings.Split(message, " ") splittedMessage := strings.Split(message, " ")
for i, v := range splittedMessage { for i, v := range splittedMessage {
@ -331,7 +394,7 @@ func (h *Handler) Handle(s *Session, message string, id uint) error {
if !ok { if !ok {
s.tconn.StartResponse(id) s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id) defer s.tconn.EndResponse(id)
return s.tconn.PrintfLine(protocol.MessageUnknownCommand) return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 500, Message: "Unknown command"}.String())
} }
return handler(s, splittedMessage[1:], id) return handler(s, splittedMessage[1:], id)
} }

27
internal/utils/range.go Normal file
View File

@ -0,0 +1,27 @@
package utils
import (
"fmt"
"strconv"
"strings"
)
func ParseRange(spec string) (int64, int64, error) {
if spec == "" {
return 0, 0, fmt.Errorf("no range specified")
}
parts := strings.Split(spec, "-")
if len(parts) == 1 {
h, err := strconv.ParseInt(parts[0], 10, 64)
return -1, h, err
}
l, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, 0, err
}
if parts[1] == "" {
return l, -1, nil
}
h, err := strconv.ParseInt(parts[1], 10, 64)
return l, h, err
}