6 "github.com/stretchr/testify/mock"
12 NewsBundle = [2]byte{0, 2}
13 NewsCategory = [2]byte{0, 3}
16 type 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
27 // ThreadedNews contains the top level of threaded news categories, bundles, and articles.
28 type ThreadedNews struct {
29 Categories map[string]NewsCategoryListData15 `yaml:"Categories"`
32 type NewsCategoryListData15 struct {
33 Type [2]byte `yaml:"Type,flow"` // Bundle (2) or category (3)
34 Name string `yaml:"Name"`
35 Articles map[uint32]*NewsArtData `yaml:"Articles"` // Optional, if Type is Category
36 SubCats map[string]NewsCategoryListData15 `yaml:"SubCats"`
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.
41 readOffset int // Internal offset to track read progress
44 func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
45 var newsArts []NewsArtList
46 var newsArtsPayload []byte
48 for i, art := range newscat.Articles {
50 binary.BigEndian.PutUint32(id, i)
52 newsArts = append(newsArts, NewsArtList{
55 ParentID: art.ParentArt,
56 Title: []byte(art.Title),
57 Poster: []byte(art.Poster),
58 ArticleSize: art.DataSize(),
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 {
65 binary.BigEndian.Uint32(a.ID[:]),
66 binary.BigEndian.Uint32(b.ID[:]),
70 for _, v := range newsArts {
71 b, err := io.ReadAll(&v)
76 newsArtsPayload = append(newsArtsPayload, b...)
79 return NewsArtListData{
82 Description: []byte{},
83 NewsArtList: newsArtsPayload,
87 // NewsArtData represents an individual news article.
88 type NewsArtData struct {
89 Title string `yaml:"Title"`
90 Poster string `yaml:"Poster"`
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"`
96 DataFlav []byte `yaml:"-"` // MIME type string. Always "text/plain".
97 Data string `yaml:"Data"`
100 func (art *NewsArtData) DataSize() [2]byte {
101 dataLen := make([]byte, 2)
102 binary.BigEndian.PutUint16(dataLen, uint16(len(art.Data)))
104 return [2]byte(dataLen)
107 type NewsArtListData struct {
108 ID [4]byte `yaml:"Type"`
109 Name []byte `yaml:"Name"`
110 Description []byte `yaml:"Description"` // not used?
111 NewsArtList []byte // List of articles Optional (if article count > 0)
114 readOffset int // Internal offset to track read progress
117 func (nald *NewsArtListData) Read(p []byte) (int, error) {
118 count := make([]byte, 4)
119 binary.BigEndian.PutUint32(count, uint32(nald.Count))
121 buf := slices.Concat(
124 []byte{uint8(len(nald.Name))},
126 []byte{uint8(len(nald.Description))},
131 if nald.readOffset >= len(buf) {
132 return 0, io.EOF // All bytes have been read
134 n := copy(p, buf[nald.readOffset:])
140 // NewsArtList is a summarized version of a NewArtData record for display in list view
141 type NewsArtList struct {
143 TimeStamp [8]byte // Year (2 bytes), milliseconds (2 bytes) and seconds (4 bytes)
148 Title []byte // string
150 // Poster Poster string
152 FlavorList []NewsFlavorList
153 // Flavor list… Optional (if flavor count > 0)
154 ArticleSize [2]byte // Size 2
156 readOffset int // Internal offset to track read progress
160 NewsFlavorLen = []byte{0x0a}
161 NewsFlavor = []byte("text/plain")
164 func (nal *NewsArtList) Read(p []byte) (int, error) {
165 out := slices.Concat(
170 []byte{0, 1}, // Flavor Count TODO: make this not hardcoded
171 []byte{uint8(len(nal.Title))},
173 []byte{uint8(len(nal.Poster))},
180 if nal.readOffset >= len(out) {
181 return 0, io.EOF // All bytes have been read
184 n := copy(p, out[nal.readOffset:])
190 type NewsFlavorList struct {
192 // Flavor text size MIME type string
196 func (newscat *NewsCategoryListData15) Read(p []byte) (int, error) {
197 count := make([]byte, 2)
198 binary.BigEndian.PutUint16(count, uint16(len(newscat.Articles)+len(newscat.SubCats)))
200 out := slices.Concat(
204 if newscat.Type == NewsCategory {
205 out = slices.Concat(out,
211 out = slices.Concat(out,
213 []byte(newscat.Name),
216 if newscat.readOffset >= len(out) {
217 return 0, io.EOF // All bytes have been read
222 newscat.readOffset = n
227 func (newscat *NewsCategoryListData15) nameLen() []byte {
228 return []byte{uint8(len(newscat.Name))}
231 // newsPathScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
232 func newsPathScanner(data []byte, _ bool) (advance int, token []byte, err error) {
237 advance = 3 + int(data[2])
238 return advance, data[3:advance], nil
241 type MockThreadNewsMgr struct {
245 func (m *MockThreadNewsMgr) ListArticles(newsPath []string) NewsArtListData {
246 args := m.Called(newsPath)
248 return args.Get(0).(NewsArtListData)
251 func (m *MockThreadNewsMgr) GetArticle(newsPath []string, articleID uint32) *NewsArtData {
252 args := m.Called(newsPath, articleID)
254 return args.Get(0).(*NewsArtData)
256 func (m *MockThreadNewsMgr) DeleteArticle(newsPath []string, articleID uint32, recursive bool) error {
257 args := m.Called(newsPath, articleID, recursive)
262 func (m *MockThreadNewsMgr) PostArticle(newsPath []string, parentArticleID uint32, article NewsArtData) error {
263 args := m.Called(newsPath, parentArticleID, article)
267 func (m *MockThreadNewsMgr) CreateGrouping(newsPath []string, name string, itemType [2]byte) error {
268 args := m.Called(newsPath, name, itemType)
273 func (m *MockThreadNewsMgr) GetCategories(paths []string) []NewsCategoryListData15 {
274 args := m.Called(paths)
276 return args.Get(0).([]NewsCategoryListData15)
279 func (m *MockThreadNewsMgr) NewsItem(newsPath []string) NewsCategoryListData15 {
280 args := m.Called(newsPath)
282 return args.Get(0).(NewsCategoryListData15)
285 func (m *MockThreadNewsMgr) DeleteNewsItem(newsPath []string) error {
286 args := m.Called(newsPath)