package hotline
import (
- "bytes"
- "crypto/rand"
+ "cmp"
"encoding/binary"
- "sort"
+ "io"
+ "slices"
)
+var (
+ NewsBundle = [2]byte{0, 2}
+ NewsCategory = [2]byte{0, 3}
+)
+
+type ThreadedNewsMgr interface {
+ ListArticles(newsPath []string) NewsArtListData
+ GetArticle(newsPath []string, articleID uint32) *NewsArtData
+ DeleteArticle(newsPath []string, articleID uint32, recursive bool) error
+ PostArticle(newsPath []string, parentArticleID uint32, article NewsArtData) error
+ CreateGrouping(newsPath []string, name string, t [2]byte) error
+ GetCategories(paths []string) []NewsCategoryListData15
+ NewsItem(newsPath []string) NewsCategoryListData15
+ DeleteNewsItem(newsPath []string) error
+}
+
+// ThreadedNews contains the top level of threaded news categories, bundles, and articles.
type ThreadedNews struct {
Categories map[string]NewsCategoryListData15 `yaml:"Categories"`
}
type NewsCategoryListData15 struct {
- Type []byte `yaml:"Type"` // Size 2 ; Bundle (2) or category (3)
- Count []byte // Article or SubCategory count Size 2
- NameSize byte
- Name string `yaml:"Name"` //
+ Type [2]byte `yaml:"Type,flow"` // Bundle (2) or category (3)
+ Name string `yaml:"Name"`
Articles map[uint32]*NewsArtData `yaml:"Articles"` // Optional, if Type is Category
SubCats map[string]NewsCategoryListData15 `yaml:"SubCats"`
- GUID []byte // Size 16
- AddSN []byte // Size 4
- DeleteSN []byte // Size 4
+ GUID [16]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
+ AddSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
+ DeleteSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
+
+ readOffset int // Internal offset to track read progress
}
func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
var newsArtsPayload []byte
for i, art := range newscat.Articles {
- ID := make([]byte, 4)
- binary.BigEndian.PutUint32(ID, i)
+ id := make([]byte, 4)
+ binary.BigEndian.PutUint32(id, i)
- newArt := NewsArtList{
- ID: ID,
+ newsArts = append(newsArts, NewsArtList{
+ ID: [4]byte(id),
TimeStamp: art.Date,
ParentID: art.ParentArt,
- Flags: []byte{0, 0, 0, 0},
- FlavorCount: []byte{0, 0},
Title: []byte(art.Title),
Poster: []byte(art.Poster),
ArticleSize: art.DataSize(),
- }
- newsArts = append(newsArts, newArt)
+ })
}
- sort.Sort(byID(newsArts))
+ // Sort the articles by ID. This is important for displaying the message threading correctly on the client side.
+ slices.SortFunc(newsArts, func(a, b NewsArtList) int {
+ return cmp.Compare(
+ binary.BigEndian.Uint32(a.ID[:]),
+ binary.BigEndian.Uint32(b.ID[:]),
+ )
+ })
for _, v := range newsArts {
- newsArtsPayload = append(newsArtsPayload, v.Payload()...)
+ b, err := io.ReadAll(&v)
+ if err != nil {
+ // TODO
+ panic(err)
+ }
+ newsArtsPayload = append(newsArtsPayload, b...)
}
- nald := NewsArtListData{
- ID: []byte{0, 0, 0, 0},
+ return NewsArtListData{
+ Count: len(newsArts),
Name: []byte{},
Description: []byte{},
NewsArtList: newsArtsPayload,
}
-
- return nald
}
-// NewsArtData represents single news article
+// NewsArtData represents an individual news article.
type NewsArtData struct {
- Title string `yaml:"Title"`
- Poster string `yaml:"Poster"`
- Date []byte `yaml:"Date"` // size 8
- PrevArt []byte `yaml:"PrevArt"` // size 4
- NextArt []byte `yaml:"NextArt"` // size 4
- ParentArt []byte `yaml:"ParentArt"` // size 4
- FirstChildArt []byte `yaml:"FirstChildArtArt"` // size 4
- DataFlav []byte `yaml:"DataFlav"` // "text/plain"
- Data string `yaml:"Data"`
-}
-
-func (art *NewsArtData) DataSize() []byte {
+ Title string `yaml:"Title"`
+ Poster string `yaml:"Poster"`
+ Date [8]byte `yaml:"Date,flow"`
+ PrevArt [4]byte `yaml:"PrevArt,flow"`
+ NextArt [4]byte `yaml:"NextArt,flow"`
+ ParentArt [4]byte `yaml:"ParentArt,flow"`
+ FirstChildArt [4]byte `yaml:"FirstChildArtArt,flow"`
+ DataFlav []byte `yaml:"-"` // MIME type string. Always "text/plain".
+ Data string `yaml:"Data"`
+}
+
+func (art *NewsArtData) DataSize() [2]byte {
dataLen := make([]byte, 2)
binary.BigEndian.PutUint16(dataLen, uint16(len(art.Data)))
- return dataLen
+ return [2]byte(dataLen)
}
type NewsArtListData struct {
- ID []byte `yaml:"ID"` // Size 4
- Name []byte `yaml:"Name"`
- Description []byte `yaml:"Description"` // not used?
- NewsArtList []byte // List of articles Optional (if article count > 0)
+ ID [4]byte `yaml:"Type"`
+ Name []byte `yaml:"Name"`
+ Description []byte `yaml:"Description"` // not used?
+ NewsArtList []byte // List of articles Optional (if article count > 0)
+ Count int
+
+ readOffset int // Internal offset to track read progress
}
-func (nald *NewsArtListData) Payload() []byte {
+func (nald *NewsArtListData) Read(p []byte) (int, error) {
count := make([]byte, 4)
- binary.BigEndian.PutUint32(count, uint32(len(nald.NewsArtList)))
-
- out := append(nald.ID, count...)
- out = append(out, []byte{uint8(len(nald.Name))}...)
- out = append(out, nald.Name...)
- out = append(out, []byte{uint8(len(nald.Description))}...)
- out = append(out, nald.Description...)
- out = append(out, nald.NewsArtList...)
+ binary.BigEndian.PutUint32(count, uint32(nald.Count))
+
+ buf := slices.Concat(
+ nald.ID[:],
+ count,
+ []byte{uint8(len(nald.Name))},
+ nald.Name,
+ []byte{uint8(len(nald.Description))},
+ nald.Description,
+ nald.NewsArtList,
+ )
+
+ if nald.readOffset >= len(buf) {
+ return 0, io.EOF // All bytes have been read
+ }
+ n := copy(p, buf[nald.readOffset:])
+ nald.readOffset += n
- return out
+ return n, nil
}
// NewsArtList is a summarized version of a NewArtData record for display in list view
type NewsArtList struct {
- ID []byte // Size 4
- TimeStamp []byte // Year (2 bytes), milliseconds (2 bytes) and seconds (4 bytes)
- ParentID []byte // Size 4
- Flags []byte // Size 4
- FlavorCount []byte // Size 2
+ ID [4]byte
+ TimeStamp [8]byte // Year (2 bytes), milliseconds (2 bytes) and seconds (4 bytes)
+ ParentID [4]byte
+ Flags [4]byte
+ FlavorCount [2]byte
// Title size 1
Title []byte // string
// Poster size 1
Poster []byte
FlavorList []NewsFlavorList
// Flavor list… Optional (if flavor count > 0)
- ArticleSize []byte // Size 2
-}
-
-type byID []NewsArtList
+ ArticleSize [2]byte // Size 2
-func (s byID) Len() int {
- return len(s)
-}
-func (s byID) Swap(i, j int) {
- s[i], s[j] = s[j], s[i]
-}
-func (s byID) Less(i, j int) bool {
- return binary.BigEndian.Uint32(s[i].ID) < binary.BigEndian.Uint32(s[j].ID)
+ readOffset int // Internal offset to track read progress
}
-func (nal *NewsArtList) Payload() []byte {
- out := append(nal.ID, nal.TimeStamp...)
- out = append(out, nal.ParentID...)
- out = append(out, nal.Flags...)
+var (
+ NewsFlavorLen = []byte{0x0a}
+ NewsFlavor = []byte("text/plain")
+)
- out = append(out, []byte{0, 1}...)
+func (nal *NewsArtList) Read(p []byte) (int, error) {
+ out := slices.Concat(
+ nal.ID[:],
+ nal.TimeStamp[:],
+ nal.ParentID[:],
+ nal.Flags[:],
+ []byte{0, 1}, // Flavor Count TODO: make this not hardcoded
+ []byte{uint8(len(nal.Title))},
+ nal.Title,
+ []byte{uint8(len(nal.Poster))},
+ nal.Poster,
+ NewsFlavorLen,
+ NewsFlavor,
+ nal.ArticleSize[:],
+ )
+
+ if nal.readOffset >= len(out) {
+ return 0, io.EOF // All bytes have been read
+ }
- out = append(out, []byte{uint8(len(nal.Title))}...)
- out = append(out, nal.Title...)
- out = append(out, []byte{uint8(len(nal.Poster))}...)
- out = append(out, nal.Poster...)
- out = append(out, []byte{0x0a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e}...) // TODO: wat?
- out = append(out, nal.ArticleSize...)
+ n := copy(p, out[nal.readOffset:])
+ nal.readOffset += n
- return out
+ return n, io.EOF
}
type NewsFlavorList struct {
// Article size 2
}
-func (newscat *NewsCategoryListData15) MarshalBinary() (data []byte, err error) {
+func (newscat *NewsCategoryListData15) Read(p []byte) (int, error) {
count := make([]byte, 2)
binary.BigEndian.PutUint16(count, uint16(len(newscat.Articles)+len(newscat.SubCats)))
- out := append(newscat.Type, count...)
-
- if bytes.Equal(newscat.Type, []byte{0, 3}) {
- // Generate a random GUID // TODO: does this need to be random?
- b := make([]byte, 16)
- _, err := rand.Read(b)
- if err != nil {
- return data, err
- }
-
- out = append(out, b...) // GUID
- out = append(out, []byte{0, 0, 0, 1}...) // Add SN (TODO: not sure what this is)
- out = append(out, []byte{0, 0, 0, 2}...) // Delete SN (TODO: not sure what this is)
+ out := slices.Concat(
+ newscat.Type[:],
+ count,
+ )
+ if newscat.Type == NewsCategory {
+ out = slices.Concat(out,
+ newscat.GUID[:],
+ newscat.AddSN[:],
+ newscat.DeleteSN[:],
+ )
}
+ out = slices.Concat(out,
+ newscat.nameLen(),
+ []byte(newscat.Name),
+ )
- out = append(out, newscat.nameLen()...)
- out = append(out, []byte(newscat.Name)...)
-
- return out, err
-}
-
-// ReadNewsCategoryListData parses a byte slice into a NewsCategoryListData15 struct
-// For use on the client side
-func ReadNewsCategoryListData(payload []byte) NewsCategoryListData15 {
- ncld := NewsCategoryListData15{
- Type: payload[0:2],
- Count: payload[2:4],
+ if newscat.readOffset >= len(out) {
+ return 0, io.EOF // All bytes have been read
}
- if bytes.Equal(ncld.Type, []byte{0, 3}) {
- ncld.GUID = payload[4:20]
- ncld.AddSN = payload[20:24]
- ncld.AddSN = payload[24:28]
- ncld.Name = string(payload[29:])
- } else {
- ncld.Name = string(payload[5:])
- }
+ n := copy(p, out)
+
+ newscat.readOffset = n
- return ncld
+ return n, nil
}
func (newscat *NewsCategoryListData15) nameLen() []byte {
return []byte{uint8(len(newscat.Name))}
}
-func ReadNewsPath(newsPath []byte) []string {
- if len(newsPath) == 0 {
- return []string{}
+// newsPathScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
+func newsPathScanner(data []byte, _ bool) (advance int, token []byte, err error) {
+ if len(data) < 3 {
+ return 0, nil, nil
}
- pathCount := binary.BigEndian.Uint16(newsPath[0:2])
-
- pathData := newsPath[2:]
- var paths []string
- for i := uint16(0); i < pathCount; i++ {
- pathLen := pathData[2]
- paths = append(paths, string(pathData[3:3+pathLen]))
-
- pathData = pathData[pathLen+3:]
- }
-
- return paths
-}
-
-func (s *Server) GetNewsCatByPath(paths []string) map[string]NewsCategoryListData15 {
- cats := s.ThreadedNews.Categories
- for _, path := range paths {
- cats = cats[path].SubCats
- }
- return cats
+ advance = 3 + int(data[2])
+ return advance, data[3:advance], nil
}