...

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