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) AddMediaOverlay(img image.Image) image.Image {
80 mediaBuf, err := os.ReadFile("static/img/media.png")
81 if err != nil {
82 log.Fatal(err)
83 }
84
85 overlayImg, err := png.Decode(bytes.NewReader(mediaBuf))
86 if err != nil {
87 log.Fatal(err)
88 }
89
90 target := image.NewRGBA(img.Bounds())
91 draw.Draw(target, img.Bounds(), img, image.Point{}, draw.Src)
92
93 overlayPosition := image.Point{
94 X: img.Bounds().Dx()/2 - overlayImg.Bounds().Dx()/2,
95 Y: img.Bounds().Dy()/2 - overlayImg.Bounds().Dy()/2,
96 }
97 draw.Draw(target, overlayImg.Bounds().Add(overlayPosition), overlayImg, image.Point{}, draw.Over)
98 return target
99 }
100
101 func (p *Post) SetNameBlock(defaultName string, capcode string, identifiers bool) {
102 var out strings.Builder
103
104 emailLink := p.Email != "" && strings.ToLower(p.Email) != "noko"
105
106 if emailLink {
107 out.WriteString(`<a href="mailto:` + html.EscapeString(p.Email) + `">`)
108 }
109 if p.Name != "" || p.Tripcode == "" {
110 name := p.Name
111 if name == "" {
112 if strings.ContainsRune(defaultName, '|') {
113 split := strings.Split(defaultName, "|")
114 name = split[rand.Intn(len(split))]
115 } else {
116 name = defaultName
117 }
118 }
119 out.WriteString(`<span class="postername">`)
120 out.WriteString(html.EscapeString(name))
121 out.WriteString(`</span>`)
122 }
123 if p.Tripcode != "" {
124 out.WriteString(`<span class="postertrip">!`)
125 out.WriteString(html.EscapeString(p.Tripcode))
126 out.WriteString(`</span>`)
127 }
128 if emailLink {
129 out.WriteString(`</a>`)
130 }
131
132 if capcode != "" {
133 spanColor := "red"
134 if capcode == "Admin" {
135 spanColor = "purple"
136 }
137 out.WriteString(` <span style="color: ` + spanColor + `;">## ` + capcode + `</span>`)
138 }
139
140 identifier := p.Identifier(identifiers, false)
141 if identifier != "" {
142 out.WriteString(" " + identifier)
143 }
144
145 out.WriteString(" " + string(p.TimestampLabel()))
146
147 p.NameBlock = out.String()
148 }
149
150 func (p *Post) Thread() int {
151 if p.Parent == 0 {
152 return p.ID
153 }
154 return p.Parent
155 }
156
157 func (p *Post) FileSizeLabel() string {
158 return FormatFileSize(p.FileSize)
159 }
160
161 func (p *Post) TimestampLabel() template.HTML {
162 return FormatTimestamp(p.Timestamp)
163 }
164
165 func (p *Post) IsOekaki() bool {
166 return strings.HasSuffix(p.File, ".tgkr")
167 }
168
169 func (p *Post) IsSWF() bool {
170 return strings.HasSuffix(p.File, ".swf")
171 }
172
173 func (p *Post) IsEmbed() bool {
174 return len(p.FileHash) > 2 && p.FileHash[1] == ' ' && p.FileHash[0] == 'e'
175 }
176
177 func (p *Post) EmbedInfo() []string {
178 if !p.IsEmbed() {
179 return nil
180 }
181 split := strings.SplitN(p.FileHash, " ", 3)
182 if len(split) != 3 {
183 return nil
184 }
185 return split
186 }
187
188 func (p *Post) MessageTruncated(lines int, account *Account) template.HTML {
189 var showOmitted bool
190 if lines == 0 {
191 lines = p.Board.Truncate
192 showOmitted = true
193 }
194 if lines == 0 {
195 return template.HTML(p.Message)
196 }
197
198 count := strings.Count(p.Message, "<br>")
199 if count < lines {
200 return template.HTML(p.Message)
201 }
202
203 msg := []byte(p.Message)
204 out := &bytes.Buffer{}
205 var start int
206 for i := 0; i < lines; i++ {
207 index := bytes.Index(msg[start:], []byte("<br>"))
208
209 end := len(msg) - start
210 if index != -1 {
211 end = index
212 }
213
214 if i > 0 {
215 out.Write([]byte("<br>"))
216 }
217 out.Write(msg[start : start+end])
218
219 start += end + 4
220
221 if start >= len(msg) {
222 break
223 }
224 }
225
226 doc, err := goquery.NewDocumentFromReader(out)
227 if err != nil {
228 log.Fatal(err)
229 }
230 truncated, err := doc.Find("body").First().Html()
231 if err != nil {
232 log.Fatal(err)
233 }
234
235 if showOmitted {
236 truncated += `<br><span class="omittedposts">` + Get(p.Board, account, "Post truncated. Click Reply to view.") + `</span><br>`
237 }
238 return template.HTML(truncated)
239 }
240
241 func (p *Post) ExpandHTML() template.HTML {
242 if p.File == "" {
243 return ""
244 } else if p.IsEmbed() {
245 return template.HTML(url.PathEscape(p.File))
246 }
247 srcPath := fmt.Sprintf("%ssrc/%s", p.Board.Path(), p.File)
248
249 isAudio := strings.HasPrefix(p.FileMIME, "audio/")
250 isVideo := strings.HasPrefix(p.FileMIME, "video/")
251 if isAudio || isVideo {
252 element := "audio"
253 loop := ""
254 if isVideo {
255 element = "video"
256 loop = " loop"
257 }
258 const expandFormat = `<%s width="%d" height="%d" class="thumb" style="pointer-events: inherit;" controls autoplay%s><source src="%s"></source></%s>`
259 return template.HTML(url.PathEscape(fmt.Sprintf(expandFormat, element, p.FileWidth, p.FileHeight, loop, srcPath, element)))
260 }
261
262 isImage := strings.HasPrefix(p.FileMIME, "image/")
263 if !isImage {
264 return ""
265 }
266 const expandFormat = `<a href="%s" onclick="return expandFile(event, '%d');"><img src="%s" width="%d" height="%d" class="thumb" style="pointer-events: inherit;"></a>`
267 return template.HTML(url.PathEscape(fmt.Sprintf(expandFormat, srcPath, p.ID, srcPath, p.FileWidth, p.FileHeight)))
268 }
269
270 func (p *Post) Identifier(identifiers bool, force bool) string {
271 if p.IP == "" || !identifiers || (p.Board.Identifiers == IdentifiersDisable && !force) {
272 return ""
273 }
274 adler.Reset()
275 if p.Board.Identifiers == IdentifiersBoard {
276 adler.Write([]byte(strconv.Itoa(p.Board.ID)))
277 }
278 adler.Write([]byte(p.IP))
279
280 adlerSum = adler.Sum(adlerSum[:0])
281
282 base64.RawURLEncoding.Encode(adlerBuf, adlerSum)
283 return string(adlerBuf[:5])
284 }
285
286 func (p *Post) Backlinks(posts []*Post) template.HTML {
287 if !p.Board.Backlinks {
288 return ""
289 }
290 var out []byte
291 BACKLINKS:
292 for _, reply := range posts {
293 matches := RefLinkPattern.FindAll([]byte(reply.Message), -1)
294 for _, match := range matches {
295 id, err := strconv.Atoi(string(match)[8:])
296 if err != nil || id != p.ID {
297 continue
298 } else if out != nil {
299 out = append(out, []byte("<wbr>")...)
300 }
301 out = append(out, FormatRefLink(p.Board.Path(), p.Thread(), reply.ID)...)
302 continue BACKLINKS
303 }
304 }
305 return template.HTML(string(out))
306 }
307
308 func FormatRefLink(boardPath string, threadID int, postID int) []byte {
309 return fmt.Appendf(nil, `<a href="%sres/%d.html#%d">>>%d</a>`, boardPath, threadID, postID, postID)
310 }
311
312 func (p *Post) RefLink() template.HTML {
313 return template.HTML(FormatRefLink(p.Board.Path(), p.Thread(), p.ID))
314 }
315
316 func (p *Post) URL(siteHome string) string {
317 var host string
318 var path string
319 if siteHome != "" {
320 u, err := url.Parse(siteHome)
321 if err == nil {
322 if u.Host != "" {
323 host = "https://" + u.Host
324 }
325 path = u.Path
326 }
327 }
328
329 path = filepath.Join(path, p.Board.Path())
330 if !strings.HasSuffix(path, "/") {
331 path += "/"
332 }
333
334 return fmt.Sprintf(`%s%sres/%d.html#%d`, host, path, p.Thread(), p.ID)
335 }
336
View as plain text