package post
import (
"fmt"
"html/template"
"io/ioutil"
"mime/multipart"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/FChannel0/FChannel-Server/activitypub"
"github.com/FChannel0/FChannel-Server/config"
"github.com/FChannel0/FChannel-Server/db"
"github.com/FChannel0/FChannel-Server/util"
"github.com/gofiber/fiber/v2"
)
func ConvertHashLink(domain string, link string) string {
re := regexp.MustCompile(`(#.+)`)
parsedLink := re.FindString(link)
if parsedLink != "" {
parsedLink = domain + "" + strings.Replace(parsedLink, "#", "", 1)
parsedLink = strings.Replace(parsedLink, "\r", "", -1)
} else {
parsedLink = link
}
return parsedLink
}
func ParseCommentForReplies(comment string, op string) ([]activitypub.ObjectBase, error) {
re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`)
match := re.FindAllStringSubmatch(comment, -1)
var links []string
for i := 0; i < len(match); i++ {
str := strings.Replace(match[i][0], ">>", "", 1)
str = strings.Replace(str, "www.", "", 1)
str = strings.Replace(str, "http://", "", 1)
str = strings.Replace(str, "https://", "", 1)
str = config.TP + "" + str
_, isReply, err := db.IsReplyToOP(op, str)
if err != nil {
return nil, util.MakeError(err, "ParseCommentForReplies")
}
if !util.IsInStringArray(links, str) && isReply {
links = append(links, str)
}
}
var validLinks []activitypub.ObjectBase
for i := 0; i < len(links); i++ {
reqActivity := activitypub.Activity{Id: links[i]}
_, isValid, err := reqActivity.CheckValid()
if err != nil {
return nil, util.MakeError(err, "ParseCommentForReplies")
}
if isValid {
var reply activitypub.ObjectBase
reply.Id = links[i]
reply.Published = time.Now().UTC()
validLinks = append(validLinks, reply)
}
}
return validLinks, nil
}
func ParseCommentForReply(comment string) (string, error) {
re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`)
match := re.FindAllStringSubmatch(comment, -1)
var links []string
for i := 0; i < len(match); i++ {
str := strings.Replace(match[i][0], ">>", "", 1)
links = append(links, str)
}
if len(links) > 0 {
reqActivity := activitypub.Activity{Id: strings.ReplaceAll(links[0], ">", "")}
_, isValid, err := reqActivity.CheckValid()
if err != nil {
return "", util.MakeError(err, "ParseCommentForReply")
}
if isValid {
return links[0], nil
}
}
return "", nil
}
func ParseLinkTitle(actorName string, op string, content string) string {
re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)\w+(#.+)?)`)
match := re.FindAllStringSubmatch(content, -1)
for i, _ := range match {
link := strings.Replace(match[i][0], ">>", "", 1)
isOP := ""
domain := match[i][2]
if link == op {
isOP = " (OP)"
}
link = ConvertHashLink(domain, link)
content = strings.Replace(content, match[i][0], ">>"+util.ShortURL(actorName, link)+isOP, 1)
}
content = strings.ReplaceAll(content, "'", "'")
content = strings.ReplaceAll(content, "\"", """)
content = strings.ReplaceAll(content, ">", `/\<`)
return content
}
func ParseOptions(ctx *fiber.Ctx, obj activitypub.ObjectBase) activitypub.ObjectBase {
options := util.EscapeString(ctx.FormValue("options"))
if options != "" {
option := strings.Split(options, ";")
email := regexp.MustCompile(".+@.+\\..+")
wallet := regexp.MustCompile("wallet:.+")
delete := regexp.MustCompile("delete:.+")
for _, e := range option {
if e == "noko" {
obj.Option = append(obj.Option, "noko")
} else if e == "sage" {
obj.Option = append(obj.Option, "sage")
} else if e == "nokosage" {
obj.Option = append(obj.Option, "nokosage")
} else if email.MatchString(e) {
obj.Option = append(obj.Option, "email:"+e)
} else if wallet.MatchString(e) {
obj.Option = append(obj.Option, "wallet")
var wallet activitypub.CryptoCur
value := strings.Split(e, ":")
wallet.Type = value[0]
wallet.Address = value[1]
obj.Wallet = append(obj.Wallet, wallet)
} else if delete.MatchString(e) {
obj.Option = append(obj.Option, e)
}
}
}
return obj
}
func CheckCaptcha(captcha string) (bool, error) {
parts := strings.Split(captcha, ":")
if strings.Trim(parts[0], " ") == "" || strings.Trim(parts[1], " ") == "" {
return false, nil
}
path := "public/" + parts[0] + ".png"
code, err := util.GetCaptchaCode(path)
if err != nil {
return false, util.MakeError(err, "ParseOptions")
}
if code != "" {
err = util.DeleteCaptchaCode(path)
if err != nil {
return false, util.MakeError(err, "ParseOptions")
}
err = util.CreateNewCaptcha()
if err != nil {
return false, util.MakeError(err, "ParseOptions")
}
}
return code == strings.ToUpper(parts[1]), nil
}
func GetCaptchaCode(captcha string) string {
re := regexp.MustCompile("\\w+\\.\\w+$")
code := re.FindString(captcha)
re = regexp.MustCompile("\\w+")
code = re.FindString(code)
return code
}
func IsMediaBanned(f multipart.File) (bool, error) {
f.Seek(0, 0)
fileBytes := make([]byte, 2048)
_, err := f.Read(fileBytes)
if err != nil {
return true, util.MakeError(err, "IsMediaBanned")
}
hash := util.HashBytes(fileBytes)
f.Seek(0, 0)
return db.IsHashBanned(hash)
}
func SupportedMIMEType(mime string) bool {
for _, e := range config.SupportedFiles {
if e == mime {
return true
}
}
return false
}
func ObjectFromForm(ctx *fiber.Ctx, obj activitypub.ObjectBase) (activitypub.ObjectBase, error) {
var err error
var file multipart.File
header, _ := ctx.FormFile("file")
if header != nil {
file, _ = header.Open()
}
if file != nil {
defer file.Close()
var tempFile = new(os.File)
obj.Attachment, tempFile, err = activitypub.CreateAttachmentObject(file, header)
if err != nil {
return obj, util.MakeError(err, "ObjectFromForm")
}
defer tempFile.Close()
fileBytes, _ := ioutil.ReadAll(file)
tempFile.Write(fileBytes)
re := regexp.MustCompile(`image/(jpe?g|png|webp)`)
if re.MatchString(obj.Attachment[0].MediaType) {
fileLoc := strings.ReplaceAll(obj.Attachment[0].Href, config.Domain, "")
cmd := exec.Command("exiv2", "rm", "."+fileLoc)
if err := cmd.Run(); err != nil {
return obj, util.MakeError(err, "ObjectFromForm")
}
}
obj.Preview = obj.Attachment[0].CreatePreview()
}
obj.AttributedTo = util.EscapeString(ctx.FormValue("name"))
obj.TripCode = util.EscapeString(ctx.FormValue("tripcode"))
obj.Name = util.EscapeString(ctx.FormValue("subject"))
obj.Content = util.EscapeString(ctx.FormValue("comment"))
obj.Sensitive = (ctx.FormValue("sensitive") != "")
obj = ParseOptions(ctx, obj)
var originalPost activitypub.ObjectBase
originalPost.Id = util.EscapeString(ctx.FormValue("inReplyTo"))
obj.InReplyTo = append(obj.InReplyTo, originalPost)
var activity activitypub.Activity
if !util.IsInStringArray(activity.To, originalPost.Id) {
activity.To = append(activity.To, originalPost.Id)
}
if originalPost.Id != "" {
if local, _ := activity.IsLocal(); !local {
actor, err := activitypub.FingerActor(originalPost.Id)
if err == nil { // Keep things moving if it fails
if !util.IsInStringArray(obj.To, actor.Id) {
obj.To = append(obj.To, actor.Id)
}
}
} else if err != nil {
return obj, util.MakeError(err, "ObjectFromForm")
}
}
replyingTo, err := ParseCommentForReplies(ctx.FormValue("comment"), originalPost.Id)
if err != nil {
return obj, util.MakeError(err, "ObjectFromForm")
}
for _, e := range replyingTo {
has := false
for _, f := range obj.InReplyTo {
if e.Id == f.Id {
has = true
break
}
}
if !has {
obj.InReplyTo = append(obj.InReplyTo, e)
var activity activitypub.Activity
activity.To = append(activity.To, e.Id)
if local, _ := activity.IsLocal(); !local {
actor, err := activitypub.FingerActor(e.Id)
if err != nil {
return obj, util.MakeError(err, "ObjectFromForm")
}
if !util.IsInStringArray(obj.To, actor.Id) {
obj.To = append(obj.To, actor.Id)
}
}
}
}
return obj, nil
}
func ResizeAttachmentToPreview() error {
return activitypub.GetObjectsWithoutPreviewsCallback(func(id, href, mediatype, name string, size int, published time.Time) error {
re := regexp.MustCompile(`^\w+`)
_type := re.FindString(mediatype)
if _type == "image" {
re = regexp.MustCompile(`.+/`)
file := re.ReplaceAllString(mediatype, "")
nHref := util.GetUniqueFilename(file)
var nPreview activitypub.NestedObjectBase
re = regexp.MustCompile(`/\w+$`)
actor := re.ReplaceAllString(id, "")
nPreview.Type = "Preview"
uid, err := util.CreateUniqueID(actor)
if err != nil {
return util.MakeError(err, "ResizeAttachmentToPreview")
}
nPreview.Id = fmt.Sprintf("%s/%s", actor, uid)
nPreview.Name = name
nPreview.Href = config.Domain + "" + nHref
nPreview.MediaType = mediatype
nPreview.Size = int64(size)
nPreview.Published = published
nPreview.Updated = published
re = regexp.MustCompile(`/public/.+`)
objFile := re.FindString(href)
if id != "" {
cmd := exec.Command("convert", "."+objFile, "-resize", "250x250>", "-strip", "."+nHref)
if err := cmd.Run(); err == nil {
config.Log.Println(objFile + " -> " + nHref)
if err := nPreview.WritePreview(); err != nil {
return util.MakeError(err, "ResizeAttachmentToPreview")
}
obj := activitypub.ObjectBase{Id: id}
if err := obj.UpdatePreview(nPreview.Id); err != nil {
return util.MakeError(err, "ResizeAttachmentToPreview")
}
} else {
return util.MakeError(err, "ResizeAttachmentToPreview")
}
}
}
return nil
})
}
func ParseAttachment(obj activitypub.ObjectBase, catalog bool) template.HTML {
// TODO: convert all of these to Sprintf statements, or use strings.Builder or something, anything but this really
// string concatenation is highly inefficient _especially_ when being used like this
if len(obj.Attachment) < 1 {
return ""
}
var media string
if regexp.MustCompile(`image\/`).MatchString(obj.Attachment[0].MediaType) {
media = ""
return template.HTML(media)
}
if regexp.MustCompile(`audio\/`).MatchString(obj.Attachment[0].MediaType) {
media = ""
return template.HTML(media)
}
if regexp.MustCompile(`video\/`).MatchString(obj.Attachment[0].MediaType) {
media = ""
return template.HTML(media)
}
return template.HTML(media)
}
func ParseContent(board activitypub.Actor, op string, content string, thread activitypub.ObjectBase, id string, _type string) (template.HTML, error) {
// TODO: should escape more than just < and >, should also escape &, ", and '
nContent := strings.ReplaceAll(content, `<`, "<")
if _type == "new" {
nContent = ParseTruncate(nContent, board, op, id)
}
nContent, err := ParseLinkComments(board, op, nContent, thread)
if err != nil {
return "", util.MakeError(err, "ParseContent")
}
nContent = ParseCommentQuotes(nContent)
nContent = strings.ReplaceAll(nContent, `/\<`, ">")
return template.HTML(nContent), nil
}
func ParseTruncate(content string, board activitypub.Actor, op string, id string) string {
if strings.Count(content, "\r") > 30 {
content = strings.ReplaceAll(content, "\r\n", "\r")
lines := strings.SplitAfter(content, "\r")
content = ""
for i := 0; i < 30; i++ {
content += lines[i]
}
content += fmt.Sprintf("(view full post...)", board.Id+"/"+util.ShortURL(board.Outbox, op)+"#"+util.ShortURL(board.Outbox, id))
}
return content
}
func ParseLinkComments(board activitypub.Actor, op string, content string, thread activitypub.ObjectBase) (string, error) {
re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`)
match := re.FindAllStringSubmatch(content, -1)
//add url to each matched reply
for i, _ := range match {
isOP := ""
domain := match[i][2]
link := strings.Replace(match[i][0], ">>", "", 1)
if link == op {
isOP = " (OP)"
}
parsedLink := ConvertHashLink(domain, link)
//formate the hover title text
var quoteTitle string
// if the quoted content is local get it
// else get it from the database
if thread.Id == link {
quoteTitle = ParseLinkTitle(board.Outbox, op, thread.Content)
} else {
for _, e := range thread.Replies.OrderedItems {
if e.Id == parsedLink {
quoteTitle = ParseLinkTitle(board.Outbox, op, e.Content)
break
}
}
if quoteTitle == "" {
obj := activitypub.ObjectBase{Id: parsedLink}
col, err := obj.GetCollectionFromPath()
if err != nil {
return "", util.MakeError(err, "ParseLinkComments")
}
if len(col.OrderedItems) > 0 {
quoteTitle = ParseLinkTitle(board.Outbox, op, col.OrderedItems[0].Content)
} else {
quoteTitle = ParseLinkTitle(board.Outbox, op, parsedLink)
}
}
}
if replyID, isReply, err := db.IsReplyToOP(op, parsedLink); err == nil || !isReply {
id := util.ShortURL(board.Outbox, replyID)
content = strings.Replace(content, match[i][0], ">>"+id+""+isOP+"", -1)
} else {
//this is a cross post
parsedOP, err := db.GetReplyOP(parsedLink)
if err == nil {
link = parsedOP + "#" + util.ShortURL(parsedOP, parsedLink)
}
actor, err := activitypub.FingerActor(parsedLink)
if err == nil && actor.Id != "" {
content = strings.Replace(content, match[i][0], ">>"+util.ShortURL(board.Outbox, parsedLink)+isOP+" →", -1)
}
}
}
return content, nil
}
func ParseCommentQuotes(content string) string {
// replace quotes
re := regexp.MustCompile(`((\r\n|\r|\n|^)>(.+)?[^\r\n])`)
match := re.FindAllStringSubmatch(content, -1)
for i, _ := range match {
quote := strings.Replace(match[i][0], ">", ">", 1)
line := re.ReplaceAllString(match[i][0], ""+quote+"")
content = strings.Replace(content, match[i][0], line, 1)
}
//replace isolated greater than symboles
re = regexp.MustCompile(`(\r\n|\n|\r)>`)
return re.ReplaceAllString(content, "\r\n>")
}