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(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(db *sql.DB, board Actor, op string, content string, thread ObjectBase) template.HTML {
nContent := strings.ReplaceAll(content, `<`, "<")
nContent = ParseLinkComments(db, board, op, nContent, thread)
nContent = ParseCommentQuotes(nContent)
nContent = strings.ReplaceAll(nContent, `/\<`, ">")
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
}