]> git.r.bdr.sh - rbdr/mobius/blame - hotline/news.go
Update documentation
[rbdr/mobius] / hotline / news.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
d9bc63a1 4 "cmp"
6988a057 5 "encoding/binary"
9cf66aea
JH
6 "io"
7 "slices"
6988a057
JH
8)
9
d9bc63a1
JH
10var (
11 NewsBundle = [2]byte{0, 2}
12 NewsCategory = [2]byte{0, 3}
13)
2d92d26e 14
d9bc63a1
JH
15type ThreadedNewsMgr interface {
16 ListArticles(newsPath []string) NewsArtListData
17 GetArticle(newsPath []string, articleID uint32) *NewsArtData
18 DeleteArticle(newsPath []string, articleID uint32, recursive bool) error
19 PostArticle(newsPath []string, parentArticleID uint32, article NewsArtData) error
20 CreateGrouping(newsPath []string, name string, t [2]byte) error
21 GetCategories(paths []string) []NewsCategoryListData15
22 NewsItem(newsPath []string) NewsCategoryListData15
23 DeleteNewsItem(newsPath []string) error
24}
2d92d26e 25
d9bc63a1 26// ThreadedNews contains the top level of threaded news categories, bundles, and articles.
6988a057
JH
27type ThreadedNews struct {
28 Categories map[string]NewsCategoryListData15 `yaml:"Categories"`
29}
30
31type NewsCategoryListData15 struct {
0ed51327
JH
32 Type [2]byte `yaml:"Type,flow"` // Bundle (2) or category (3)
33 Name string `yaml:"Name"`
6988a057
JH
34 Articles map[uint32]*NewsArtData `yaml:"Articles"` // Optional, if Type is Category
35 SubCats map[string]NewsCategoryListData15 `yaml:"SubCats"`
0ed51327
JH
36 GUID [16]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
37 AddSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
38 DeleteSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused.
a2ef262a
JH
39
40 readOffset int // Internal offset to track read progress
6988a057
JH
41}
42
43func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
44 var newsArts []NewsArtList
45 var newsArtsPayload []byte
46
47 for i, art := range newscat.Articles {
9cf66aea
JH
48 id := make([]byte, 4)
49 binary.BigEndian.PutUint32(id, i)
6988a057 50
d9bc63a1 51 newsArts = append(newsArts, NewsArtList{
0ed51327
JH
52 ID: [4]byte(id),
53 TimeStamp: art.Date,
54 ParentID: art.ParentArt,
6988a057
JH
55 Title: []byte(art.Title),
56 Poster: []byte(art.Poster),
57 ArticleSize: art.DataSize(),
d9bc63a1 58 })
6988a057
JH
59 }
60
d9bc63a1
JH
61 // Sort the articles by ID. This is important for displaying the message threading correctly on the client side.
62 slices.SortFunc(newsArts, func(a, b NewsArtList) int {
63 return cmp.Compare(
64 binary.BigEndian.Uint32(a.ID[:]),
65 binary.BigEndian.Uint32(b.ID[:]),
66 )
67 })
6988a057
JH
68
69 for _, v := range newsArts {
95159e55
JH
70 b, err := io.ReadAll(&v)
71 if err != nil {
72 // TODO
0ed51327 73 panic(err)
95159e55
JH
74 }
75 newsArtsPayload = append(newsArtsPayload, b...)
6988a057
JH
76 }
77
0ed51327 78 return NewsArtListData{
33265393 79 Count: len(newsArts),
6988a057
JH
80 Name: []byte{},
81 Description: []byte{},
82 NewsArtList: newsArtsPayload,
83 }
6988a057
JH
84}
85
d9bc63a1 86// NewsArtData represents an individual news article.
6988a057 87type NewsArtData struct {
95159e55
JH
88 Title string `yaml:"Title"`
89 Poster string `yaml:"Poster"`
0ed51327
JH
90 Date [8]byte `yaml:"Date,flow"`
91 PrevArt [4]byte `yaml:"PrevArt,flow"`
92 NextArt [4]byte `yaml:"NextArt,flow"`
93 ParentArt [4]byte `yaml:"ParentArt,flow"`
94 FirstChildArt [4]byte `yaml:"FirstChildArtArt,flow"`
d9bc63a1 95 DataFlav []byte `yaml:"-"` // MIME type string. Always "text/plain".
95159e55 96 Data string `yaml:"Data"`
6988a057
JH
97}
98
d9bc63a1 99func (art *NewsArtData) DataSize() [2]byte {
6988a057
JH
100 dataLen := make([]byte, 2)
101 binary.BigEndian.PutUint16(dataLen, uint16(len(art.Data)))
102
d9bc63a1 103 return [2]byte(dataLen)
6988a057
JH
104}
105
106type NewsArtListData struct {
d9bc63a1 107 ID [4]byte `yaml:"Type"`
9cf66aea
JH
108 Name []byte `yaml:"Name"`
109 Description []byte `yaml:"Description"` // not used?
110 NewsArtList []byte // List of articles Optional (if article count > 0)
33265393 111 Count int
0ed51327
JH
112
113 readOffset int // Internal offset to track read progress
6988a057
JH
114}
115
9cf66aea 116func (nald *NewsArtListData) Read(p []byte) (int, error) {
6988a057 117 count := make([]byte, 4)
33265393 118 binary.BigEndian.PutUint32(count, uint32(nald.Count))
6988a057 119
0ed51327
JH
120 buf := slices.Concat(
121 nald.ID[:],
122 count,
123 []byte{uint8(len(nald.Name))},
124 nald.Name,
125 []byte{uint8(len(nald.Description))},
126 nald.Description,
127 nald.NewsArtList,
128 )
129
130 if nald.readOffset >= len(buf) {
131 return 0, io.EOF // All bytes have been read
132 }
133 n := copy(p, buf[nald.readOffset:])
134 nald.readOffset += n
135
136 return n, nil
6988a057
JH
137}
138
72dd37f1 139// NewsArtList is a summarized version of a NewArtData record for display in list view
6988a057 140type NewsArtList struct {
0ed51327
JH
141 ID [4]byte
142 TimeStamp [8]byte // Year (2 bytes), milliseconds (2 bytes) and seconds (4 bytes)
143 ParentID [4]byte
144 Flags [4]byte
145 FlavorCount [2]byte
6988a057
JH
146 // Title size 1
147 Title []byte // string
148 // Poster size 1
149 // Poster Poster string
150 Poster []byte
151 FlavorList []NewsFlavorList
152 // Flavor list… Optional (if flavor count > 0)
d9bc63a1 153 ArticleSize [2]byte // Size 2
95159e55
JH
154
155 readOffset int // Internal offset to track read progress
6988a057
JH
156}
157
0ed51327
JH
158var (
159 NewsFlavorLen = []byte{0x0a}
160 NewsFlavor = []byte("text/plain")
161)
162
95159e55
JH
163func (nal *NewsArtList) Read(p []byte) (int, error) {
164 out := slices.Concat(
0ed51327
JH
165 nal.ID[:],
166 nal.TimeStamp[:],
167 nal.ParentID[:],
168 nal.Flags[:],
a2ef262a 169 []byte{0, 1}, // Flavor Count TODO: make this not hardcoded
95159e55
JH
170 []byte{uint8(len(nal.Title))},
171 nal.Title,
172 []byte{uint8(len(nal.Poster))},
173 nal.Poster,
0ed51327
JH
174 NewsFlavorLen,
175 NewsFlavor,
d9bc63a1 176 nal.ArticleSize[:],
95159e55
JH
177 )
178
179 if nal.readOffset >= len(out) {
180 return 0, io.EOF // All bytes have been read
181 }
6988a057 182
95159e55
JH
183 n := copy(p, out[nal.readOffset:])
184 nal.readOffset += n
6988a057 185
95159e55 186 return n, io.EOF
6988a057
JH
187}
188
189type NewsFlavorList struct {
190 // Flavor size 1
191 // Flavor text size MIME type string
192 // Article size 2
193}
194
a2ef262a 195func (newscat *NewsCategoryListData15) Read(p []byte) (int, error) {
6988a057
JH
196 count := make([]byte, 2)
197 binary.BigEndian.PutUint16(count, uint16(len(newscat.Articles)+len(newscat.SubCats)))
198
a2ef262a
JH
199 out := slices.Concat(
200 newscat.Type[:],
201 count,
202 )
d9bc63a1
JH
203 if newscat.Type == NewsCategory {
204 out = slices.Concat(out,
205 newscat.GUID[:],
206 newscat.AddSN[:],
207 newscat.DeleteSN[:],
208 )
6988a057 209 }
d9bc63a1
JH
210 out = slices.Concat(out,
211 newscat.nameLen(),
212 []byte(newscat.Name),
213 )
6988a057 214
a2ef262a
JH
215 if newscat.readOffset >= len(out) {
216 return 0, io.EOF // All bytes have been read
217 }
218
219 n := copy(p, out)
d9bc63a1 220
a2ef262a
JH
221 newscat.readOffset = n
222
223 return n, nil
6988a057
JH
224}
225
6988a057
JH
226func (newscat *NewsCategoryListData15) nameLen() []byte {
227 return []byte{uint8(len(newscat.Name))}
228}
229
d9bc63a1
JH
230// newsPathScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
231func newsPathScanner(data []byte, _ bool) (advance int, token []byte, err error) {
232 if len(data) < 3 {
233 return 0, nil, nil
6988a057 234 }
6988a057 235
d9bc63a1
JH
236 advance = 3 + int(data[2])
237 return advance, data[3:advance], nil
6988a057 238}