diff options
-rwxr-xr-x | Server | bin | 11195223 -> 0 bytes | |||
-rw-r--r-- | activityPubStruct.go | 11 | ||||
-rw-r--r-- | client.go | 289 | ||||
-rw-r--r-- | database.go | 62 | ||||
-rw-r--r-- | databaseschema.psql | 7 | ||||
-rw-r--r-- | follow.go | 95 | ||||
-rw-r--r-- | main.go | 261 | ||||
-rw-r--r-- | outboxPost.go | 65 | ||||
-rw-r--r-- | static/faq.html | 10 | ||||
-rw-r--r-- | static/main.html | 9 | ||||
-rw-r--r-- | static/manage.html | 2 | ||||
-rw-r--r-- | static/ncatalog.html | 51 | ||||
-rw-r--r-- | static/npost.html | 2 | ||||
-rw-r--r-- | static/posts.html | 134 |
14 files changed, 722 insertions, 276 deletions
Binary files differ diff --git a/activityPubStruct.go b/activityPubStruct.go index e790573..62fabe1 100644 --- a/activityPubStruct.go +++ b/activityPubStruct.go @@ -1,6 +1,9 @@ package main -import "encoding/json" +import ( + "encoding/json" + "html/template" +) type AtContextRaw struct { Context json.RawMessage `json:"@context,omitempty"` @@ -111,7 +114,8 @@ type ObjectBase struct { TripCode string `json:"tripcode,omitempty"` Actor string `json:"actor,omitempty"` Audience string `json:"audience,omitempty"` - Content string `json:"content,omitempty"` + ContentHTML template.HTML `json:"contenthtml,omitempty"` + Content string `json:"content,omitempty"` EndTime string `json:"endTime,omitempty"` Generator string `json:"generator,omitempty"` Icon string `json:"icon,omitempty"` @@ -156,7 +160,8 @@ type NestedObjectBase struct { TripCode string `json:"tripcode,omitempty"` Actor string `json:"actor,omitempty"` Audience string `json:"audience,omitempty"` - Content string `json:"content,omitempty"` + ContentHTML template.HTML `json:"contenthtml,omitempty"` + Content string `json:"content,omitempty"` EndTime string `json:"endTime,omitempty"` Generator string `json:"generator,omitempty"` Icon string `json:"icon,omitempty"` @@ -1,14 +1,16 @@ package main -import "net/http" -import "html/template" -import "database/sql" -import _ "github.com/lib/pq" -import "strings" -import "strconv" -import "sort" -import "regexp" -import "time" +import ( + "net/http" + "html/template" + "database/sql" + _ "github.com/lib/pq" + "strings" + "strconv" + "sort" + "regexp" + "time" +) var Key *string = new(string) @@ -116,6 +118,7 @@ func IndexGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { if(len(data.BoardRemainer) == 3){ data.BoardRemainer = make([]int, 0) } + data.InstanceIndex = GetCollectionFromReq("https://fchan.xyz/followers").Items data.NewsItems = getNewsFromDB(db, 3) @@ -180,10 +183,23 @@ func AllNewsGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { } func OutboxGet(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) + }, + "parseReplyLink": func(actorId string, op string, id string, content string) template.HTML { + actor := FingerActor(actorId) + title := strings.ReplaceAll(ParseLinkTitle(actor.Id, op, content), `/\<`, ">") + link := "<a href=\"" + actor.Name + "/" + shortURL(actor.Outbox, op) + "#" + shortURL(actor.Outbox, id) + "\" title=\"" + title + "\">>>" + shortURL(actor.Outbox, id) + "</a>" + return template.HTML(link) + }, "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/nposts.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) @@ -219,6 +235,14 @@ func OutboxGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Co returnData.Boards = Boards returnData.Posts = collection.OrderedItems + for i, e := range returnData.Posts { + returnData.Posts[i].ContentHTML = ParseContent(db, returnData.Board.Actor, e.Id, e.Content, e) + + for j, k := range e.Replies.OrderedItems { + returnData.Posts[i].Replies.OrderedItems[j].ContentHTML = ParseContent(db, returnData.Board.Actor, e.Id, k.Content, e) + } + } + var offset = 8 var pages []int pageLimit := (float64(collection.TotalItems) / float64(offset)) @@ -236,7 +260,13 @@ func CatalogGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection C 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) + }, "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/ncatalog.html", "./static/top.html")) actor := collection.Actor @@ -273,7 +303,20 @@ func PostGet(w http.ResponseWriter, r *http.Request, db *sql.DB){ 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) + }, + "parseReplyLink": func(actorId string, op string, id string, content string) template.HTML { + actor := FingerActor(actorId) + title := strings.ReplaceAll(ParseLinkTitle(actor.Id, op, content), `/\<`, ">") + link := "<a href=\"" + actor.Name + "/" + shortURL(actor.Outbox, op) + "#" + shortURL(actor.Outbox, id) + "\" title=\"" + title + "\">>>" + shortURL(actor.Outbox, id) + "</a>" + return template.HTML(link) + }, "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/npost.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) path := r.URL.Path @@ -333,6 +376,14 @@ func PostGet(w http.ResponseWriter, r *http.Request, db *sql.DB){ if len(returnData.Posts) > 0 { returnData.PostId = shortURL(returnData.Board.To, returnData.Posts[0].Id) + + for i, e := range returnData.Posts { + returnData.Posts[i].ContentHTML = ParseContent(db, returnData.Board.Actor, e.Id, e.Content, e) + + for j, k := range e.Replies.OrderedItems { + returnData.Posts[i].Replies.OrderedItems[j].ContentHTML = ParseContent(db, returnData.Board.Actor, e.Id, k.Content, e) + } + } } t.ExecuteTemplate(w, "layout", returnData) @@ -596,7 +647,221 @@ func MediaProxy(url string) string { 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 = "<img " + media += "id=\"img\" " + media += "main=\"1\" " + media += "enlarge=\"0\" " + media += "attachment=\"" + obj.Attachment[0].Href + "\"" + if catalog { + media += "style=\"max-width: 180px; max-height: 180px;\" " + } else { + media += "style=\"float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;\"" + } + if obj.Preview.Id != "" { + media += "src=\"" + MediaProxy(obj.Preview.Href) + "\"" + media += "preview=\"" + MediaProxy(obj.Preview.Href) + "\"" + } else { + media += "src=\"" + MediaProxy(obj.Attachment[0].Href) + "\"" + media += "preview=\"" + MediaProxy(obj.Attachment[0].Href) + "\"" + } + + media += ">" + + return template.HTML(media) + } + + if(regexp.MustCompile(`audio\/`).MatchString(obj.Attachment[0].MediaType)){ + media = "<audio " + media += "controls=\"controls\" " + media += "preload=\"metadta\" " + if catalog { + media += "style=\"margin-right: 10px; margin-bottom: 10px; max-width: 180px; max-height: 180px;\" " + } else { + media += "style=\"float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;\" " + } + media += ">" + media += "<source " + media += "src=\"" + MediaProxy(obj.Attachment[0].Href) + "\" " + media += "type=\"" + obj.Attachment[0].MediaType + "\" " + media += ">" + media += "Audio is not supported." + media += "</audio>" + + return template.HTML(media) + } + + if(regexp.MustCompile(`video\/`).MatchString(obj.Attachment[0].MediaType)){ + media = "<video " + media += "controls=\"controls\" " + media += "preload=\"metadta\" " + media += "muted=\"muted\" " + if catalog { + media += "style=\"margin-right: 10px; margin-bottom: 10px; max-width: 180px; max-height: 180px;\" " + } else { + media += "style=\"float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;\" " + } + media += ">" + media += "<source " + media += "src=\"" + MediaProxy(obj.Attachment[0].Href) + "\" " + media += "type=\"" + obj.Attachment[0].MediaType + "\" " + media += ">" + media += "Video is not supported." + media += "</video>" + + 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) + } + } + } + + var style string + if board.Restricted { + style = "color: #af0a0f;" + } + + //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], "<a class=\"reply\" style=\"" + style + "\" title=\"" + quoteTitle + "\" href=\"/" + board.Name + "/" + shortURL(board.Outbox, op) + "#" + id + "\">>>" + id + "" + isOP + "</a>", -1) + + } else { + + //this is a cross post + parsedOP := GetReplyOP(db, parsedLink) + + if parsedOP != "" { + link = parsedOP + "#" + shortURL(parsedOP, parsedLink) + } + + content = strings.Replace(content, match[i][0], "<a class=\"reply\" style=\"" + style + "\" title=\"" + quoteTitle + "\" href=\"" + link + "\">>>" + shortURL(board.Outbox, parsedLink) + isOP + " →</a>", -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], "<span class=\"quote\">" + quote + "</span>") + 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<span class=\"quote\">></span>") +} + +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 +} diff --git a/database.go b/database.go index 1b2ec3d..db6a3d1 100644 --- a/database.go +++ b/database.go @@ -6,6 +6,7 @@ import ( "os" "sort" "strings" + "regexp" "time" "html/template" @@ -554,6 +555,57 @@ func GetObjectFromDB(db *sql.DB, id string) Collection { return nColl } +func GetObjectFromDBFromID(db *sql.DB, id string) Collection { + var nColl Collection + var result []ObjectBase + + query := `select x.id, x.name, x.content, x.type, x.published, x.updated, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from activitystream where id like $1 and type='Note' union select id, name, content, type, published, updated, attributedto, attachment, preview, actor, tripcode, sensitive from cacheactivitystream where id like $1 and type='Note') as x order by x.updated` + + re := regexp.MustCompile(`f(\w+)\-`) + match := re.FindStringSubmatch(id) + + if len(match) > 0 { + re := regexp.MustCompile(`(.+)\-`) + id = re.ReplaceAllString(id, "") + id = "%" + match[1] + "/" + id + } + + rows, err := db.Query(query, id) + + CheckError(err, "error query object from db") + + defer rows.Close() + for rows.Next(){ + var post ObjectBase + var actor Actor + var attachID string + var previewID string + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &previewID, &actor.Id, &post.TripCode, &post.Sensitive) + + CheckError(err, "error scan object into post struct") + + post.Actor = actor.Id + + var postCnt int + var imgCnt int + post.Replies, postCnt, imgCnt = GetObjectRepliesDB(db, post) + + post.Replies.TotalItems = postCnt + post.Replies.TotalImgs = imgCnt + + post.Attachment = GetObjectAttachment(db, attachID) + + post.Preview = GetObjectPreview(db, previewID) + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl +} + func GetObjectFromDBCatalog(db *sql.DB, id string) Collection { var nColl Collection var result []ObjectBase @@ -671,7 +723,7 @@ func GetObjectRepliesDBLimit(db *sql.DB, parent ObjectBase, limit int) (*Collect rows, err := db.Query(query, parent.Id, limit) - CheckError(err, "error with replies db query") + CheckError(err, "error with replies db query") var postCount int var attachCount int @@ -692,7 +744,8 @@ func GetObjectRepliesDBLimit(db *sql.DB, parent ObjectBase, limit int) (*Collect post.Actor = actor.Id var postCnt int - var imgCnt int + var imgCnt int + post.Replies, postCnt, imgCnt = GetObjectRepliesRepliesDB(db, post) post.Replies.TotalItems = postCnt @@ -742,7 +795,8 @@ func GetObjectRepliesDB(db *sql.DB, parent ObjectBase) (*CollectionBase, int, in post.Actor = actor.Id var postCnt int - var imgCnt int + var imgCnt int + post.Replies, postCnt, imgCnt = GetObjectRepliesRepliesDB(db, post) post.Replies.TotalItems = postCnt @@ -805,7 +859,7 @@ func GetObjectRepliesRepliesDB(db *sql.DB, parent ObjectBase) (*CollectionBase, query := `select count(x.id) over(), sum(case when RTRIM(x.attachment) = '' then 0 else 1 end) over(), x.id, x.name, x.content, x.type, x.published, x.attributedto, x.attachment, x.preview, x.actor, x.tripcode, x.sensitive from (select * from activitystream where id in (select id from replies where inreplyto=$1) and type='Note' union select * from cacheactivitystream where id in (select id from replies where inreplyto=$1) and type='Note') as x order by x.published asc` - rows, err := db.Query(query, parent.Id) + rows, err := db.Query(query, parent.Id) CheckError(err, "error with replies replies db query") diff --git a/databaseschema.psql b/databaseschema.psql index 954aa2e..67681cd 100644 --- a/databaseschema.psql +++ b/databaseschema.psql @@ -232,4 +232,9 @@ id serial primary key, regex varchar(200) ); -ALTER TABLE actor ADD COLUMN IF NOT EXISTS autosubscribe boolean default false;
\ No newline at end of file +ALTER TABLE actor ADD COLUMN IF NOT EXISTS autosubscribe boolean default false; + +CREATE TABLE IF NOT EXISTS bannedmedia( +id serial primary key, +hash varchar(200) +);
\ No newline at end of file @@ -6,7 +6,6 @@ import ( "net/http" _ "github.com/lib/pq" - ) func GetActorFollowing(w http.ResponseWriter, db *sql.DB, id string) { @@ -180,69 +179,111 @@ func IsAlreadyFollower(db *sql.DB, actor string, follow string) bool { return false; } -func SetActorFollowerDB(db *sql.DB, activity Activity) Activity { +func SetActorFollowerDB(db *sql.DB, activity Activity) Activity { var query string alreadyFollow := IsAlreadyFollower(db, activity.Actor.Id, activity.Object.Actor) + activity.Type = "Reject" + if activity.Actor.Id == activity.Object.Actor { + return activity + } + if alreadyFollow { query = `delete from follower where id=$1 and follower=$2` activity.Summary = activity.Object.Actor + " Unfollow " + activity.Actor.Id _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor) - if CheckError(err, "error with follower db insert/delete") != nil { + if CheckError(err, "error with follower db delete") != nil { activity.Type = "Reject" return activity - } + } + + activity.Type = "Accept" + return activity } else { query = `insert into follower (id, follower) values ($1, $2)` - activity.Summary = activity.Object.Actor + " Follow " + activity.Actor.Id - } + activity.Summary = activity.Object.Actor + " Follow " + activity.Actor.Id - _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor) + _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor) + + if CheckError(err, "error with follower db insert") != nil { + activity.Type = "Reject" + return activity + } + + activity.Type = "Accept" + return activity + } - if CheckError(err, "error with follower db insert/delete") != nil { - activity.Type = "Reject" - return activity - } - activity.Type = "Accept" return activity } func SetActorFollowingDB(db *sql.DB, activity Activity) Activity { var query string - alreadyFollow := false + alreadyFollowing := false + alreadyFollower := false following := GetActorFollowingDB(db, activity.Object.Actor) + actor := FingerActor(activity.Actor.Id) + + remoteActorFollowerCol := GetCollectionFromReq(actor.Followers) + for _, e := range following { if e.Id == activity.Actor.Id { - alreadyFollow = true + alreadyFollowing = true } } + + for _, e := range remoteActorFollowerCol.Items { + if e.Id == activity.Object.Actor { + alreadyFollower = true + } + } + + activity.Type = "Reject" + + if activity.Actor.Id == activity.Object.Actor { + return activity + } - if alreadyFollow { + if alreadyFollowing && alreadyFollower { query = `delete from following where id=$1 and following=$2` activity.Summary = activity.Object.Actor + " Unfollowing " + activity.Actor.Id if !IsActorLocal(db, activity.Actor.Id) { go DeleteActorCache(db, activity.Actor.Id) - } - } else { + } + _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id) + + if CheckError(err, "error with following db delete") != nil { + activity.Type = "Reject" + return activity + } + + activity.Type = "Accept" + return activity + } + + if !alreadyFollowing && !alreadyFollower { + query = `insert into following (id, following) values ($1, $2)` activity.Summary = activity.Object.Actor + " Following " + activity.Actor.Id if !IsActorLocal(db, activity.Actor.Id) { go WriteActorToCache(db, activity.Actor.Id) } - } - - _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id) + _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id) - if CheckError(err, "error with following db insert/delete") != nil { - activity.Type = "Reject" + if CheckError(err, "error with following db insert") != nil { + activity.Type = "Reject" + return activity + } + + activity.Type = "Accept" return activity - } + } + - activity.Type = "Accept" return activity } @@ -260,9 +301,11 @@ func AutoFollow(db *sql.DB, actor string) { } if !isFollowing && e.Id != Domain && e.Id != actor { - followActivity := MakeFollowActivity(db, actor, e.Id) + followActivity := MakeFollowActivity(db, actor, e.Id) + + nActor := FingerActor(e.Id) - if FingerActor(e.Id).Id != "" { + if nActor.Id != "" { MakeActivityRequestOutbox(db, followActivity) } } @@ -49,6 +49,8 @@ var activitystreams = "application/ld+json; profile=\"https://www.w3.org/ns/acti var MediaHashs = make(map[string]string) +var ActorCache = make(map[string]Actor) + func main() { CreatedNeededDirectories() @@ -270,7 +272,7 @@ func main() { auth := CreateTripCode(verify.Code) auth = CreateTripCode(auth) - + if CreateTripCode(auth) == code { w.WriteHeader(http.StatusOK) } else { @@ -334,8 +336,8 @@ func main() { } if(r.FormValue("inReplyTo") == "" && file == nil) { - w.Write([]byte("Media is required for new posts")) - return + w.Write([]byte("Media is required for new posts")) + return } @@ -500,7 +502,7 @@ func main() { } } - //follow all of boards followers + //follow all of boards followers } else if followers.MatchString(follow){ followersActor := FingerActor(follow) col := GetActorCollection(followersActor.Followers) @@ -519,7 +521,7 @@ func main() { } } - //do a normal follow to a single board + //do a normal follow to a single board } else { followActivity := MakeFollowActivity(db, actorId, follow) @@ -601,7 +603,7 @@ func main() { } else if admin || actor.Id == Domain { t := template.Must(template.New("").Funcs(template.FuncMap{ "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/nadmin.html")) - + actor := GetActor(Domain) follow := GetActorCollection(actor.Following).Items follower := GetActorCollection(actor.Followers).Items @@ -769,7 +771,96 @@ func main() { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("404 no path")) } - }) + }) + + http.HandleFunc("/banmedia", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + board := r.URL.Query().Get("board") + + _, auth := GetPasswordFromSession(r) + + if id == "" || auth == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + col := GetCollectionFromID(id) + + if len(col.OrderedItems) > 0 { + + actor := col.OrderedItems[0].Actor + + if !HasAuth(db, auth, actor) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + if len(col.OrderedItems[0].Attachment) > 0 { + re := regexp.MustCompile(Domain) + file := re.ReplaceAllString(col.OrderedItems[0].Attachment[0].Href, "") + + f, err := os.Open("." + file) + CheckError(err, "could not open attachment for ban media") + + defer f.Close() + + bytes := make([]byte, 2048) + + _, err = f.Read(bytes) + if err != nil { + fmt.Println("error readin bytes for setting media ban") + } + + if !IsMediaBanned(db, f) { + query := `insert into bannedmedia (hash) values ($1)` + + _, err := db.Exec(query, HashBytes(bytes)) + + CheckError(err, "error inserting banend media into db") + + } + + var obj ObjectBase + obj.Id = id + obj.Actor = actor + + isOP := CheckIfObjectOP(db, obj.Id) + + var OP string + if len(col.OrderedItems[0].InReplyTo) > 0 { + OP = col.OrderedItems[0].InReplyTo[0].Id + } + + if !isOP { + TombstoneObject(db, id) + } else { + TombstoneObjectAndReplies(db, id) + } + + if IsIDLocal(db, id){ + go DeleteObjectRequest(db, id) + } + + + if !isOP { + if (!IsIDLocal(db, id)){ + http.Redirect(w, r, "/" + board + "/" + remoteShort(OP), http.StatusSeeOther) + return + } else { + http.Redirect(w, r, OP, http.StatusSeeOther) + return + } + } else { + http.Redirect(w, r, "/" + board, http.StatusSeeOther) + return + } + } + } + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + }) http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request){ id := r.URL.Query().Get("id") @@ -1197,7 +1288,13 @@ func main() { return } - if !IsActorLocal(db, TP + "" + actorDomain[1] + "/" + actorDomain[0]) { + if actorDomain[0] == "main" { + actorDomain[0] = "" + } else { + actorDomain[0] = "/" + actorDomain[0] + } + + if !IsActorLocal(db, TP + "" + actorDomain[1] + "" + actorDomain[0]) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("actor not local")) return @@ -1209,7 +1306,7 @@ func main() { finger.Subject = "acct:" + actorDomain[0] + "@" + actorDomain[1] link.Rel = "self" link.Type = "application/activity+json" - link.Href = TP + "" + actorDomain[1] + "/" + actorDomain[0] + link.Href = TP + "" + actorDomain[1] + "" + actorDomain[0] finger.Links = append(finger.Links, link) @@ -1369,7 +1466,6 @@ func CreateTripCode(input string) string { return code[0] } - func GetActorFromPath(db *sql.DB, location string, prefix string) Actor { pattern := fmt.Sprintf("%s([^/\n]+)(/.+)?", prefix) re := regexp.MustCompile(pattern) @@ -1639,9 +1735,9 @@ func CreateAttachmentObject(file multipart.File, header *multipart.FileHeader) ( return nAttachment, tempFile } -func ParseCommentForReplies(comment string) []ObjectBase { - - re := regexp.MustCompile("(>>)(https://|http://)?(www\\.)?.+\\/\\w+") +func ParseCommentForReplies(db *sql.DB, comment string, op string) []ObjectBase { + + 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 @@ -1652,7 +1748,8 @@ func ParseCommentForReplies(comment string) []ObjectBase { str = strings.Replace(str, "http://", "", 1) str = strings.Replace(str, "https://", "", 1) str = TP + "" + str - if !IsInStringArray(links, str) { + _ , isReply := IsReplyToOP(db, op, str) + if !IsInStringArray(links, str) && isReply { links = append(links, str) } } @@ -1721,26 +1818,34 @@ func GetActor(id string) Actor { return respActor } - req, err := http.NewRequest("GET", id, nil) + actor, instance := GetActorInstance(id) - CheckError(err, "error with getting actor req") + if ActorCache[actor + "@" + instance].Id != "" { + respActor = ActorCache[actor + "@" + instance] + } else { + req, err := http.NewRequest("GET", id, nil) - req.Header.Set("Accept", activitystreams) + CheckError(err, "error with getting actor req") - resp, err := RouteProxy(req) + req.Header.Set("Accept", activitystreams) - if err != nil { - fmt.Println("error with getting actor resp " + id) - return respActor - } + resp, err := RouteProxy(req) - defer resp.Body.Close() + if err != nil { + fmt.Println("error with getting actor resp " + id) + return respActor + } - body, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &respActor) - err = json.Unmarshal(body, &respActor) + CheckError(err, "error getting actor from body") - CheckError(err, "error getting actor from body") + ActorCache[actor + "@" + instance] = respActor + } return respActor } @@ -1765,7 +1870,6 @@ func GetActorCollection(collection string) Collection { return nCollection } - defer resp.Body.Close() if resp.StatusCode == 200 { @@ -2076,7 +2180,9 @@ func MakeActivityRequest(db *sql.DB, activity Activity) { _, err = RouteProxy(req) - CheckError(err, "error with sending activity resp to") + if err != nil { + fmt.Println("error with sending activity resp to actor " + instance) + } } } } @@ -2304,7 +2410,7 @@ func UpdateObjectWithPreview(db *sql.DB, id string, preview string) { func ParseCommentForReply(comment string) string { - re := regexp.MustCompile("(>>)(https://|http://)?(www\\.)?.+\\/\\w+") + 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 @@ -2315,7 +2421,7 @@ func ParseCommentForReply(comment string) string { } if(len(links) > 0){ - _, isValid := CheckValidActivity(links[0]) + _, isValid := CheckValidActivity(strings.ReplaceAll(links[0], ">", "")) if(isValid) { return links[0] @@ -2489,21 +2595,30 @@ func CreatedNeededDirectories() { //looks for actor with pattern of board@instance func FingerActor(path string) Actor{ + var nActor Actor + actor, instance := GetActorInstance(path) - r := FingerRequest(actor, instance) + if actor == "" && instance == "" { + return nActor + } - var nActor Actor + if ActorCache[actor + "@" + instance].Id != "" { + nActor = ActorCache[actor + "@" + instance] + } else { + r := FingerRequest(actor, instance) + if r != nil && r.StatusCode == 200 { + defer r.Body.Close() + + body, _ := ioutil.ReadAll(r.Body) - if r != nil && r.StatusCode == 200 { - defer r.Body.Close() - - body, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(body, &nActor) - err := json.Unmarshal(body, &nActor) + CheckError(err, "error getting fingerrequet resp from json body") - CheckError(err, "error getting fingerrequet resp from json body") - } + ActorCache[actor + "@" + instance] = nActor + } + } return nActor } @@ -2561,7 +2676,16 @@ func GetActorInstance(path string) (string, string) { } } - re = regexp.MustCompile(`(https?:\\)?(www)?([\w\d-_.:]+)\/([\w\d-_.]+)(\/([\w\d-_.]+))?`) + re = regexp.MustCompile(`(https?://)(www)?([\w\d-_.:]+)(/|\s+|\r|\r\n)?$`) + mainActor := re.MatchString(path) + if(mainActor) { + match := re.FindStringSubmatch(path) + if(len(match) > 2) { + return "main", match[3] + } + } + + re = regexp.MustCompile(`(https?://)?(www)?([\w\d-_.:]+)\/([\w\d-_.]+)(\/([\w\d-_.]+))?`) httpFormat := re.MatchString(path) if(httpFormat) { @@ -2663,6 +2787,12 @@ func HashMedia(media string) string { return hex.EncodeToString(h.Sum(nil)) } +func HashBytes(media []byte) string { + h:= sha256.New() + h.Write(media) + return hex.EncodeToString(h.Sum(nil)) +} + func RouteImages(w http.ResponseWriter, media string) { req, err := http.NewRequest("GET", MediaHashs[media], nil) @@ -2720,3 +2850,54 @@ func HasValidation(w http.ResponseWriter, r *http.Request, actor Actor) bool { return true } + +func IsReplyToOP(db *sql.DB, op string, link string) (string, bool) { + + if op == link { + return link, true + } + + re := regexp.MustCompile(`f(\w+)\-`) + match := re.FindStringSubmatch(link) + + if len(match) > 0 { + re := regexp.MustCompile(`(.+)\-`) + link = re.ReplaceAllString(link, "") + link = "%" + match[1] + "/" + link + } + + query := `select id from replies where id like $1 and inreplyto=$2` + + rows, err := db.Query(query, link, op) + + CheckError(err, "error selecting in reply to op from db") + + var id string + defer rows.Close() + rows.Next() + rows.Scan(&id) + + if id != "" { + + return id, true + } + + return "", false +} + +func GetReplyOP(db *sql.DB, link string) string { + + query := `select id from replies where id in (select inreplyto from replies where id=$1) and inreplyto=''` + + rows, err := db.Query(query, link) + + CheckError(err, "could not get reply OP from db ") + + var id string + + defer rows.Close() + rows.Next() + rows.Scan(&id) + + return id +} diff --git a/outboxPost.go b/outboxPost.go index 356647e..6d23e23 100644 --- a/outboxPost.go +++ b/outboxPost.go @@ -7,6 +7,7 @@ import _ "github.com/lib/pq" import "encoding/json" import "reflect" import "io/ioutil" +import "mime/multipart" import "os" import "regexp" import "strings" @@ -24,13 +25,21 @@ func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { r.ParseMultipartForm(5 << 20) if(BoardHasAuthType(db, actor.Name, "captcha") && CheckCaptcha(db, r.FormValue("captcha"))) { f, header, _ := r.FormFile("file") + if(header != nil) { + defer f.Close() if(header.Size > (7 << 20)){ w.WriteHeader(http.StatusRequestEntityTooLarge) w.Write([]byte("7MB max file size")) return } + if(IsMediaBanned(db, f)) { + fmt.Println("media banned") + http.Redirect(w, r, Domain, http.StatusSeeOther) + return + } + contentType, _ := GetFileContentType(f) if(!SupportedMIMEType(contentType)) { @@ -39,7 +48,7 @@ func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { return } } - + var nObj = CreateObject("Note") nObj = ObjectFromForm(r, db, nObj) @@ -92,7 +101,7 @@ func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { var rActivity Activity if validActor && validLocalActor { rActivity = AcceptFollow(activity) - SetActorFollowingDB(db, rActivity) + rActivity = SetActorFollowingDB(db, rActivity) MakeActivityRequest(db, activity) } @@ -339,7 +348,6 @@ func ObjectFromForm(r *http.Request, db *sql.DB, obj ObjectBase) ObjectBase { err := cmd.Run() CheckError(err, "error with removing exif data from image") - } obj.Preview = CreatePreviewObject(obj.Attachment[0]) @@ -373,7 +381,7 @@ func ObjectFromForm(r *http.Request, db *sql.DB, obj ObjectBase) ObjectBase { } } - replyingTo := ParseCommentForReplies(r.FormValue("comment")) + replyingTo := ParseCommentForReplies(db, r.FormValue("comment"), originalPost.Id) for _, e := range replyingTo { @@ -557,16 +565,26 @@ func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { MakeActivityRequest(db, response) alreadyFollow := false + alreadyFollowing := false autoSub := GetActorAutoSubscribeDB(db, response.Actor.Id) following := GetActorFollowingDB(db, response.Actor.Id) for _, e := range following { - if e.Id == activity.Actor.Id { + if e.Id == response.Object.Id { alreadyFollow = true } } - if autoSub && !alreadyFollow { + actor := FingerActor(response.Object.Actor) + remoteActorFollowingCol := GetCollectionFromReq(actor.Following) + + for _, e := range remoteActorFollowingCol.Items { + if e.Id == response.Actor.Id { + alreadyFollowing = true + } + } + + if autoSub && !alreadyFollow && alreadyFollowing { followActivity := MakeFollowActivity(db, response.Actor.Id, response.Object.Actor) if FingerActor(response.Object.Actor).Id != "" { @@ -589,7 +607,6 @@ func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { } break } - } func MakeActivityFollowingReq(w http.ResponseWriter, r *http.Request, activity Activity) bool { @@ -617,3 +634,37 @@ func MakeActivityFollowingReq(w http.ResponseWriter, r *http.Request, activity A return false } + +func IsMediaBanned(db *sql.DB, f multipart.File) bool { + f.Seek(0, 0) + + fileBytes := make([]byte, 2048) + + _, err := f.Read(fileBytes) + if err != nil { + fmt.Println("error readin bytes for media ban") + } + + hash := HashBytes(fileBytes) + + f.Seek(0, 0) + + query := `select hash from bannedmedia where hash=$1` + + rows, err := db.Query(query, hash) + + CheckError(err, "could not get hash from banned media in db") + + var h string + + defer rows.Close() + + rows.Next() + rows.Scan(&h) + + if h == hash { + return true + } + + return false +} diff --git a/static/faq.html b/static/faq.html index a087477..fd06956 100644 --- a/static/faq.html +++ b/static/faq.html @@ -24,7 +24,7 @@ <ul> <li><span style="font-weight: bold;">Secure tripcode:</span> Type in your name in the "Name" field, and then a double hash mark (##) which is followed by a password. So for example, typing in <span style="font-weight: bold;">moot##faggot</span> in the "Name" field will result in <span style="color:#117743"><span style="font-weight: bold;">moot</span> !!IefQUZVu/1</span></li> <li><span style="font-weight: bold;">Regular tripcode:</span> Type in your name in the "Name" field, and then a hash mark (#) which is followed by a password. So for example, typing in <span style="font-weight: bold;">moot#faggot</span> in the "Name" field will result in <span style="color:#117743"><span style="font-weight: bold;">moot</span> !UcVghOVf01</span></li> - </ul></p> + </ul></p> <h4 id="quote">How do I quote?</h4> <p>Use the greater-than symbol (>) to quote strings of text. Use double (>>) followed by the URL id of the post you are referencing or click on the unique ID of the post (for example, FIDV40Q2) if you want to reference a post (keep in mind that this will be changed later for better use).</p> @@ -47,20 +47,20 @@ </ul> <h4 id="javascript">Why use JavaScript?</h4> - <p>A version of the frontend with no JavaScript will be made eventually. Current version requires it as it is needed for some basic functionality. There are no external libraries used by the frontend, just basic selection of DOM elements and modifying their styling. <a href="https://github.com/FChannel0/FChannel-Server/pulls">Perhaps (You) could contribute a frontend that uses no JavaScript?</a></p> + <p>A version of the frontend with no JavaScript will be made eventually. Current version requires it as it is needed for some basic functionality. There are no external libraries used by the frontend, just basic selection of DOM elements and modifying their styling. <a href="https://github.com/FChannel0/FChannel-Server/pulls">Perhaps (You) could contribute a frontend that uses no JavaScript?</a></p> <h4 id="seqnum">Why do the posts not have sequential ID numbers?</h4> <p>Sequential ID numbers have run their course, random base36 is better.</p> <h4 id="pubexamp">ActivityPub specific examples</h4> <p>Soon™.</p> - + <h4 id="version">What version is this FChannel instance?</h4> - <p>v0.0.11-release</p> + <p>v0.0.13-release</p> </div> <div style="width: 500px; margin:0 auto; margin-top: 50px; text-align: center;"> <a href="/">[Home]</a><a href="/static/rules.html">[Rules]</a><a href="/static/faq.html">[FAQ]</a> <p>All trademarks and copyrights on this page are owned by their respective parties.</p> - </div> + </div> </body> </html> diff --git a/static/main.html b/static/main.html index 3cb8555..9209ed9 100644 --- a/static/main.html +++ b/static/main.html @@ -53,14 +53,6 @@ color: #789922; } - .reply { - {{ if .Board.Restricted }} - color:#af0a0f; - {{ else }} - color:#000080; - {{ end }} - } - .post { {{ if .Board.Restricted }} background-color: #d5daf0; @@ -85,6 +77,7 @@ <body> <ul style="display: inline; padding:0;"> {{ $l := len .Boards }} + <li style="display: inline;">[<a href="/">Home</a>]</li> {{range $i, $e := .Boards}} {{ if eq (sub $l 1) 0 }} <li style="display: inline;">[ <a href="{{.Location}}">{{$e.Name}} </a>]</li> diff --git a/static/manage.html b/static/manage.html index 2647532..3bd621b 100644 --- a/static/manage.html +++ b/static/manage.html @@ -22,7 +22,7 @@ {{ if .IsLocal }} <div id="following" class="popup-box" style="margin-bottom: 25px; margin-top: 5px; padding: 12px;"> <h4 style="margin: 0; margin-bottom: 5px;">Following</h4> - <a href="/autosubscribe?board={{ .Board.Name }}">{{ if .AutoSubscribe }}[Auto Follow On]{{ else }}[Auto Follow Off]{{ end }}</a> + {{ if .AutoSubscribe }}<a title="Auto Follow is On" href="/autosubscribe?board={{ .Board.Name }}">[Toggle Auto Follow Off]{{ else }}<a title="Auto Follow is Off" href="/autosubscribe?board={{ .Board.Name }}">[Toggle Auto Follow On]{{ end }}</a> <form id="follow-form" action="/{{ .Key }}/{{ .Board.Name }}/follow" method="post" enctype="application/x-www-form-urlencoded" style="margin-top: 5px;"> <input id="follow" name="follow" style="margin-bottom: 5px;" size="35" placeholder="https://fchan.xyz/g"></input> <input type="submit" value="Follow"><br> diff --git a/static/ncatalog.html b/static/ncatalog.html index 68bb01a..c7f5b30 100644 --- a/static/ncatalog.html +++ b/static/ncatalog.html @@ -37,8 +37,8 @@ {{ end }} <div id="hide-{{ .Id }}" style="display: none;">[Hide]</div> <div id="sensitive-{{ .Id }}" style="display: none;"><div style="position: relative; text-align: center;"><img id="sensitive-img-{{ .Id }}" style="float: left; margin-right: 10px; margin-bottom: 10px; max-width: 180px; max-height: 180px;" src="/static/sensitive.png"><div id="sensitive-text-{{ .Id }}" style="width: 170px; position: absolute; margin-top: 75px; padding: 5px; background-color: black; color: white; cursor: default; ">NSFW Content</div></div></div> - <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/"> - <div id="media-{{ .Id }}" style="width:180px;"></div> + <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/{{ short $board.Actor.Outbox .Id}}"> + <div id="media-{{ .Id }}" style="width:180px;"> {{ parseAttachment . true }}</div> </a> <script> media = document.getElementById("media-{{ .Id }}") @@ -61,51 +61,9 @@ sensitive.style = "display: block" media.style = "display: none;" } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "image"){ - var img = document.createElement("img"); - img.style = "max-width: 180px; max-height: 180px;" - img.setAttribute("id", "img") - img.setAttribute("main", "1") - img.setAttribute("enlarge", "0") - img.setAttribute("attachment", "{{ proxy (index .Attachment 0).Href }}") - {{ if .Preview.Href }} - img.setAttribute("src", "{{ proxy .Preview.Href }}") - img.setAttribute("preview", "{{ proxy .Preview.Href }}") - {{ else }} - img.setAttribute("src", "{{ proxy (index .Attachment 0).Href }}") - img.setAttribute("preview", "{{ proxy (index .Attachment 0).Href }}") - {{ end }} - media.appendChild(img) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "audio"){ - var audio = document.createElement("audio") - audio.controls = 'controls' - audio.preload = 'metadata' - audio.src = '{{ proxy (index .Attachment 0).Href }}' - audio.type = '{{ (index .Attachment 0).MediaType }}' - audio.style = "margin-right: 10px; margin-bottom: 10px; max-width: 180px; max-height: 180px;" - audio.innerText = 'Audio is not supported.' - media.appendChild(audio) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "video"){ - var video = document.createElement("video") - video.controls = 'controls' - video.preload = 'metadata' - video.muted = 'muted' - video.src = '{{ proxy (index .Attachment 0).Href }}' - video.type = '{{ (index .Attachment 0).MediaType }}' - video.style = "margin-right: 10px; margin-bottom: 10px; max-width: 180px; max-height: 180px;" - video.innerText = 'Video is not supported.' - media.appendChild(video) - } - - document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor.Id}}", "{{ .Id }}") </script> {{ end }} - <a id="{{ .Id }}-link" href="/{{ $board.Name }}/"> + <a id="{{ .Id }}-link" href="/{{ $board.Name }}/{{ short $board.Actor.Outbox .Id }}"> <div> {{ $replies := .Replies }} {{ if $replies }} @@ -121,9 +79,6 @@ </div> </a> </div> - <script> - document.getElementById("{{ .Id }}-link").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor.Id}}", "{{ .Id }}") - </script> {{ end }} </div> <hr> diff --git a/static/npost.html b/static/npost.html index 3df4090..419a1a9 100644 --- a/static/npost.html +++ b/static/npost.html @@ -17,7 +17,6 @@ {{ end }} <script src="/static/js/posts.js"></script> -<script src="/static/js/timer.js"></script> {{ end }} {{ define "content" }} @@ -52,6 +51,7 @@ {{ define "script" }} <script src="/static/js/footerscript.js"></script> +<script src="/static/js/timer.js"></script> <script> viewLink("{{ .Board.Name }}", "{{ .Board.Actor.Id }}") </script> diff --git a/static/posts.html b/static/posts.html index d16f780..67a755c 100644 --- a/static/posts.html +++ b/static/posts.html @@ -7,22 +7,22 @@ <hr> {{ end }} <div style="overflow: auto;"> - <div id="{{ .Id }}" style="overflow: visible; margin-bottom: 12px;"> + <div id="{{ short $board.Actor.Outbox .Id }}" style="overflow: visible; margin-bottom: 12px;"> {{ if eq $board.ModCred $board.Domain $board.Actor.Id }} <a href="/delete?id={{ .Id }}&board={{ $board.Actor.Name }}">[Delete Post]</a> {{ end }} {{ if .Attachment }} - {{ if eq $board.ModCred $board.Domain $board.Actor.Id }} + {{ if eq $board.ModCred $board.Domain $board.Actor.Id }} + <a href="/banmedia?id={{ .Id }}&board={{ $board.Actor.Name }}">[Ban Media]</a> <a href="/deleteattach?id={{ .Id }}&board={{ $board.Actor.Name }}">[Delete Attachment]</a> <a href="/marksensitive?id={{ .Id }}&board={{ $board.Actor.Name }}">[Mark Sensitive]</a> {{ end }} <span style="display: block;">File: <a id="{{ .Id }}-img" href="{{ proxy (index .Attachment 0).Href}}">{{ (index .Attachment 0).Name }}</a><span id="{{ .Id }}-size">({{ (index .Attachment 0).Size }})</span></span> <div id="hide-{{ .Id }}" style="display: none;">[Hide]</div> <div id="sensitive-{{ .Id }}" style="display: none;"><div style="position: relative; text-align: center;"><img id="sensitive-img-{{ .Id }}" style="float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" src="/static/sensitive.png"><div id="sensitive-text-{{ .Id }}" style="width: 240px; position: absolute; margin-top: 110px; padding: 5px; background-color: black; color: white; cursor: default; ">NSFW Content</div></div></div> - <div id="media-{{ .Id }}"></div> + <div id="media-{{ .Id }}">{{ parseAttachment . false }}</div> <script> media = document.getElementById("media-{{ .Id }}") - if(({{ .Sensitive }} && {{ $board.Actor.Restricted }}) || (isOnion("{{ .Id }}") && !isOnion("{{ $board.Domain }}"))){ sensitive = document.getElementById("sensitive-{{ .Id }}") hide = document.getElementById("hide-{{ .Id }}") @@ -42,50 +42,10 @@ sensitive.style = "display: block" media.style = "display: none;" } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "image"){ - var img = document.createElement("img"); - img.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - img.setAttribute("id", "img") - img.setAttribute("main", "1") - img.setAttribute("enlarge", "0") - img.setAttribute("attachment", "{{ proxy (index .Attachment 0).Href }}") - {{ if .Preview.Href }} - img.setAttribute("src", "{{ proxy .Preview.Href }}") - img.setAttribute("preview", "{{ proxy .Preview.Href }}") - {{ else }} - img.setAttribute("src", "{{ proxy (index .Attachment 0).Href }}") - img.setAttribute("preview", "{{ proxy (index .Attachment 0).Href }}") - {{ end }} - media.appendChild(img) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "audio"){ - var audio = document.createElement("audio") - audio.controls = 'controls' - audio.preload = 'metadata' - audio.src = '{{ proxy (index .Attachment 0).Href }}' - audio.type = '{{ (index .Attachment 0).MediaType }}' - audio.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - audio.innerText = 'Audio is not supported.' - media.appendChild(audio) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "video"){ - var video = document.createElement("video") - video.controls = 'controls' - video.preload = 'metadata' - video.muted = 'muted' - video.src = '{{ proxy (index .Attachment 0).Href }}' - video.type = '{{ (index .Attachment 0).MediaType }}' - video.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - video.innerText = 'Video is not supported.' - media.appendChild(video) - } </script> {{ end }} - <span style="color: #0f0c5d;"><b>{{ .Name }}</b></span><span style="color: #117743;"><b>{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}</b></span><span class="tripcode"> {{ .TripCode }} </span><span>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/">No.</a> <a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor.Id }}', '{{ $opId }}', '{{ .Id }}')">{{ .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="javascript:report('{{ $board.Actor.Id }}', '{{ .Id }}')">[Report]</a>{{ end }}</span> - <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.Content}}</p> + <span style="color: #0f0c5d;"><b>{{ .Name }}</b></span><span style="color: #117743;"><b>{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}</b></span><span class="tripcode"> {{ .TripCode }} </span><span>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/{{ short $board.Actor.Outbox $opId }}#{{ short $board.Actor.Outbox .Id }}">No.</a> <a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor.Id }}', '{{ $opId }}', '{{ .Id }}')">{{ short $board.Actor.Outbox .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="javascript:report('{{ $board.Actor.Id }}', '{{ .Id }}')">[Report]</a>{{ end }}</span> + <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.ContentHTML}}</p> {{ if .Replies }} {{ $replies := .Replies }} {{ if gt $replies.TotalItems 5 }} @@ -94,7 +54,7 @@ {{ end }} {{ end }} {{ range $replies.OrderedItems }} - <div id="{{ .Id }}"> + <div id="{{ short $board.Actor.Outbox .Id }}"> <div style="display: inline-block; overflow: auto;"> <div style="float: left; display: block; margin-right: 5px;">>></div> <div class="post" style="overflow: auto; padding: 5px; margin-bottom: 2px;"> @@ -102,15 +62,16 @@ <a href="/delete?id={{ .Id }}&board={{ $board.Actor.Name }}">[Delete Post]</a> {{ end }} {{ if .Attachment }} - {{ if eq $board.ModCred $board.Domain $board.Actor.Id }} + {{ if eq $board.ModCred $board.Domain $board.Actor.Id }} + <a href="/banmedia?id={{ .Id }}&board={{ $board.Actor.Name }}">[Ban Media]</a> <a href="/deleteattach?id={{ .Id }}&board={{ $board.Actor.Name }}">[Delete Attachment]</a> - <a href="/marksensitive?id={{ .Id }}&board={{ $board.Actor.Name }}">[Mark Sensitive]</a> + <a href="/marksensitive?id={{ .Id }}&board={{ $board.Actor.Name }}">[Mark Sensitive]</a> {{ end }} <span style="display: block;">File <a id="{{ .Id }}-img" href="{{ proxy (index .Attachment 0).Href}}">{{ (index .Attachment 0).Name }}</a> <span id="{{ .Id }}-size">({{ (index .Attachment 0).Size }})</span></span> <div id="hide-{{ .Id }}" style="display: none;">[Hide]</div> <div id="sensitive-{{ .Id }}" style="display: none;"><div style="position: relative; text-align: center;"><img id="sensitive-img-{{ .Id }}" style="float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" src="/static/sensitive.png"><div id="sensitive-text-{{ .Id }}" style="width: 240px; position: absolute; margin-top: 110px; padding: 5px; background-color: black; color: white; cursor: default; ">NSFW Content</div></div></div> <div> </div> - <div id="media-{{ .Id }}" sensitive="0"></div> + <div id="media-{{ .Id }}" sensitive="0">{{ parseAttachment . false }}</div> <script> media = document.getElementById("media-{{ .Id }}") @@ -133,61 +94,16 @@ sensitive.style = "display: block" media.style = "display: none;" } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "image"){ - var img = document.createElement("img"); - img.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - img.setAttribute("id", "img") - img.setAttribute("main", "1") - img.setAttribute("enlarge", "0") - img.setAttribute("attachment", "{{ proxy (index .Attachment 0).Href }}") - img.setAttribute("post", "{{ .Id }}") - {{ if and .Preview.Href . }} - img.setAttribute("src", "{{ proxy .Preview.Href }}") - img.setAttribute("preview", "{{ proxy .Preview.Href }}") - {{ else }} - img.setAttribute("src", "{{ proxy (index .Attachment 0).Href }}") - img.setAttribute("preview", "{{ proxy (index .Attachment 0).Href }}") - {{ end }} - media.appendChild(img) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "audio"){ - var audio = document.createElement("audio") - audio.controls = 'controls' - audio.preload = 'metadata' - audio.src = '{{ proxy (index .Attachment 0).Href }}' - audio.type = '{{ (index .Attachment 0).MediaType }}' - audio.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - audio.innerText = 'Audio is not supported.' - media.appendChild(audio) - } - - if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "video"){ - var video = document.createElement("video") - video.controls = 'controls' - video.preload = 'metadata' - video.muted = 'muted' - video.src = '{{ proxy (index .Attachment 0).Href }}' - video.type = '{{ (index .Attachment 0).MediaType }}' - video.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 250px; max-height: 250px;" - video.innerText = 'Video is not supported.' - media.appendChild(video) - } </script> {{ end }} - <span style="color: #0f0c5d;"><b>{{ .Name }}</b></span><span style="color: #117743;"><b>{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}</b></span><span class="tripcode"> {{ .TripCode }} </span><span>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/post/{{ $opId }}#{{ .Id }}">No. </a><a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor.Id }}', '{{ $opId }}', '{{ .Id }}')">{{ .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="javascript:report('{{ $board.Actor.Id }}', '{{ .Id }}')">[Report]</a>{{ end }}</span> + <span style="color: #0f0c5d;"><b>{{ .Name }}</b></span><span style="color: #117743;"><b>{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}</b></span><span class="tripcode"> {{ .TripCode }} </span><span>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/{{ short $board.Actor.Outbox $opId }}#{{ short $board.Actor.Outbox .Id }}">No. </a><a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor.Id }}', '{{ $opId }}', '{{ .Id }}')">{{ short $board.Actor.Outbox .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="javascript:report('{{ $board.Actor.Id }}', '{{ .Id }}')">[Report]</a>{{ end }}</span> {{ $parentId := .Id }} {{ if .Replies.OrderedItems }} {{ range .Replies.OrderedItems }} - <span id="{{$parentId}}-replyto-{{.Id}}"></span> - <script> - var content = convertContentNoLink('{{$board.Actor.Id}}', '{{ .Content }}', '{{ $opId }}') - document.getElementById("{{ $parentId }}-replyto-{{.Id}}").innerHTML = "<a title='" + content +"' href='/{{ $board.Name }}/" + shortURL("{{ $board.Actor.Id }}", "{{ $opId }}") + "#" + shortURL("{{ $board.Actor.Id }}", "{{ .Id }}") + "'>>>" + shortURL("{{ $board.Actor.Id }}", "{{ .Id }}") + "</a>"; - </script> + <span id="{{$parentId}}-replyto-{{.Id}}">{{ parseReplyLink $board.Actor.Id $opId .Id .Content }}</span> {{ end }} {{ end }} - <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.Content}}</p> + <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.ContentHTML}}</p> </div> </div> </div> @@ -196,17 +112,6 @@ document.getElementById("{{ .Id }}-size").innerText = " (" + convertSize({{ (index .Attachment 0).Size }}) + ")"; document.getElementById("{{ .Id }}-img").innerText = shortImg("{{ (index .Attachment 0).Name }}"); {{ end }} - - document.getElementById("{{ .Id }}-link").innerText = shortURL("{{ $board.Actor.Id }}", "{{ .Id }}"); - - document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor.Id}}", "{{ $opId }}") + - "#" + shortURL("{{$board.Actor.Id}}", "{{ .Id }}"); - document.getElementById("{{ .Id }}").setAttribute("id", shortURL("{{$board.Actor.Id}}", "{{ .Id }}")); - - var content = document.getElementById("{{ .Id }}-content"); - - content.innerHTML = convertContent('{{$board.Actor.Id}}', content.innerText, '{{ $opId }}') - </script> {{ end }} {{ end }} @@ -217,17 +122,6 @@ document.getElementById("{{ .Id }}-size").innerText = " (" + convertSize({{ (index .Attachment 0).Size }}) + ")"; document.getElementById("{{ .Id }}-img").innerText = shortImg("{{ (index .Attachment 0).Name }}"); {{ end }} - - document.getElementById("{{ .Id }}-link").innerText = shortURL("{{ $board.Actor.Id }}", "{{ .Id }}"); - - document.getElementById("{{ .Id }}").setAttribute("id", shortURL("{{ $board.Actor.Id }}", "{{ .Id }}")); - - document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor.Id}}", "{{ $opId }}") + - "#" + shortURL("{{$board.Actor.Id}}", "{{ .Id }}"); - - var content = document.getElementById("{{ .Id }}-content"); - - content.innerHTML = convertContent('{{$board.Actor.Id}}', content.innerText, '{{ $opId }}') </script> {{ end }} {{ end }} |