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