1 package model
2
3 import (
4 "bytes"
5 "encoding/base64"
6 "fmt"
7 "hash/adler32"
8 "html"
9 "html/template"
10 "image"
11 "image/draw"
12 "image/png"
13 "log"
14 "math/rand"
15 "net/url"
16 "os"
17 "path/filepath"
18 "strconv"
19 "strings"
20
21 "github.com/PuerkitoBio/goquery"
22
23 . "codeberg.org/tslocum/sriracha/util"
24 )
25
26 var (
27 adler = adler32.New()
28 adlerBuf = make([]byte, 8)
29 adlerSum []byte
30 )
31
32 type PostModerated int
33
34 const (
35 ModeratedHidden PostModerated = 0
36 ModeratedVisible PostModerated = 1
37 ModeratedApproved PostModerated = 2
38 )
39
40 type Post struct {
41 ID int
42 Board *Board
43 Parent int
44 Timestamp int64
45 Bumped int64
46 IP string
47 Name string
48 Tripcode string
49 Email string
50 NameBlock string
51 Subject string
52 Message string
53 Password string
54 File string
55 FileMIME string
56 FileHash string
57 FileOriginal string
58 FileSize int64
59 FileWidth int
60 FileHeight int
61 Thumb string
62 ThumbWidth int
63 ThumbHeight int
64 Moderated PostModerated
65 Stickied bool
66 Locked bool
67
68
69 Replies int
70 }
71
72 func (p *Post) Copy() *Post {
73 pp := &Post{}
74 *pp = *p
75 pp.Board = p.Board
76 return pp
77 }
78
79 func (p *Post) ResetAttachment() {
80 p.File = ""
81 p.FileMIME = ""
82 p.FileHash = ""
83 p.FileOriginal = ""
84 p.FileSize = 0
85 p.FileWidth = 0
86 p.FileHeight = 0
87 p.Thumb = ""
88 p.ThumbWidth = 0
89 p.ThumbHeight = 0
90 }
91
92 func (p *Post) AddMediaOverlay(img image.Image) image.Image {
93 mediaBuf, err := os.ReadFile("static/img/media.png")
94 if err != nil {
95 log.Fatal(err)
96 }
97
98 overlayImg, err := png.Decode(bytes.NewReader(mediaBuf))
99 if err != nil {
100 log.Fatal(err)
101 }
102
103 target := image.NewRGBA(img.Bounds())
104 draw.Draw(target, img.Bounds(), img, image.Point{}, draw.Src)
105
106 overlayPosition := image.Point{
107 X: img.Bounds().Dx()/2 - overlayImg.Bounds().Dx()/2,
108 Y: img.Bounds().Dy()/2 - overlayImg.Bounds().Dy()/2,
109 }
110 draw.Draw(target, overlayImg.Bounds().Add(overlayPosition), overlayImg, image.Point{}, draw.Over)
111 return target
112 }
113
114 func (p *Post) SetNameBlock(defaultName string, capcode string, identifiers bool) {
115 var out strings.Builder
116
117 emailLink := p.Email != "" && strings.ToLower(p.Email) != "noko"
118
119 if emailLink {
120 out.WriteString(`<a href="mailto:` + html.EscapeString(p.Email) + `">`)
121 }
122 if p.Name != "" || p.Tripcode == "" {
123 name := p.Name
124 if name == "" {
125 if strings.ContainsRune(defaultName, '|') {
126 split := strings.Split(defaultName, "|")
127 name = split[rand.Intn(len(split))]
128 } else {
129 name = defaultName
130 }
131 }
132 out.WriteString(`<span class="postername">`)
133 out.WriteString(html.EscapeString(name))
134 out.WriteString(`</span>`)
135 }
136 if p.Tripcode != "" {
137 out.WriteString(`<span class="postertrip">!`)
138 out.WriteString(html.EscapeString(p.Tripcode))
139 out.WriteString(`</span>`)
140 }
141 if emailLink {
142 out.WriteString(`</a>`)
143 }
144
145 if capcode != "" {
146 spanColor := "red"
147 if capcode == "Admin" {
148 spanColor = "purple"
149 }
150 out.WriteString(` <span style="color: ` + spanColor + `;">## ` + capcode + `</span>`)
151 }
152
153 identifier := p.Identifier(identifiers, false)
154 if identifier != "" {
155 out.WriteString(" " + identifier)
156 }
157
158 out.WriteString(" " + string(p.TimestampLabel()))
159
160 p.NameBlock = out.String()
161 }
162
163 func (p *Post) Thread() int {
164 if p.Parent == 0 {
165 return p.ID
166 }
167 return p.Parent
168 }
169
170 func (p *Post) FileSizeLabel() string {
171 return FormatFileSize(p.FileSize)
172 }
173
174 func (p *Post) TimestampLabel() template.HTML {
175 return FormatTimestamp(p.Timestamp)
176 }
177
178 func (p *Post) BumpLabel() template.HTML {
179 if p.Bumped != 0 {
180 return FormatTimestamp(p.Bumped)
181 }
182 return FormatTimestamp(p.Timestamp)
183 }
184
185 func (p *Post) IsOekaki() bool {
186 return strings.HasSuffix(p.File, ".tgkr")
187 }
188
189 func (p *Post) IsSWF() bool {
190 return strings.HasSuffix(p.File, ".swf")
191 }
192
193 func (p *Post) IsEmbed() bool {
194 return len(p.FileHash) > 2 && p.FileHash[1] == ' ' && p.FileHash[0] == 'e'
195 }
196
197 func (p *Post) EmbedInfo() []string {
198 if !p.IsEmbed() {
199 return nil
200 }
201 split := strings.SplitN(p.FileHash, " ", 3)
202 if len(split) != 3 {
203 return nil
204 }
205 return split
206 }
207
208 func (p *Post) MessageTruncated(lines int, account *Account) template.HTML {
209 var showOmitted bool
210 if lines == 0 {
211 lines = p.Board.Truncate
212 showOmitted = true
213 }
214 if lines == 0 {
215 return template.HTML(p.Message)
216 }
217
218 split := bytes.Split([]byte(p.Message), []byte("\n"))
219 if len(split) <= lines {
220 return template.HTML(p.Message)
221 }
222
223 blankMessage := template.HTML("…")
224 if showOmitted {
225 blankMessage = template.HTML(`<span class="omittedposts">` + Get(p.Board, account, "Post truncated. Click Reply to view.") + `</span><br>`)
226 }
227
228 buf := bytes.Join(split[:lines], []byte("\n"))
229 if bytes.Contains(buf, []byte(`<div class="codeblock">`)) {
230 return blankMessage
231 }
232
233 doc, err := goquery.NewDocumentFromReader(bytes.NewReader(buf))
234 if err != nil {
235 log.Fatal(err)
236 }
237 body := doc.Find("body")
238 if body == nil || body.Length() == 0 {
239 return blankMessage
240 }
241 first := body.First()
242 if first == nil || first.Length() == 0 || first.Text() == "" {
243 return blankMessage
244 }
245
246
247 truncated, err := first.Html()
248 if err != nil {
249 log.Fatal(err)
250 }
251
252 truncated = strings.ReplaceAll(truncated, "<br/>", "<br>")
253
254 if showOmitted {
255 if !strings.HasSuffix(truncated, "<br>\n<br>") {
256 truncated += "<br>"
257 }
258 truncated += string(blankMessage)
259 }
260 return template.HTML(truncated)
261 }
262
263 func (p *Post) ExpandHTML() string {
264 if p.File == "" {
265 return ""
266 } else if p.IsEmbed() {
267 return p.File
268 }
269 srcPath := fmt.Sprintf("%ssrc/%s", p.Board.Path(), p.File)
270
271 isAudio := strings.HasPrefix(p.FileMIME, "audio/") && p.FileMIME != "audio/midi"
272 isVideo := strings.HasPrefix(p.FileMIME, "video/") && p.FileMIME != "video/mpeg" && p.FileMIME != "video/ogg" && p.FileMIME != "video/x-matroska" && p.FileMIME != "video/x-msvideo"
273 if isAudio || isVideo {
274 element := "audio"
275 loop := ""
276 if isVideo {
277 element = "video"
278 loop = " loop"
279 }
280 const expandFormat = `<%s width="%d" height="%d" style="pointer-events: inherit;" controls autoplay%s><source src="%s"></source></%s>`
281 return fmt.Sprintf(expandFormat, element, p.FileWidth, p.FileHeight, loop, srcPath, element)
282 }
283
284 isImage := strings.HasPrefix(p.FileMIME, "image/")
285 if !isImage {
286 return ""
287 }
288 const expandFormat = `<a href="%s" onclick="return expandFile(event, '%d');"><img src="%s" width="%d" height="%d" style="pointer-events: inherit;"></a>`
289 return fmt.Sprintf(expandFormat, srcPath, p.ID, srcPath, p.FileWidth, p.FileHeight)
290 }
291
292 func (p *Post) Identifier(identifiers bool, force bool) string {
293 if p.IP == "" || !identifiers || (p.Board.Identifiers == IdentifiersDisable && !force) {
294 return ""
295 }
296 adler.Reset()
297 if p.Board.Identifiers == IdentifiersBoard {
298 adler.Write([]byte(strconv.Itoa(p.Board.ID)))
299 }
300 adler.Write([]byte(p.IP))
301
302 adlerSum = adler.Sum(adlerSum[:0])
303
304 base64.RawURLEncoding.Encode(adlerBuf, adlerSum)
305 return string(adlerBuf[:5])
306 }
307
308 func (p *Post) Backlinks(posts []*Post) template.HTML {
309 if !p.Board.Backlinks {
310 return ""
311 }
312 var out []byte
313 BACKLINKS:
314 for _, reply := range posts {
315 matches := RefLinkPattern.FindAll([]byte(reply.Message), -1)
316 for _, match := range matches {
317 id, err := strconv.Atoi(string(match)[8:])
318 if err != nil || id != p.ID {
319 continue
320 } else if out != nil {
321 out = append(out, []byte("<wbr>")...)
322 }
323 out = append(out, FormatRefLink(p.Board.Path(), p.Thread(), reply.ID)...)
324 continue BACKLINKS
325 }
326 }
327 if out == nil {
328 return ""
329 }
330 return template.HTML(`<span class="backlink">` + string(out) + `</span>`)
331 }
332
333 func FormatRefLink(boardPath string, threadID int, postID int) []byte {
334 return fmt.Appendf(nil, `<a href="%sres/%d.html#%d">>>%d</a>`, boardPath, threadID, postID, postID)
335 }
336
337 func (p *Post) RefLink() template.HTML {
338 return template.HTML(FormatRefLink(p.Board.Path(), p.Thread(), p.ID))
339 }
340
341 func (p *Post) URL(siteHome string) string {
342 var host string
343 var path string
344 if siteHome != "" {
345 u, err := url.Parse(siteHome)
346 if err == nil {
347 if u.Host != "" {
348 host = "https://" + u.Host
349 }
350 path = u.Path
351 }
352 }
353
354 path = filepath.Join(path, p.Board.Path())
355 if !strings.HasSuffix(path, "/") {
356 path += "/"
357 }
358
359 return fmt.Sprintf(`%s%sres/%d.html#%d`, host, path, p.Thread(), p.ID)
360 }
361
View as plain text