package main import ( "database/sql" "fmt" "github.com/gofiber/fiber/v2" _ "github.com/lib/pq" "html/template" "log" "net/http" "regexp" "sort" "strconv" "strings" "time" ) var Key *string = new(string) var FollowingBoards []ObjectBase var Boards []Board type Board struct { Name string Actor Actor Summary string PrefName string InReplyTo string Location string To string RedirectTo string Captcha string CaptchaCode string ModCred string Domain string TP string Restricted bool Post ObjectBase } type PageData struct { Title string PreferredUsername string Board Board Pages []int CurrentPage int TotalPage int Boards []Board Posts []ObjectBase Key string PostId string Instance Actor InstanceIndex []ObjectBase ReturnTo string NewsItems []NewsItem BoardRemainer []int Themes *[]string ThemeCookie string } type AdminPage struct { Title string Board Board Key string Actor string Boards []Board Following []string Followers []string Reported []Report Domain string IsLocal bool PostBlacklist []PostBlacklist AutoSubscribe bool Themes *[]string ThemeCookie string } type Report struct { ID string Count int Reason string } type Removed struct { ID string Type string Board string } func mod(i, j int) bool { return i%j == 0 } func sub(i, j int) int { return i - j } func unixToReadable(u int) string { return time.Unix(int64(u), 0).Format("Jan 02, 2006") } func timeToReadableLong(t time.Time) string { return t.Format("01/02/06(Mon)15:04:05") } func timeToUnix(t time.Time) string { return fmt.Sprint(t.Unix()) } func NewsGet(w http.ResponseWriter, r *http.Request, db *sql.DB, timestamp int) { t := template.Must(template.New("").Funcs(template.FuncMap{ "sub": sub, "unixtoreadable": unixToReadable}).ParseFiles("./static/main.html", "./static/news.html")) actor := GetActorFromDB(db, Domain) var data PageData data.PreferredUsername = actor.PreferredUsername data.Boards = Boards data.Board.Name = "" data.Key = *Key data.Board.Domain = Domain data.Board.ModCred, _ = GetPasswordFromSession(r) data.Board.Actor = actor data.Board.Post.Actor = actor.Id data.Board.Restricted = actor.Restricted data.NewsItems = []NewsItem{NewsItem{}} var err error data.NewsItems[0], err = getNewsItemFromDB(db, timestamp) if err != nil { w.WriteHeader(http.StatusForbidden) w.Write([]byte("404 no path")) return } data.Title = actor.PreferredUsername + ": " + data.NewsItems[0].Title data.Themes = &Themes if cookie, err := r.Cookie("theme"); err == nil { data.ThemeCookie = strings.SplitN(cookie.String(), "=", 2)[1] } err = t.ExecuteTemplate(w, "layout", data) if err != nil { // TODO: actual error handler log.Printf("NewsGet: %s\n", err) } } func AllNewsGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { t := template.Must(template.New("").Funcs(template.FuncMap{ "mod": mod, "sub": sub, "unixtoreadable": unixToReadable}).ParseFiles("./static/main.html", "./static/anews.html")) actor := GetActorFromDB(db, Domain) var data PageData data.PreferredUsername = actor.PreferredUsername data.Title = actor.PreferredUsername + " News" data.Boards = Boards data.Board.Name = "" data.Key = *Key data.Board.Domain = Domain data.Board.ModCred, _ = GetPasswordFromSession(r) data.Board.Actor = actor data.Board.Post.Actor = actor.Id data.Board.Restricted = actor.Restricted data.NewsItems = getNewsFromDB(db, 0) data.Themes = &Themes if cookie, err := r.Cookie("theme"); err == nil { data.ThemeCookie = strings.SplitN(cookie.String(), "=", 2)[1] } err := t.ExecuteTemplate(w, "layout", data) if err != nil { // TODO: actual error handler log.Printf("AllNewsGet: %s\n", err) } } func OutboxGet(c *fiber.Ctx) error { collection, valid := WantToServePage(DB, c.Params("actor"), 0) if !valid { return c.SendString("404") } actor := collection.Actor postNum := c.Query("page") page, _ := strconv.Atoi(postNum) var returnData PageData returnData.Board.Name = actor.Name returnData.Board.PrefName = actor.PreferredUsername returnData.Board.Summary = actor.Summary returnData.Board.InReplyTo = "" returnData.Board.To = actor.Outbox returnData.Board.Actor = *actor returnData.Board.ModCred, _ = GetPasswordFromCtx(c) returnData.Board.Domain = Domain returnData.Board.Restricted = actor.Restricted returnData.CurrentPage = page returnData.ReturnTo = "feed" returnData.Board.Post.Actor = actor.Id returnData.Board.Captcha = Domain + "/" + GetRandomCaptcha(DB) returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername returnData.Key = *Key returnData.Boards = Boards returnData.Posts = collection.OrderedItems var offset = 15 var pages []int pageLimit := (float64(collection.TotalItems) / float64(offset)) if pageLimit > 11 { pageLimit = 11 } for i := 0.0; i < pageLimit; i++ { pages = append(pages, int(i)) } returnData.Pages = pages returnData.TotalPage = len(returnData.Pages) - 1 returnData.Themes = &Themes returnData.ThemeCookie = GetThemeCookie(c) return c.Render("nposts", fiber.Map{ "page": returnData, }, "layouts/main") } func CatalogGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection) { t := template.Must(template.New("").Funcs(template.FuncMap{ "proxy": func(url string) string { return MediaProxy(url) }, "short": func(actorName string, url string) string { return shortURL(actorName, url) }, "parseAttachment": func(obj ObjectBase, catalog bool) template.HTML { return ParseAttachment(obj, catalog) }, "isOnion": func(url string) bool { return IsOnion(url) }, "showArchive": func() bool { col := GetActorCollectionDBTypeLimit(db, collection.Actor.Id, "Archive", 1) if len(col.OrderedItems) > 0 { return true } return false }, "sub": sub}).ParseFiles("./static/main.html", "./static/ncatalog.html", "./static/top.html")) actor := collection.Actor var returnData PageData returnData.Board.Name = actor.Name returnData.Board.PrefName = actor.PreferredUsername returnData.Board.InReplyTo = "" returnData.Board.To = actor.Outbox returnData.Board.Actor = *actor returnData.Board.Summary = actor.Summary returnData.Board.ModCred, _ = GetPasswordFromSession(r) returnData.Board.Domain = Domain returnData.Board.Restricted = actor.Restricted returnData.Key = *Key returnData.ReturnTo = "catalog" returnData.Board.Post.Actor = actor.Id returnData.Instance = GetActorFromDB(db, Domain) returnData.Board.Captcha = Domain + "/" + GetRandomCaptcha(db) returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername returnData.Boards = Boards returnData.Posts = collection.OrderedItems returnData.Themes = &Themes if cookie, err := r.Cookie("theme"); err == nil { returnData.ThemeCookie = strings.SplitN(cookie.String(), "=", 2)[1] } err := t.ExecuteTemplate(w, "layout", returnData) if err != nil { // TODO: actual error handler log.Printf("CatalogGet: %s\n", err) } } func ArchiveGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection) { t := template.Must(template.New("").Funcs(template.FuncMap{ "proxy": func(url string) string { return MediaProxy(url) }, "short": func(actorName string, url string) string { return shortURL(actorName, url) }, "shortExcerpt": func(post ObjectBase) template.HTML { return template.HTML(ShortExcerpt(post)) }, "parseAttachment": func(obj ObjectBase, catalog bool) template.HTML { return ParseAttachment(obj, catalog) }, "mod": mod, "sub": sub}).ParseFiles("./static/main.html", "./static/archive.html", "./static/bottom.html")) actor := collection.Actor var returnData PageData returnData.Board.Name = actor.Name returnData.Board.PrefName = actor.PreferredUsername returnData.Board.InReplyTo = "" returnData.Board.To = actor.Outbox returnData.Board.Actor = *actor returnData.Board.Summary = actor.Summary returnData.Board.ModCred, _ = GetPasswordFromSession(r) returnData.Board.Domain = Domain returnData.Board.Restricted = actor.Restricted returnData.Key = *Key returnData.ReturnTo = "archive" returnData.Board.Post.Actor = actor.Id returnData.Instance = GetActorFromDB(db, Domain) returnData.Board.Captcha = Domain + "/" + GetRandomCaptcha(db) returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername returnData.Boards = Boards returnData.Posts = collection.OrderedItems returnData.Themes = &Themes if cookie, err := r.Cookie("theme"); err == nil { returnData.ThemeCookie = strings.SplitN(cookie.String(), "=", 2)[1] } err := t.ExecuteTemplate(w, "layout", returnData) if err != nil { // TODO: actual error handler log.Printf("ArchiveGet: %s\n", err) } } func PostGet(c *fiber.Ctx) error { actor := GetActorByNameFromDB(DB, c.Params("actor")) postId := c.Params("post") inReplyTo := actor.Id + "/" + postId var returnData PageData returnData.Board.Name = actor.Name returnData.Board.PrefName = actor.PreferredUsername returnData.Board.To = actor.Outbox returnData.Board.Actor = actor returnData.Board.Summary = actor.Summary returnData.Board.ModCred, _ = GetPasswordFromCtx(c) returnData.Board.Domain = Domain returnData.Board.Restricted = actor.Restricted returnData.ReturnTo = "feed" returnData.Board.Captcha = Domain + "/" + GetRandomCaptcha(DB) returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) returnData.Instance = GetActorFromDB(DB, Domain) returnData.Title = "/" + returnData.Board.Name + "/ - " + returnData.Board.PrefName returnData.Key = *Key returnData.Boards = Boards re := regexp.MustCompile("f(\\w|[!@#$%^&*<>])+-(\\w|[!@#$%^&*<>])+") if re.MatchString(postId) { // if non local actor post name := GetActorFollowNameFromPath(postId) followActors := GetActorsFollowFromName(actor, name) followCollection := GetActorsFollowPostFromId(DB, followActors, postId) if len(followCollection.OrderedItems) > 0 { returnData.Board.InReplyTo = followCollection.OrderedItems[0].Id returnData.Posts = append(returnData.Posts, followCollection.OrderedItems[0]) var actor Actor actor = FingerActor(returnData.Board.InReplyTo) returnData.Board.Post.Actor = actor.Id } } else { collection := GetObjectByIDFromDB(DB, inReplyTo) if collection.Actor != nil { returnData.Board.Post.Actor = collection.Actor.Id returnData.Board.InReplyTo = inReplyTo if len(collection.OrderedItems) > 0 { returnData.Posts = append(returnData.Posts, collection.OrderedItems[0]) } } } if len(returnData.Posts) > 0 { returnData.PostId = shortURL(returnData.Board.To, returnData.Posts[0].Id) } returnData.Themes = &Themes returnData.ThemeCookie = GetThemeCookie(c) return c.Render("npost", fiber.Map{ "page": returnData, }, "layouts/main") } func GetBoardCollection(db *sql.DB) []Board { var collection []Board for _, e := range FollowingBoards { var board Board boardActor := GetActorFromDB(db, e.Id) if boardActor.Id == "" { boardActor = FingerActor(e.Id) } board.Name = boardActor.Name board.PrefName = boardActor.PreferredUsername board.Location = "/" + boardActor.Name board.Actor = boardActor board.Restricted = boardActor.Restricted collection = append(collection, board) } sort.Sort(BoardSortAsc(collection)) return collection } func WantToServePage(db *sql.DB, actorName string, page int) (Collection, bool) { var collection Collection serve := false if page > 10 { return collection, serve } actor := GetActorByNameFromDB(db, actorName) if actor.Id != "" { collection = GetObjectFromDBPage(db, actor.Id, page) collection.Actor = &actor return collection, true } return collection, serve } func WantToServeCatalog(db *sql.DB, actorName string) (Collection, bool) { var collection Collection serve := false actor := GetActorByNameFromDB(db, actorName) if actor.Id != "" { collection = GetObjectFromDBCatalog(db, actor.Id) collection.Actor = &actor return collection, true } return collection, serve } func WantToServeArchive(db *sql.DB, actorName string) (Collection, bool) { var collection Collection serve := false actor := GetActorByNameFromDB(db, actorName) if actor.Id != "" { collection = GetActorCollectionDBType(db, actor.Id, "Archive") collection.Actor = &actor return collection, true } return collection, serve } func StripTransferProtocol(value string) string { re := regexp.MustCompile("(http://|https://)?(www.)?") value = re.ReplaceAllString(value, "") return value } 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 CreateLocalDeleteDB(db *sql.DB, id string, _type string) { query := `select id from removed where id=$1` rows, err := db.Query(query, id) CheckError(err, "could not query removed") defer rows.Close() if rows.Next() { var i string rows.Scan(&i) if i != "" { query := `update removed set type=$1 where id=$2` _, err := db.Exec(query, _type, id) CheckError(err, "Could not update removed post") } } else { query := `insert into removed (id, type) values ($1, $2)` _, err := db.Exec(query, id, _type) CheckError(err, "Could not insert removed post") } } func GetLocalDeleteDB(db *sql.DB) []Removed { var deleted []Removed query := `select id, type from removed` rows, err := db.Query(query) CheckError(err, "could not query removed") defer rows.Close() for rows.Next() { var r Removed rows.Scan(&r.ID, &r.Type) deleted = append(deleted, r) } return deleted } func CreateLocalReportDB(db *sql.DB, id string, board string, reason string) { query := `select id, count from reported where id=$1 and board=$2` rows, err := db.Query(query, id, board) CheckError(err, "could not query reported") defer rows.Close() if rows.Next() { var i string var count int rows.Scan(&i, &count) if i != "" { count = count + 1 query := `update reported set count=$1 where id=$2` _, err := db.Exec(query, count, id) CheckError(err, "Could not update reported post") } } else { query := `insert into reported (id, count, board, reason) values ($1, $2, $3, $4)` _, err := db.Exec(query, id, 1, board, reason) CheckError(err, "Could not insert reported post") } } func GetLocalReportDB(db *sql.DB, board string) []Report { var reported []Report query := `select id, count, reason from reported where board=$1` rows, err := db.Query(query, board) CheckError(err, "could not query reported") defer rows.Close() for rows.Next() { var r Report rows.Scan(&r.ID, &r.Count, &r.Reason) reported = append(reported, r) } return reported } func CloseLocalReportDB(db *sql.DB, id string, board string) { query := `delete from reported where id=$1 and board=$2` _, err := db.Exec(query, id, board) CheckError(err, "Could not delete local report from db") } func GetActorFollowNameFromPath(path string) string { var actor string re := regexp.MustCompile("f\\w+-") actor = re.FindString(path) actor = strings.Replace(actor, "f", "", 1) actor = strings.Replace(actor, "-", "", 1) return actor } func GetActorsFollowFromName(actor Actor, name string) []string { var followingActors []string follow := GetActorCollection(actor.Following) re := regexp.MustCompile("\\w+?$") for _, e := range follow.Items { if re.FindString(e.Id) == name { followingActors = append(followingActors, e.Id) } } return followingActors } func GetActorsFollowPostFromId(db *sql.DB, actors []string, id string) Collection { var collection Collection for _, e := range actors { tempCol := GetObjectByIDFromDB(db, e+"/"+id) if len(tempCol.OrderedItems) > 0 { collection = tempCol return collection } } return collection } type ObjectBaseSortDesc []ObjectBase func (a ObjectBaseSortDesc) Len() int { return len(a) } func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated.After(a[j].Updated) } func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type ObjectBaseSortAsc []ObjectBase func (a ObjectBaseSortAsc) Len() int { return len(a) } func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published.Before(a[j].Published) } func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type BoardSortAsc []Board func (a BoardSortAsc) Len() int { return len(a) } func (a BoardSortAsc) Less(i, j int) bool { return a[i].Name < a[j].Name } func (a BoardSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func MediaProxy(url string) string { re := regexp.MustCompile("(.+)?" + Domain + "(.+)?") if re.MatchString(url) { return url } re = regexp.MustCompile("(.+)?\\.onion(.+)?") if re.MatchString(url) { return url } MediaHashs[HashMedia(url)] = url return "/api/media?hash=" + HashMedia(url) } func ParseAttachment(obj ObjectBase, catalog bool) template.HTML { if len(obj.Attachment) < 1 { return "" } var media string if regexp.MustCompile(`image\/`).MatchString(obj.Attachment[0].MediaType) { media = "") return template.HTML(nContent) } func ParseLinkComments(db *sql.DB, board Actor, op string, content string, thread ObjectBase) string { 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 { link := strings.Replace(match[i][0], ">>", "", 1) isOP := "" domain := match[i][2] 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 := GetObjectFromDBFromID(db, parsedLink) if len(obj.OrderedItems) > 0 { quoteTitle = ParseLinkTitle(board.Outbox, op, obj.OrderedItems[0].Content) } else { quoteTitle = ParseLinkTitle(board.Outbox, op, parsedLink) } } } //replace link with quote format replyID, isReply := IsReplyToOP(db, op, parsedLink) if isReply { id := shortURL(board.Outbox, replyID) content = strings.Replace(content, match[i][0], ">>"+id+""+isOP+"", -1) } else { //this is a cross post parsedOP := GetReplyOP(db, parsedLink) actor := FingerActor(parsedLink) if parsedOP != "" { link = parsedOP + "#" + shortURL(parsedOP, parsedLink) } if actor.Id != "" { content = strings.Replace(content, match[i][0], ">>"+shortURL(board.Outbox, parsedLink)+isOP+" →", -1) } } } return content } 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], ">>"+shortURL(actorName, link)+isOP, 1) } content = strings.ReplaceAll(content, "'", "") content = strings.ReplaceAll(content, "\"", "") content = strings.ReplaceAll(content, ">", `/\<`) return content } 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>") } 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 ShortImg(url string) string { nURL := url re := regexp.MustCompile(`(\.\w+$)`) fileName := re.ReplaceAllString(url, "") if len(fileName) > 26 { re := regexp.MustCompile(`(^.{26})`) match := re.FindStringSubmatch(fileName) if len(match) > 0 { nURL = match[0] } re = regexp.MustCompile(`(\..+$)`) match = re.FindStringSubmatch(url) if len(match) > 0 { nURL = nURL + "(...)" + match[0] } } return nURL } func ConvertSize(size int64) string { var rValue string convert := float32(size) / 1024.0 if convert > 1024 { convert = convert / 1024.0 rValue = fmt.Sprintf("%.2f MB", convert) } else { rValue = fmt.Sprintf("%.2f KB", convert) } return rValue } func ShortExcerpt(post ObjectBase) string { var returnString string if post.Name != "" { returnString = post.Name + "| " + post.Content } else { returnString = post.Content } re := regexp.MustCompile(`(^(.|\r\n|\n){100})`) match := re.FindStringSubmatch(returnString) if len(match) > 0 { returnString = match[0] + "..." } re = regexp.MustCompile(`(^.+\|)`) match = re.FindStringSubmatch(returnString) if len(match) > 0 { returnString = strings.Replace(returnString, match[0], ""+match[0]+"", 1) returnString = strings.Replace(returnString, "|", ":", 1) } return returnString }