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