...

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) 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">&gt;&gt;%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