initial commit, read-only NNTP server
This commit is contained in:
commit
fe171d9fa9
|
@ -0,0 +1,5 @@
|
|||
module iris-news
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/dustin/go-nntp v0.0.0-20210723005859-f00d51cf8cc1
|
|
@ -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=
|
|
@ -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") + ">"
|
||||
}
|
Loading…
Reference in New Issue