...

Source file src/codeberg.org/tslocum/sriracha/model/model_post.go

Documentation: codeberg.org/tslocum/sriracha/model

     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  	// Calculated fields.
    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  	// Get document body HTML.
   247  	truncated, err := first.Html()
   248  	if err != nil {
   249  		log.Fatal(err)
   250  	}
   251  	// Replace XHTML line break tags.
   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">&gt;&gt;%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