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