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) IsOekaki() bool {
179 return strings.HasSuffix(p.File, ".tgkr")
180 }
181
182 func (p *Post) IsSWF() bool {
183 return strings.HasSuffix(p.File, ".swf")
184 }
185
186 func (p *Post) IsEmbed() bool {
187 return len(p.FileHash) > 2 && p.FileHash[1] == ' ' && p.FileHash[0] == 'e'
188 }
189
190 func (p *Post) EmbedInfo() []string {
191 if !p.IsEmbed() {
192 return nil
193 }
194 split := strings.SplitN(p.FileHash, " ", 3)
195 if len(split) != 3 {
196 return nil
197 }
198 return split
199 }
200
201 func (p *Post) MessageTruncated(lines int, account *Account) template.HTML {
202 var showOmitted bool
203 if lines == 0 {
204 lines = p.Board.Truncate
205 showOmitted = true
206 }
207 if lines == 0 {
208 return template.HTML(p.Message)
209 }
210
211 count := strings.Count(p.Message, "<br>")
212 if count < lines {
213 return template.HTML(p.Message)
214 }
215
216 msg := []byte(p.Message)
217 out := &bytes.Buffer{}
218 var start int
219 for i := 0; i < lines; i++ {
220 index := bytes.Index(msg[start:], []byte("<br>"))
221
222 end := len(msg) - start
223 if index != -1 {
224 end = index
225 }
226
227 if i > 0 {
228 out.Write([]byte("<br>"))
229 }
230 out.Write(msg[start : start+end])
231
232 start += end + 4
233
234 if start >= len(msg) {
235 break
236 }
237 }
238
239 doc, err := goquery.NewDocumentFromReader(out)
240 if err != nil {
241 log.Fatal(err)
242 }
243 truncated, err := doc.Find("body").First().Html()
244 if err != nil {
245 log.Fatal(err)
246 }
247
248 if showOmitted {
249 truncated += `<br><span class="omittedposts">` + Get(p.Board, account, "Post truncated. Click Reply to view.") + `</span><br>`
250 }
251 return template.HTML(truncated)
252 }
253
254 func (p *Post) ExpandHTML() template.HTML {
255 if p.File == "" {
256 return ""
257 } else if p.IsEmbed() {
258 return template.HTML(url.PathEscape(p.File))
259 }
260 srcPath := fmt.Sprintf("%ssrc/%s", p.Board.Path(), p.File)
261
262 isAudio := strings.HasPrefix(p.FileMIME, "audio/")
263 isVideo := strings.HasPrefix(p.FileMIME, "video/")
264 if isAudio || isVideo {
265 element := "audio"
266 loop := ""
267 if isVideo {
268 element = "video"
269 loop = " loop"
270 }
271 const expandFormat = `<%s width="%d" height="%d" class="thumb" style="pointer-events: inherit;" controls autoplay%s><source src="%s"></source></%s>`
272 return template.HTML(url.PathEscape(fmt.Sprintf(expandFormat, element, p.FileWidth, p.FileHeight, loop, srcPath, element)))
273 }
274
275 isImage := strings.HasPrefix(p.FileMIME, "image/")
276 if !isImage {
277 return ""
278 }
279 const expandFormat = `<a href="%s" onclick="return expandFile(event, '%d');"><img src="%s" width="%d" height="%d" class="thumb" style="pointer-events: inherit;"></a>`
280 return template.HTML(url.PathEscape(fmt.Sprintf(expandFormat, srcPath, p.ID, srcPath, p.FileWidth, p.FileHeight)))
281 }
282
283 func (p *Post) Identifier(identifiers bool, force bool) string {
284 if p.IP == "" || !identifiers || (p.Board.Identifiers == IdentifiersDisable && !force) {
285 return ""
286 }
287 adler.Reset()
288 if p.Board.Identifiers == IdentifiersBoard {
289 adler.Write([]byte(strconv.Itoa(p.Board.ID)))
290 }
291 adler.Write([]byte(p.IP))
292
293 adlerSum = adler.Sum(adlerSum[:0])
294
295 base64.RawURLEncoding.Encode(adlerBuf, adlerSum)
296 return string(adlerBuf[:5])
297 }
298
299 func (p *Post) Backlinks(posts []*Post) template.HTML {
300 if !p.Board.Backlinks {
301 return ""
302 }
303 var out []byte
304 BACKLINKS:
305 for _, reply := range posts {
306 matches := RefLinkPattern.FindAll([]byte(reply.Message), -1)
307 for _, match := range matches {
308 id, err := strconv.Atoi(string(match)[8:])
309 if err != nil || id != p.ID {
310 continue
311 } else if out != nil {
312 out = append(out, []byte("<wbr>")...)
313 }
314 out = append(out, FormatRefLink(p.Board.Path(), p.Thread(), reply.ID)...)
315 continue BACKLINKS
316 }
317 }
318 return template.HTML(string(out))
319 }
320
321 func FormatRefLink(boardPath string, threadID int, postID int) []byte {
322 return fmt.Appendf(nil, `<a href="%sres/%d.html#%d">>>%d</a>`, boardPath, threadID, postID, postID)
323 }
324
325 func (p *Post) RefLink() template.HTML {
326 return template.HTML(FormatRefLink(p.Board.Path(), p.Thread(), p.ID))
327 }
328
329 func (p *Post) URL(siteHome string) string {
330 var host string
331 var path string
332 if siteHome != "" {
333 u, err := url.Parse(siteHome)
334 if err == nil {
335 if u.Host != "" {
336 host = "https://" + u.Host
337 }
338 path = u.Path
339 }
340 }
341
342 path = filepath.Join(path, p.Board.Path())
343 if !strings.HasSuffix(path, "/") {
344 path += "/"
345 }
346
347 return fmt.Sprintf(`%s%sres/%d.html#%d`, host, path, p.Thread(), p.ID)
348 }
349
View as plain text