From 05816654d3e6d6b5a6dbc1283ec052627b5610bd Mon Sep 17 00:00:00 2001 From: ChronosX88 Date: Sat, 5 Feb 2022 21:31:22 +0300 Subject: [PATCH] Implement OVER/XOVER command --- README.md | 4 +- internal/backend/sqlite/sqlite.go | 18 +++++ internal/backend/storage_backend.go | 1 + internal/protocol/constants.go | 2 + internal/server/handler.go | 100 ++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b62f4e2..8359f85 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ - :heavy_check_mark: `HEAD` - :heavy_check_mark: `BODY` - :heavy_check_mark: `STAT` -- :x: Articles overview - - :x: `OVER` +- :construction: Articles overview + - :heavy_check_mark: `OVER` - :x: `LIST OVERVIEW.FMT` - :x: `HDR` - :x: `LIST HEADERS` diff --git a/internal/backend/sqlite/sqlite.go b/internal/backend/sqlite/sqlite.go index d430f4c..4508510 100644 --- a/internal/backend/sqlite/sqlite.go +++ b/internal/backend/sqlite/sqlite.go @@ -195,6 +195,24 @@ func (sb *SQLiteBackend) GetNextArticleByNum(g *models.Group, a *models.Article) return nextArticle, json.Unmarshal([]byte(nextArticle.HeaderRaw), &nextArticle.Header) } +func (sb *SQLiteBackend) GetArticlesByRange(g *models.Group, low, high int64) ([]models.Article, error) { + var articles []models.Article + + if err := sb.db.Select(&articles, "SELECT articles.* FROM articles INNER JOIN articles_to_groups atg on atg.article_id = articles.id WHERE atg.article_number >= ? AND atg.article_number <= ? AND atg.group_id = ? ORDER BY atg.article_number", low, high, g.ID); err != nil { + return nil, err + } + for i := 0; i < len(articles); i++ { + if err := sb.db.Get(&articles[i].ArticleNumber, "SELECT article_number FROM articles_to_groups WHERE article_id = ?", articles[i].ID); err != nil { + return nil, err + } + if err := json.Unmarshal([]byte(articles[i].HeaderRaw), &articles[i].Header); err != nil { + return nil, err + } + } + + return articles, nil +} + func (sb *SQLiteBackend) GetNewArticlesSince(timestamp int64) ([]string, error) { var articleIds []string return articleIds, sb.db.Select(&articleIds, "SELECT json_extract(articles.header, '$.Message-Id[0]') FROM articles WHERE created_at > datetime(?, 'unixepoch')", timestamp) diff --git a/internal/backend/storage_backend.go b/internal/backend/storage_backend.go index ce9e0fc..10a7f60 100644 --- a/internal/backend/storage_backend.go +++ b/internal/backend/storage_backend.go @@ -21,4 +21,5 @@ type StorageBackend interface { GetNewArticlesSince(timestamp int64) ([]string, error) GetLastArticleByNum(g *models.Group, a *models.Article) (models.Article, error) GetNextArticleByNum(g *models.Group, a *models.Article) (models.Article, error) + GetArticlesByRange(g *models.Group, low, high int64) ([]models.Article, error) } diff --git a/internal/protocol/constants.go b/internal/protocol/constants.go index 02184c8..e524cd6 100644 --- a/internal/protocol/constants.go +++ b/internal/protocol/constants.go @@ -64,6 +64,8 @@ const ( CommandNewNews = "NEWNEWS" CommandLast = "LAST" CommandNext = "NEXT" + CommandOver = "OVER" + CommandXover = "XOVER" ) const ( diff --git a/internal/server/handler.go b/internal/server/handler.go index 95972ee..0bb325a 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -44,6 +44,8 @@ func NewHandler(b backend.StorageBackend, serverDomain string) *Handler { protocol.CommandNewNews: h.handleNewNews, protocol.CommandLast: h.handleLast, protocol.CommandNext: h.handleNext, + protocol.CommandOver: h.handleOver, + protocol.CommandXover: h.handleOver, } h.serverDomain = serverDomain return h @@ -300,6 +302,11 @@ func (h *Handler) handlePost(s *Session, command string, arguments []string, id // set path header headers.Set("Path", fmt.Sprintf("%s!not-for-mail", h.serverDomain)) + // set date header + if headers.Get("Date") == "" { + headers.Set("Date", time.Now().UTC().Format(time.RFC1123Z)) + } + headerJson, err := json.Marshal(headers) if err != nil { return err @@ -707,6 +714,99 @@ func (h *Handler) handleNext(s *Session, command string, arguments []string, id return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 223, Message: fmt.Sprintf("%d %s retrieved", a.ArticleNumber, a.Header.Get("Message-Id"))}.String()) } +func (h *Handler) handleOver(s *Session, command string, arguments []string, id uint) error { + s.tconn.StartResponse(id) + defer s.tconn.EndResponse(id) + + if len(arguments) == 0 && s.currentArticle == nil { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 420, Message: "No current article selected"}.String()) + } + + byRange := false + byNum := false + byMsgID := false + curArticle := false + + if len(arguments) == 1 { + if _, _, err := utils.ParseRange(arguments[0]); err == nil { + byRange = true + } else if strings.ContainsAny(arguments[0], "<>") { + byMsgID = true + } else if _, err := strconv.Atoi(arguments[0]); err == nil { + byNum = true + } + } else if len(arguments) == 0 { + curArticle = true + } else { + return s.tconn.PrintfLine(protocol.ErrSyntaxError.String()) + } + + var articles []models.Article + + if byRange { + if s.currentGroup == nil { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 412, Message: "No newsgroup selected"}.String()) + } + + low, high, err := utils.ParseRange(arguments[0]) + if err != nil { + return err + } + if low > high { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 423, Message: "Empty range"}.String()) + } + a, err := h.backend.GetArticlesByRange(s.currentGroup, low, high) + if err != nil { + if err == sql.ErrNoRows { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 423, Message: "No articles in that range"}.String()) + } + return err + } + articles = append(articles, a...) + } else if byMsgID { + a, err := h.backend.GetArticle(arguments[0]) + if err != nil { + if err == sql.ErrNoRows { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 430, Message: "No such article with that message-id"}.String()) + } + return err + } + a.ArticleNumber = 0 + articles = append(articles, a) + } else if byNum { + num, _ := strconv.Atoi(arguments[0]) + a, err := h.backend.GetArticleByNumber(s.currentGroup, num) + if err != nil { + if err == sql.ErrNoRows { + return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 423, Message: "No such article in this group"}.String()) + } + return err + } + articles = append(articles, a) + } else if curArticle { + articles = append(articles, *s.currentArticle) + } + + dw := s.tconn.DotWriter() + dw.Write([]byte(protocol.NNTPResponse{Code: 224, Message: "Overview information follows" + protocol.CRLF}.String())) + for _, v := range articles { + dw.Write([]byte(strconv.Itoa(v.ArticleNumber) + " ")) + dw.Write([]byte(v.Header.Get("Subject") + " ")) + dw.Write([]byte(v.Header.Get("From") + " ")) + dw.Write([]byte(v.Header.Get("Date") + " ")) + dw.Write([]byte(v.Header.Get("Message-ID") + " ")) + dw.Write([]byte(v.Header.Get("References") + " ")) + + bytesMetadata := len([]byte(v.Body)) + linesMetadata := strings.Count(v.Body, "\n") + + dw.Write([]byte(strconv.Itoa(bytesMetadata) + " ")) + dw.Write([]byte(strconv.Itoa(linesMetadata) + protocol.CRLF)) + } + + return dw.Close() +} + func (h *Handler) Handle(s *Session, message string, id uint) error { splittedMessage := strings.Split(message, " ") for i, v := range splittedMessage {