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