initial commit, read-only NNTP server

This commit is contained in:
TJP 2023-02-03 07:38:09 -07:00
commit fe171d9fa9
3 changed files with 221 additions and 0 deletions

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module iris-news
go 1.19
require github.com/dustin/go-nntp v0.0.0-20210723005859-f00d51cf8cc1

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/dustin/go-couch v0.0.0-20160816170231-8251128dab73/go.mod h1:WG/TWzFd/MRvOZ4jjna3FQ+K8AKhb2jOw4S2JMw9VKI=
github.com/dustin/go-nntp v0.0.0-20210723005859-f00d51cf8cc1 h1:R90ND7acg9HKYj3oJBKKefk73DULdC7IlcnS7MV0X1s=
github.com/dustin/go-nntp v0.0.0-20210723005859-f00d51cf8cc1/go.mod h1:elGbp3dKCIIdwu6jm3y6L93EVn+I6MSzYrcZXhpNS3Y=
github.com/dustin/httputil v0.0.0-20170305193905-c47743f54f89/go.mod h1:ZoDWdnxro8Kesk3zrCNOHNFWtajFPSnDMjVEjGjQu/0=

212
main.go Normal file
View File

@ -0,0 +1,212 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net"
"net/textproto"
"os"
"os/exec"
"path"
"strconv"
"strings"
"time"
"github.com/dustin/go-nntp"
nntpserver "github.com/dustin/go-nntp/server"
)
func main() {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
if err := writeLocation(l.Addr().String()); err != nil {
log.Fatal(err)
}
s := nntpserver.NewServer(&backend{})
for {
c, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go s.Process(c)
}
}
func writeLocation(location string) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
filepath := path.Join(home, ".iris-news-server")
f, err := os.Create(filepath)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
_, err = fmt.Fprintln(f, location)
return err
}
var group = &nntp.Group{
Name: "ctrl-c.iris",
Description: "The iris message board.",
Posting: nntp.PostingNotPermitted,
Low: 1,
}
type backend struct {
messages []*nntp.Article
}
func (b backend) ListGroups(max int) ([]*nntp.Group, error) {
return []*nntp.Group{group}, nil
}
func (b *backend) GetGroup(name string) (*nntp.Group, error) {
if name != group.Name {
return nil, nntpserver.ErrNoSuchGroup
}
if err := b.refresh(); err != nil {
return nil, err
}
return group, nil
}
func (b *backend) refresh() error {
binpath, err := exec.LookPath("iris")
if err != nil {
return err
}
cmd := exec.Command(binpath, "-d")
buf := &bytes.Buffer{}
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
return err
}
msgs := irisDump{}
if err := json.NewDecoder(buf).Decode(&msgs); err != nil {
return err
}
b.messages = msgs.Articles()
group.High = int64(len(b.messages))
group.Count = int64(len(b.messages))
return nil
}
func (b *backend) GetArticles(_ *nntp.Group, from, to int64) ([]nntpserver.NumberedArticle, error) {
if err := b.refresh(); err != nil {
return nil, err
}
numbered := make([]nntpserver.NumberedArticle, 0, len(b.messages))
for i, msg := range b.messages {
num := int64(i + 1)
if num >= from && num <= to {
numbered = append(numbered, nntpserver.NumberedArticle{
Num: num,
Article: msg,
})
}
}
return numbered, nil
}
func (b *backend) GetArticle(_ *nntp.Group, messageID string) (*nntp.Article, error) {
if len(b.messages) == 0 {
if err := b.refresh(); err != nil {
return nil, err
}
}
for _, msg := range b.messages {
if msg.Header.Get("Message-Id") == messageID {
return msg, nil
}
}
num, err := strconv.Atoi(messageID)
if err == nil && num <= len(b.messages) {
return b.messages[num-1], nil
}
return nil, nntpserver.ErrInvalidMessageID
}
func (b backend) Authorized() bool {
return true
}
func (b backend) Authenticate(user, pass string) (nntpserver.Backend, error) {
return nil, nil
}
func (b backend) AllowPost() bool {
return false
}
func (b backend) Post(article *nntp.Article) error {
return nntpserver.ErrPostingNotPermitted
}
type irisDump []struct {
Hash string `json:"hash"`
EditHash string `json:"edit_hash"`
IsDeleted *bool `json:"is_deleted"`
Data struct {
Author string `json:"author"`
Parent *string `json:"parent"`
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
} `json:"data"`
}
func (id irisDump) Articles() []*nntp.Article {
out := make([]*nntp.Article, len(id))
subjects := make(map[string]string)
for i, d := range id {
msgID := toMsgID(d.Hash)
out[i] = &nntp.Article{
Header: textproto.MIMEHeader{
"Message-Id": []string{msgID},
"From": []string{d.Data.Author},
"Newsgroups": []string{group.Name},
"Date": []string{d.Data.Timestamp.Format(time.RFC1123Z)},
},
Body: bytes.NewBufferString(d.Data.Message),
Bytes: len(d.Data.Message),
Lines: strings.Count(d.Data.Message, "\n"),
}
if d.Data.Parent != nil {
parentID := toMsgID(*d.Data.Parent)
out[i].Header.Add("References", parentID)
out[i].Header.Add("Subject", "Re: "+subjects[parentID])
} else {
subject := strings.SplitN(d.Data.Message, "\n", 2)[0]
out[i].Header.Add("Subject", subject)
subjects[msgID] = subject
}
}
return out
}
func toMsgID(irisID string) string {
return "<" + strings.TrimSuffix(irisID, "\n") + ">"
}