mirror of
https://github.com/ChronosX88/yans.git
synced 2024-11-21 11:22:19 +00:00
Implement POST command
This commit is contained in:
parent
e8aa350c53
commit
163f2bcba6
@ -32,7 +32,7 @@
|
|||||||
- :x: `NEWNEWS`
|
- :x: `NEWNEWS`
|
||||||
- :x: `NEXT`
|
- :x: `NEXT`
|
||||||
- :x: `OVER`
|
- :x: `OVER`
|
||||||
- :x: `POST`
|
- :construction: `POST`
|
||||||
- :x: `STAT`
|
- :x: `STAT`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
address = "localhost"
|
address = "localhost"
|
||||||
port = 1119
|
port = 1119
|
||||||
backend_type = "sqlite"
|
backend_type = "sqlite"
|
||||||
|
domain = "localhost"
|
||||||
|
|
||||||
[sqlite]
|
[sqlite]
|
||||||
path = "yans.db"
|
path = "yans.db"
|
@ -4,15 +4,13 @@ CREATE TABLE IF NOT EXISTS groups(
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
group_name TEXT UNIQUE NOT NULL,
|
group_name TEXT UNIQUE NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
created_at UNSIGNED BIG INT NOT NULL
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS articles(
|
CREATE TABLE IF NOT EXISTS articles(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
date INTEGER NOT NULL,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
path TEXT,
|
header TEXT,
|
||||||
reply_to TEXT,
|
|
||||||
thread TEXT,
|
thread TEXT,
|
||||||
subject TEXT NOT NULL,
|
|
||||||
body TEXT NOT NULL
|
body TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS articles_to_groups(
|
CREATE TABLE IF NOT EXISTS articles_to_groups(
|
||||||
|
@ -3,6 +3,8 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"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/ChronosX88/yans/internal/utils"
|
||||||
@ -11,6 +13,7 @@ import (
|
|||||||
"github.com/mattn/go-sqlite3"
|
"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"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
@ -91,5 +94,43 @@ func (sb *SQLiteBackend) GetGroup(groupName string) (models.Group, error) {
|
|||||||
|
|
||||||
func (sb *SQLiteBackend) GetNewGroupsSince(timestamp int64) ([]models.Group, error) {
|
func (sb *SQLiteBackend) GetNewGroupsSince(timestamp int64) ([]models.Group, error) {
|
||||||
var groups []models.Group
|
var groups []models.Group
|
||||||
return groups, sb.db.Select(&groups, "SELECT * FROM groups WHERE created_at > ?", timestamp)
|
return groups, sb.db.Select(&groups, "SELECT * FROM groups WHERE created_at > datetime(?, 'unixepoch')", timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sb *SQLiteBackend) SaveArticle(a models.Article, groups []string) error {
|
||||||
|
res, err := sb.db.Exec("INSERT INTO articles (header, body, thread) VALUES (?, ?, ?)", a.HeaderRaw, a.Body, a.Thread)
|
||||||
|
articleID, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupIDs []int
|
||||||
|
for _, v := range groups {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
g, err := sb.GetGroup(v)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("no such newsgroup")
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groupIDs = append(groupIDs, g.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range groupIDs {
|
||||||
|
_, err = sb.db.Exec("INSERT INTO articles_to_groups (article_id, group_id) VALUES (?, ?)", articleID, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sb *SQLiteBackend) GetArticle(messageID string) (models.Article, error) {
|
||||||
|
var a models.Article
|
||||||
|
if err := sb.db.Get(&a, "SELECT * FROM articles WHERE json_extract(articles.header, '$.Message-ID[0]') = ?", messageID); err != nil {
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
return a, json.Unmarshal([]byte(a.HeaderRaw), &a.Header)
|
||||||
}
|
}
|
||||||
|
@ -14,4 +14,6 @@ type StorageBackend interface {
|
|||||||
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
|
||||||
|
GetArticle(messageID string) (models.Article, error)
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ type Config struct {
|
|||||||
Address string `toml:"address"`
|
Address string `toml:"address"`
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
BackendType string `toml:"backend_type"`
|
BackendType string `toml:"backend_type"`
|
||||||
|
Domain string `toml:"domain"`
|
||||||
SQLite SQLiteBackendConfig `toml:"sqlite"`
|
SQLite SQLiteBackendConfig `toml:"sqlite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
81
internal/models/article.go
Normal file
81
internal/models/article.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/textproto"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Article struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
HeaderRaw string `db:"header"`
|
||||||
|
Header textproto.MIMEHeader `db:"-"`
|
||||||
|
Body string `db:"body"`
|
||||||
|
Thread sql.NullString `db:"thread"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//func ParseArticle(lines []string) (Article, error) {
|
||||||
|
// article := Article{}
|
||||||
|
// headerBlock := true
|
||||||
|
// for _, v := range lines {
|
||||||
|
// if v == "" {
|
||||||
|
// headerBlock = false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if headerBlock {
|
||||||
|
// kv := strings.Split(v, ":")
|
||||||
|
// if len(kv) < 2 {
|
||||||
|
// return Article{}, fmt.Errorf("invalid header format")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// kv[0] = strings.TrimSpace(kv[0])
|
||||||
|
// kv[1] = strings.TrimSpace(kv[1])
|
||||||
|
//
|
||||||
|
// if !protocol.IsMessageHeaderAllowed(kv[0]) {
|
||||||
|
// return Article{}, fmt.Errorf("invalid header element")
|
||||||
|
// }
|
||||||
|
// if kv[1] == "" {
|
||||||
|
// return Article{}, fmt.Errorf("header value should not be empty")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// switch kv[0] {
|
||||||
|
// case "Archive":
|
||||||
|
// {
|
||||||
|
// if kv[1] == "yes" {
|
||||||
|
// article.Archive = true
|
||||||
|
// } else {
|
||||||
|
// article.Archive = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// case "Injection-Date":
|
||||||
|
// {
|
||||||
|
// date, err := mail.ParseDate(kv[1])
|
||||||
|
// if err != nil {
|
||||||
|
// return Article{}, err
|
||||||
|
// }
|
||||||
|
// article.InjectionDate = date
|
||||||
|
// }
|
||||||
|
// case "Date":
|
||||||
|
// {
|
||||||
|
// date, err := mail.ParseDate(kv[1])
|
||||||
|
// if err != nil {
|
||||||
|
// return Article{}, err
|
||||||
|
// }
|
||||||
|
// article.Date = date
|
||||||
|
// }
|
||||||
|
// case "Expires":
|
||||||
|
// {
|
||||||
|
// date, err := mail.ParseDate(kv[1])
|
||||||
|
// if err != nil {
|
||||||
|
// return Article{}, err
|
||||||
|
// }
|
||||||
|
// article.Expires = date
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// } else {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return article, nil
|
||||||
|
//}
|
@ -1,8 +1,10 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
GroupName string `db:"group_name"`
|
GroupName string `db:"group_name"`
|
||||||
Description *string `db:"description"`
|
Description *string `db:"description"`
|
||||||
CreatedAt uint64 `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,46 @@
|
|||||||
package protocol
|
package protocol
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSyntaxError = NNTPResponse{Code: 501, Message: "Syntax Error"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsMessageHeaderAllowed(headerName string) bool {
|
||||||
|
switch headerName {
|
||||||
|
case
|
||||||
|
"Date",
|
||||||
|
"From",
|
||||||
|
"Message-ID",
|
||||||
|
"Newsgroups",
|
||||||
|
"Path",
|
||||||
|
"Subject",
|
||||||
|
"Comments",
|
||||||
|
"Keywords",
|
||||||
|
"In-Reply-To",
|
||||||
|
"Sender",
|
||||||
|
"MIME-Version",
|
||||||
|
"Content-Type",
|
||||||
|
"Content-Transfer-Encoding",
|
||||||
|
"Content-Disposition",
|
||||||
|
"Content-Language",
|
||||||
|
"Approved",
|
||||||
|
"Archive",
|
||||||
|
"Control",
|
||||||
|
"Distribution",
|
||||||
|
"Expires",
|
||||||
|
"Followup-To",
|
||||||
|
"Injection-Date",
|
||||||
|
"Injection-Info",
|
||||||
|
"Organization",
|
||||||
|
"References",
|
||||||
|
"Summary",
|
||||||
|
"Supersedes",
|
||||||
|
"User-Agent",
|
||||||
|
"Xref":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CRLF = "\r\n"
|
CRLF = "\r\n"
|
||||||
MultilineEnding = "."
|
MultilineEnding = "."
|
||||||
@ -13,6 +54,7 @@ const (
|
|||||||
CommandList = "LIST"
|
CommandList = "LIST"
|
||||||
CommandGroup = "GROUP"
|
CommandGroup = "GROUP"
|
||||||
CommandNewGroups = "NEWGROUPS"
|
CommandNewGroups = "NEWGROUPS"
|
||||||
|
CommandPost = "POST"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -35,7 +77,7 @@ const (
|
|||||||
MessageUnknownCommand = "500 Unknown command"
|
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"
|
||||||
MessageNewGroupsListOfNewsgroupsFollows = "231 list of new newsgroups follows"
|
|
||||||
MessageSyntaxError = "501 Syntax Error"
|
|
||||||
MessageNoSuchGroup = "411 No such newsgroup"
|
MessageNoSuchGroup = "411 No such newsgroup"
|
||||||
|
MessageInputArticle = "340 Input article; end with <CR-LF>.<CR-LF>"
|
||||||
|
MessageArticleReceived = "240 Article received OK"
|
||||||
)
|
)
|
||||||
|
12
internal/protocol/nntp_response.go
Normal file
12
internal/protocol/nntp_response.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type NNTPResponse struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nr NNTPResponse) String() string {
|
||||||
|
return fmt.Sprintf("%d %s", nr.Code, nr.Message)
|
||||||
|
}
|
@ -2,20 +2,25 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"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/models"
|
||||||
"github.com/ChronosX88/yans/internal/protocol"
|
"github.com/ChronosX88/yans/internal/protocol"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"io"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
handlers map[string]func(s *Session, arguments []string, id uint) error
|
handlers map[string]func(s *Session, arguments []string, id uint) error
|
||||||
backend backend.StorageBackend
|
backend backend.StorageBackend
|
||||||
|
serverDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(b backend.StorageBackend) *Handler {
|
func NewHandler(b backend.StorageBackend, serverDomain string) *Handler {
|
||||||
h := &Handler{}
|
h := &Handler{}
|
||||||
h.backend = b
|
h.backend = b
|
||||||
h.handlers = map[string]func(s *Session, arguments []string, id uint) error{
|
h.handlers = map[string]func(s *Session, arguments []string, id uint) error{
|
||||||
@ -26,7 +31,9 @@ func NewHandler(b backend.StorageBackend) *Handler {
|
|||||||
protocol.CommandMode: h.handleModeReader,
|
protocol.CommandMode: h.handleModeReader,
|
||||||
protocol.CommandGroup: h.handleGroup,
|
protocol.CommandGroup: h.handleGroup,
|
||||||
protocol.CommandNewGroups: h.handleNewGroups,
|
protocol.CommandNewGroups: h.handleNewGroups,
|
||||||
|
protocol.CommandPost: h.handlePost,
|
||||||
}
|
}
|
||||||
|
h.serverDomain = serverDomain
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +130,7 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
return s.tconn.PrintfLine(protocol.MessageSyntaxError)
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +141,7 @@ func (h *Handler) handleList(s *Session, arguments []string, id uint) error {
|
|||||||
|
|
||||||
func (h *Handler) handleModeReader(s *Session, arguments []string, id uint) error {
|
func (h *Handler) handleModeReader(s *Session, arguments []string, id uint) error {
|
||||||
if len(arguments) == 0 || arguments[0] != "READER" {
|
if len(arguments) == 0 || arguments[0] != "READER" {
|
||||||
return s.tconn.PrintfLine(protocol.MessageSyntaxError)
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
(&s.capabilities).Remove(protocol.ModeReaderCapability)
|
(&s.capabilities).Remove(protocol.ModeReaderCapability)
|
||||||
@ -151,7 +158,7 @@ func (h *Handler) handleGroup(s *Session, arguments []string, id uint) error {
|
|||||||
defer s.tconn.EndResponse(id)
|
defer s.tconn.EndResponse(id)
|
||||||
|
|
||||||
if len(arguments) == 0 || len(arguments) > 1 {
|
if len(arguments) == 0 || len(arguments) > 1 {
|
||||||
return s.tconn.PrintfLine(protocol.MessageSyntaxError)
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := h.backend.GetGroup(arguments[0])
|
g, err := h.backend.GetGroup(arguments[0])
|
||||||
@ -177,7 +184,10 @@ func (h *Handler) handleGroup(s *Session, arguments []string, id uint) error {
|
|||||||
|
|
||||||
s.currentGroup = &g
|
s.currentGroup = &g
|
||||||
|
|
||||||
return s.tconn.PrintfLine("211 %d %d %d %s", articlesCount, lowWaterMark, highWaterMark, g.GroupName)
|
return s.tconn.PrintfLine(protocol.NNTPResponse{
|
||||||
|
Code: 211,
|
||||||
|
Message: fmt.Sprintf("%d %d %d %s", articlesCount, lowWaterMark, highWaterMark, g.GroupName),
|
||||||
|
}.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error {
|
func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error {
|
||||||
@ -185,7 +195,7 @@ func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error
|
|||||||
defer s.tconn.EndResponse(id)
|
defer s.tconn.EndResponse(id)
|
||||||
|
|
||||||
if len(arguments) < 2 || len(arguments) > 3 {
|
if len(arguments) < 2 || len(arguments) > 3 {
|
||||||
return s.tconn.PrintfLine(protocol.MessageSyntaxError)
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
dateString := arguments[0] + " " + arguments[1]
|
dateString := arguments[0] + " " + arguments[1]
|
||||||
@ -208,7 +218,7 @@ func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return s.tconn.PrintfLine(protocol.MessageSyntaxError)
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := h.backend.GetNewGroupsSince(date.Unix())
|
g, err := h.backend.GetNewGroupsSince(date.Unix())
|
||||||
@ -216,9 +226,8 @@ func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
dw := s.tconn.DotWriter()
|
||||||
|
dw.Write([]byte(protocol.NNTPResponse{Code: 231, Message: "list of new newsgroups follows"}.String() + protocol.CRLF))
|
||||||
sb.Write([]byte(protocol.MessageListOfNewsgroupsFollows + 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)
|
||||||
@ -234,14 +243,82 @@ func (h *Handler) handleNewGroups(s *Session, arguments []string, id uint) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sb.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark)))
|
dw.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark)))
|
||||||
} else {
|
} else {
|
||||||
sb.Write([]byte(fmt.Sprintf("%s 0 1 n"+protocol.CRLF, v.GroupName)))
|
dw.Write([]byte(fmt.Sprintf("%s 0 1 n"+protocol.CRLF, v.GroupName)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.Write([]byte(protocol.MultilineEnding))
|
|
||||||
|
|
||||||
return s.tconn.PrintfLine(sb.String())
|
return dw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handlePost(s *Session, arguments []string, id uint) error {
|
||||||
|
s.tconn.StartResponse(id)
|
||||||
|
defer s.tconn.EndResponse(id)
|
||||||
|
|
||||||
|
if len(arguments) != 0 {
|
||||||
|
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.tconn.PrintfLine(protocol.MessageInputArticle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := s.tconn.ReadMIMEHeader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate message id
|
||||||
|
messageID := fmt.Sprintf("<%s@%s>", uuid.New().String(), h.serverDomain)
|
||||||
|
headers["Message-ID"] = []string{messageID}
|
||||||
|
|
||||||
|
headerJson, err := json.Marshal(headers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a := models.Article{}
|
||||||
|
a.HeaderRaw = string(headerJson)
|
||||||
|
a.Header = headers
|
||||||
|
|
||||||
|
dr := s.tconn.DotReader()
|
||||||
|
// TODO handle multipart message
|
||||||
|
body, err := io.ReadAll(dr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Body = string(body)
|
||||||
|
|
||||||
|
// set thread property
|
||||||
|
if headers.Get("In-Reply-To") != "" {
|
||||||
|
parentMessage, err := h.backend.GetArticle(headers.Get("In-Reply-To"))
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: "no such message you are replying to"}.String())
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !parentMessage.Thread.Valid {
|
||||||
|
var parentHeader mail.Header
|
||||||
|
err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader)
|
||||||
|
parentMessageID := parentHeader["Message-ID"]
|
||||||
|
a.Thread = sql.NullString{String: parentMessageID[0], Valid: true}
|
||||||
|
} else {
|
||||||
|
a.Thread = parentMessage.Thread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.backend.SaveArticle(a, strings.Split(a.Header.Get("Newsgroups"), ","))
|
||||||
|
if err != nil {
|
||||||
|
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: err.Error()}.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.tconn.PrintfLine(protocol.MessageArticleReceived)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Handle(s *Session, message string, id uint) error {
|
func (h *Handler) Handle(s *Session, message string, id uint) error {
|
||||||
|
@ -96,7 +96,7 @@ func (ns *NNTPServer) Start() error {
|
|||||||
|
|
||||||
id, _ := uuid.NewUUID()
|
id, _ := uuid.NewUUID()
|
||||||
closed := make(chan bool)
|
closed := make(chan bool)
|
||||||
session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend))
|
session, err := NewSession(ctx, conn, Capabilities, id.String(), closed, NewHandler(ns.backend, ns.cfg.Domain))
|
||||||
ns.sessionPoolMutex.Lock()
|
ns.sessionPoolMutex.Lock()
|
||||||
ns.sessionPool[id.String()] = session
|
ns.sessionPool[id.String()] = session
|
||||||
ns.sessionPoolMutex.Unlock()
|
ns.sessionPoolMutex.Unlock()
|
||||||
|
Loading…
Reference in New Issue
Block a user