aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFChannel <=>2021-01-22 21:12:26 -0800
committerFChannel <=>2021-01-22 21:12:26 -0800
commit2566e314e8351ffbceae9a2688d90a5c811d49e5 (patch)
treee802622593c540265e464beecf76696b65e229ed
parent626fee35bacd68baf6e9bb38ca71d88fff700b13 (diff)
client to server conversion
-rw-r--r--Follow.go4
-rw-r--r--OutboxPost.go5
-rw-r--r--client.go731
-rw-r--r--clientkey1
-rw-r--r--databaseschema.psql5
-rw-r--r--go.mod6
-rw-r--r--go.sum4
-rw-r--r--main.go735
-rw-r--r--session.go90
-rw-r--r--static/admin.html77
-rw-r--r--static/bottom.html23
-rw-r--r--static/catalog.html189
-rw-r--r--static/index.html14
-rw-r--r--static/js/footerscript.js40
-rw-r--r--static/js/posts.js239
-rw-r--r--static/main.html42
-rw-r--r--static/manage.html81
-rw-r--r--static/nadmin.html76
-rw-r--r--static/ncatalog.html93
-rw-r--r--static/npost.html39
-rw-r--r--static/nposts.html51
-rw-r--r--static/posts.html179
-rw-r--r--static/top.html37
-rw-r--r--static/verify.html17
24 files changed, 2682 insertions, 96 deletions
diff --git a/Follow.go b/Follow.go
index dd5a321..ca281ae 100644
--- a/Follow.go
+++ b/Follow.go
@@ -14,7 +14,7 @@ func GetActorFollowing(w http.ResponseWriter, db *sql.DB, id string) {
following.Items = GetActorFollowingDB(db, id)
enc, _ := json.MarshalIndent(following, "", "\t")
- w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ w.Header().Set("Content-Type", activitystreams)
w.Write(enc)
}
@@ -27,7 +27,7 @@ func GetActorFollowers(w http.ResponseWriter, db *sql.DB, id string) {
following.Items = GetActorFollowDB(db, id)
enc, _ := json.MarshalIndent(following, "", "\t")
- w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ w.Header().Set("Content-Type", activitystreams)
w.Write(enc)
}
diff --git a/OutboxPost.go b/OutboxPost.go
index 84810c9..9fb8710 100644
--- a/OutboxPost.go
+++ b/OutboxPost.go
@@ -66,9 +66,9 @@ func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) {
w.Write([]byte(id))
return
}
-
+
w.WriteHeader(http.StatusForbidden)
- w.Write([]byte("could not authenticate"))
+ w.Write([]byte("captcha could not auth"))
} else {
activity = GetActivityFromJson(r, db)
@@ -528,6 +528,7 @@ func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) {
case "Follow":
for _, e := range activity.To {
+
if GetActorFromDB(db, e).Id != "" {
response := AcceptFollow(activity)
response = SetActorFollowerDB(db, response)
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..976eb31
--- /dev/null
+++ b/client.go
@@ -0,0 +1,731 @@
+package main
+
+import "fmt"
+import "net/http"
+import "html/template"
+import "database/sql"
+import _ "github.com/lib/pq"
+import "strings"
+import "strconv"
+import "math"
+import "sort"
+import "regexp"
+import "io/ioutil"
+import "encoding/json"
+import "os"
+
+var Key *string = new(string)
+
+var Boards *[]ObjectBase = new([]ObjectBase)
+
+type Board struct{
+ Name string
+ Actor string
+ Summary string
+ PrefName string
+ InReplyTo string
+ Location string
+ To string
+ RedirectTo string
+ Captcha string
+ CaptchaCode string
+ ModCred string
+ Domain string
+ TP string
+}
+
+type PageData struct {
+ Title string
+ Message string
+ Board Board
+ Pages []int
+ CurrentPage int
+ TotalPage int
+ Boards []Board
+ Posts []ObjectBase
+ Key string
+}
+
+type AdminPage struct {
+ Title string
+ Board Board
+ Key string
+ Actor string
+ Boards []Board
+ Following []string
+ Followers []string
+ Reported []Report
+ Domain string
+}
+
+type Report struct {
+ ID string
+ Count int
+}
+
+type Removed struct {
+ ID string
+ Type string
+ Board string
+}
+
+func IndexGet(w http.ResponseWriter, r *http.Request, db *sql.DB) {
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/index.html"))
+
+ actor := GetActorFromDB(db, Domain)
+
+ var boardCollection []Board
+
+ for _, e := range *Boards {
+ var board Board
+ boardActor := GetActor(e.Id)
+ board.Name = "/" + boardActor.Name + "/"
+ board.PrefName = boardActor.PreferredUsername
+ board.Location = "/" + boardActor.Name
+ boardCollection = append(boardCollection, board)
+ }
+
+ var data PageData
+ data.Title = "Welcome to " + actor.PreferredUsername
+ data.Message = fmt.Sprintf("This is the client for the image board %s. The current version of the code running the server and client is volatile, expect a bumpy ride for the time being. Get the server and client code here https://github.com/FChannel0", Domain)
+ data.Boards = boardCollection
+ data.Board.Name = ""
+ data.Key = *Key
+ data.Board.Domain = Domain
+ data.Board.ModCred, _ = GetPasswordFromSession(r)
+
+ t.ExecuteTemplate(w, "layout", data)
+}
+
+func OutboxGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection){
+
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/nposts.html", "./static/top.html", "./static/bottom.html", "./static/posts.html"))
+
+ actor := collection.Actor
+
+ postNum := strings.Replace(r.URL.EscapedPath(), "/" + actor.Name + "/", "", 1)
+
+ 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.Id
+ returnData.Board.ModCred, _ = GetPasswordFromSession(r)
+ returnData.Board.Domain = Domain
+ returnData.CurrentPage = page
+
+ returnData.Board.Captcha = GetCaptcha(*actor)
+ returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha)
+
+ returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername
+
+ returnData.Key = *Key
+
+ var mergeCollection Collection
+
+ for _, e := range collection.OrderedItems {
+ if e.Type != "Tombstone" {
+ mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e)
+ }
+ }
+
+ domainURL := GetDomainURL(*actor)
+
+ if domainURL == Domain {
+ followCol := GetObjectsFromFollow(*actor)
+ for _, e := range followCol {
+ if e.Type != "Tombstone" {
+ mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e)
+ }
+ }
+ }
+
+ DeleteRemovedPosts(db, &mergeCollection)
+ DeleteTombstoneReplies(&mergeCollection)
+
+ for i, _ := range mergeCollection.OrderedItems {
+ sort.Sort(ObjectBaseSortAsc(mergeCollection.OrderedItems[i].Replies.OrderedItems))
+ }
+
+ DeleteTombstonePosts(&mergeCollection)
+ sort.Sort(ObjectBaseSortDesc(mergeCollection.OrderedItems))
+
+
+
+ returnData.Boards = GetBoardCollection(db)
+
+ offset := 8
+ start := page * offset
+ for i := 0; i < offset; i++ {
+ length := len(mergeCollection.OrderedItems)
+ current := start + i
+ if(current < length) {
+ returnData.Posts = append(returnData.Posts, mergeCollection.OrderedItems[current])
+ }
+ }
+
+
+ for i, e := range returnData.Posts {
+ var replies []ObjectBase
+ for i := 0; i < 5; i++ {
+ cur := len(e.Replies.OrderedItems) - i - 1
+ if cur > -1 {
+ replies = append(replies, e.Replies.OrderedItems[cur])
+ }
+ }
+
+ var orderedReplies []ObjectBase
+ for i := 0; i < 5; i++ {
+ cur := len(replies) - i - 1
+ if cur > -1 {
+ orderedReplies = append(orderedReplies, replies[cur])
+ }
+ }
+ returnData.Posts[i].Replies.OrderedItems = orderedReplies
+ }
+
+ var pages []int
+ pageLimit := math.Round(float64(len(mergeCollection.OrderedItems) / offset))
+ for i := 0.0; i <= pageLimit; i++ {
+ pages = append(pages, int(i))
+ }
+
+ returnData.Pages = pages
+ returnData.TotalPage = len(returnData.Pages) - 1
+
+ t.ExecuteTemplate(w, "layout", returnData)
+}
+
+func CatalogGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection){
+
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/ncatalog.html", "./static/top.html"))
+
+ actor := collection.Actor
+
+ var mergeCollection Collection
+
+ for _, e := range collection.OrderedItems {
+ mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e)
+ }
+
+ domainURL := GetDomainURL(*actor)
+
+ if domainURL == Domain {
+ followCol := GetObjectsFromFollow(*actor)
+ for _, e := range followCol {
+
+ mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e)
+ }
+ }
+
+ DeleteRemovedPosts(db, &mergeCollection)
+ DeleteTombstonePosts(&mergeCollection)
+
+ sort.Sort(ObjectBaseSortDesc(mergeCollection.OrderedItems))
+
+ 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.Id
+ returnData.Board.Summary = actor.Summary
+ returnData.Board.ModCred, _ = GetPasswordFromSession(r)
+ returnData.Board.Domain = Domain
+ returnData.Key = *Key
+
+ returnData.Board.Captcha = GetCaptcha(*actor)
+ returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha)
+
+ returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername
+
+ returnData.Boards = GetBoardCollection(db)
+
+ returnData.Posts = mergeCollection.OrderedItems
+
+ t.ExecuteTemplate(w, "layout", returnData)
+}
+
+func PostGet(w http.ResponseWriter, r *http.Request, db *sql.DB){
+
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/npost.html", "./static/top.html", "./static/bottom.html", "./static/posts.html"))
+
+ path := r.URL.Path
+ actor := GetActorFromPath(db, path, "/")
+ re := regexp.MustCompile("\\w+$")
+ postId := re.FindString(path)
+
+ 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.Id
+ returnData.Board.Summary = actor.Summary
+ returnData.Board.ModCred, _ = GetPasswordFromSession(r)
+ returnData.Board.Domain = Domain
+
+
+ if GetDomainURL(actor) != "" {
+ returnData.Board.Captcha = GetCaptcha(actor)
+ returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha)
+ }
+
+ returnData.Title = "/" + returnData.Board.Name + "/ - " + returnData.Board.PrefName
+
+ returnData.Key = *Key
+
+ returnData.Boards = GetBoardCollection(db)
+
+ re = regexp.MustCompile("f\\w+-\\w+")
+
+ if re.MatchString(path) {
+ name := GetActorFollowNameFromPath(path)
+ followActors := GetActorsFollowFromName(actor, name)
+ followCollection := GetActorsFollowPostFromId(followActors, postId)
+
+ DeleteRemovedPosts(db, &followCollection)
+ DeleteTombstoneReplies(&followCollection)
+
+ for i, _ := range followCollection.OrderedItems {
+ sort.Sort(ObjectBaseSortAsc(followCollection.OrderedItems[i].Replies.OrderedItems))
+ }
+
+ if len(followCollection.OrderedItems) > 0 {
+ returnData.Board.InReplyTo = followCollection.OrderedItems[0].Id
+ returnData.Posts = append(returnData.Posts, followCollection.OrderedItems[0])
+ sort.Sort(ObjectBaseSortAsc(returnData.Posts[0].Replies.OrderedItems))
+ }
+
+ } else {
+
+ returnData.Board.InReplyTo = inReplyTo
+ collection := GetActorCollection(inReplyTo)
+
+ DeleteRemovedPosts(db, &collection)
+
+ for i, e := range collection.OrderedItems {
+ var replies CollectionBase
+ for _, k := range e.Replies.OrderedItems {
+ if k.Type != "Tombstone" {
+ replies.OrderedItems = append(replies.OrderedItems, k)
+ } else {
+ collection.OrderedItems[i].Replies.TotalItems = collection.OrderedItems[i].Replies.TotalItems - 1
+ if k.Preview.Id != "" {
+ collection.OrderedItems[i].Replies.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs - 1
+ }
+ }
+ }
+ collection.TotalItems = collection.OrderedItems[i].Replies.TotalItems
+ collection.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs
+ collection.OrderedItems[i].Replies = &replies
+ sort.Sort(ObjectBaseSortAsc(e.Replies.OrderedItems))
+ }
+
+ if len(collection.OrderedItems) > 0 {
+ returnData.Posts = append(returnData.Posts, collection.OrderedItems[0])
+ sort.Sort(ObjectBaseSortAsc(returnData.Posts[0].Replies.OrderedItems))
+ }
+ }
+
+ t.ExecuteTemplate(w, "layout", returnData)
+}
+
+func GetRemoteActor(id string) Actor {
+
+ var respActor Actor
+
+ id = StripTransferProtocol(id)
+
+ req, err := http.NewRequest("GET", "http://" + id, nil)
+
+ CheckError(err, "error with getting actor req")
+
+ req.Header.Set("Accept", activitystreams)
+
+ resp, err := http.DefaultClient.Do(req)
+
+ if err != nil || resp.StatusCode != 200 {
+ fmt.Println("could not get actor from " + id)
+ return respActor
+ }
+
+ defer resp.Body.Close()
+
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ err = json.Unmarshal(body, &respActor)
+
+ CheckError(err, "error getting actor from body")
+
+ return respActor
+}
+
+func GetBoardCollection(db *sql.DB) []Board {
+ var collection []Board
+ for _, e := range *Boards {
+ var board Board
+ boardActor := GetActorFromDB(db, e.Id)
+ if boardActor.Id == "" {
+ boardActor = GetRemoteActor(e.Id)
+ }
+ board.Name = "/" + boardActor.Name + "/"
+ board.PrefName = boardActor.PreferredUsername
+ board.Location = "/" + boardActor.Name
+ collection = append(collection, board)
+ }
+
+ return collection
+}
+
+func WantToServe(db *sql.DB, actorName string) (Collection, bool) {
+
+ var collection Collection
+ serve := false
+
+ for _, e := range *Boards {
+ boardActor := GetActorFromDB(db, e.Id)
+
+ if boardActor.Id == "" {
+ boardActor = GetActor(e.Id)
+ }
+
+ if boardActor.Id == actorName {
+ serve = true
+ collection = GetActorCollection(boardActor.Outbox)
+ collection.Actor = &boardActor
+ }
+ }
+
+ return collection, serve
+}
+
+func StripTransferProtocol(value string) string {
+ re := regexp.MustCompile("(http://|https://)?(www.)?")
+
+ value = re.ReplaceAllString(value, "")
+
+ return value
+}
+
+func GetCaptcha(actor Actor) string {
+ re := regexp.MustCompile("(https://|http://)?(www)?.+/")
+
+ domainURL := re.FindString(actor.Id)
+
+ re = regexp.MustCompile("/$")
+
+ domainURL = re.ReplaceAllString(domainURL, "")
+
+ resp, err := http.Get(domainURL + "/getcaptcha")
+
+ CheckError(err, "error getting captcha")
+
+ defer resp.Body.Close()
+
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ return domainURL + "/" + string(body)
+}
+
+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 GetDomainURL(actor Actor) string {
+ re := regexp.MustCompile("(https://|http://)?(www)?.+/")
+
+ domainURL := re.FindString(actor.Id)
+
+ re = regexp.MustCompile("/$")
+
+ domainURL = re.ReplaceAllString(domainURL, "")
+
+ return domainURL
+}
+
+func DeleteTombstoneReplies(collection *Collection) {
+
+ for i, e := range collection.OrderedItems {
+ var replies CollectionBase
+ for _, k := range e.Replies.OrderedItems {
+ if k.Type != "Tombstone" {
+ replies.OrderedItems = append(replies.OrderedItems, k)
+ }
+ }
+
+ replies.TotalItems = collection.OrderedItems[i].Replies.TotalItems
+ replies.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs
+ collection.OrderedItems[i].Replies = &replies
+ }
+}
+
+func DeleteTombstonePosts(collection *Collection) {
+ var nColl Collection
+
+ for _, e := range collection.OrderedItems {
+ if e.Type != "Tombstone" {
+ nColl.OrderedItems = append(nColl.OrderedItems, e)
+ }
+ }
+ collection.OrderedItems = nColl.OrderedItems
+}
+
+func DeleteRemovedPosts(db *sql.DB, collection *Collection) {
+
+ removed := GetLocalDeleteDB(db)
+
+ for p, e := range collection.OrderedItems {
+ for _, j := range removed {
+ if e.Id == j.ID {
+ if j.Type == "attachment" {
+ collection.OrderedItems[p].Preview.Href = "/public/removed.png"
+ collection.OrderedItems[p].Preview.Name = "deleted"
+ collection.OrderedItems[p].Preview.MediaType = "image/png"
+ collection.OrderedItems[p].Attachment[0].Href = "/public/removed.png"
+ collection.OrderedItems[p].Attachment[0].Name = "deleted"
+ collection.OrderedItems[p].Attachment[0].MediaType = "image/png"
+ } else {
+ collection.OrderedItems[p].AttributedTo = "deleted"
+ collection.OrderedItems[p].Content = ""
+ collection.OrderedItems[p].Type = "Tombstone"
+ if collection.OrderedItems[p].Attachment != nil {
+ collection.OrderedItems[p].Preview.Href = "/public/removed.png"
+ collection.OrderedItems[p].Preview.Name = "deleted"
+ collection.OrderedItems[p].Preview.MediaType = "image/png"
+ collection.OrderedItems[p].Attachment[0].Href = "/public/removed.png"
+ collection.OrderedItems[p].Attachment[0].Name = "deleted"
+ collection.OrderedItems[p].Attachment[0].MediaType = "image/png"
+ }
+ }
+ }
+ }
+
+ for i, r := range e.Replies.OrderedItems {
+ for _, k := range removed {
+ if r.Id == k.ID {
+ if k.Type == "attachment" {
+ e.Replies.OrderedItems[i].Preview.Href = "/public/removed.png"
+ e.Replies.OrderedItems[i].Preview.Name = "deleted"
+ e.Replies.OrderedItems[i].Preview.MediaType = "image/png"
+ e.Replies.OrderedItems[i].Attachment[0].Href = "/public/removed.png"
+ e.Replies.OrderedItems[i].Attachment[0].Name = "deleted"
+ e.Replies.OrderedItems[i].Attachment[0].MediaType = "image/png"
+ collection.OrderedItems[p].Replies.TotalImgs = collection.OrderedItems[p].Replies.TotalImgs - 1
+ } else {
+ e.Replies.OrderedItems[i].AttributedTo = "deleted"
+ e.Replies.OrderedItems[i].Content = ""
+ e.Replies.OrderedItems[i].Type = "Tombstone"
+ if e.Replies.OrderedItems[i].Attachment != nil {
+ e.Replies.OrderedItems[i].Preview.Href = "/public/removed.png"
+ e.Replies.OrderedItems[i].Preview.Name = "deleted"
+ e.Replies.OrderedItems[i].Preview.MediaType = "image/png"
+ e.Replies.OrderedItems[i].Attachment[0].Name = "deleted"
+ e.Replies.OrderedItems[i].Attachment[0].Href = "/public/removed.png"
+ e.Replies.OrderedItems[i].Attachment[0].MediaType = "image/png"
+ collection.OrderedItems[p].Replies.TotalImgs = collection.OrderedItems[p].Replies.TotalImgs - 1
+ }
+ collection.OrderedItems[p].Replies.TotalItems = collection.OrderedItems[p].Replies.TotalItems - 1
+ }
+ }
+ }
+ }
+ }
+}
+
+func CreateLocalDeleteDB(db *sql.DB, id string, _type string) {
+ query := fmt.Sprintf("select id from removed where id='%s'", id)
+
+ rows, err := db.Query(query)
+
+ CheckError(err, "could not query removed")
+
+ defer rows.Close()
+
+ if rows.Next() {
+ var i string
+
+ rows.Scan(&i)
+
+ if i != "" {
+ query := fmt.Sprintf("update removed set type='%s' where id='%s'", _type, id)
+
+ _, err := db.Exec(query)
+
+ CheckError(err, "Could not update removed post")
+
+ }
+ } else {
+ query := fmt.Sprintf("insert into removed (id, type) values ('%s', '%s')", id, _type)
+
+ _, err := db.Exec(query)
+
+ CheckError(err, "Could not insert removed post")
+ }
+}
+
+func GetLocalDeleteDB(db *sql.DB) []Removed {
+ var deleted []Removed
+
+ query := fmt.Sprintf("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) {
+ query := fmt.Sprintf("select id, count from reported where id='%s' and board='%s'", id, board)
+
+ rows, err := db.Query(query)
+
+ 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 := fmt.Sprintf("update reported set count='%d' where id='%s'", count, id)
+
+ _, err := db.Exec(query)
+
+ CheckError(err, "Could not update reported post")
+ }
+ } else {
+ query := fmt.Sprintf("insert into reported (id, count, board) values ('%s', '%d', '%s')", id, 1, board)
+
+ _, err := db.Exec(query)
+
+ CheckError(err, "Could not insert reported post")
+ }
+
+}
+
+func GetLocalReportDB(db *sql.DB, board string) []Report {
+ var reported []Report
+
+ query := fmt.Sprintf("select id, count from reported where board='%s'", board)
+
+ rows, err := db.Query(query)
+
+ CheckError(err, "could not query reported")
+
+ defer rows.Close()
+
+ for rows.Next() {
+ var r Report
+
+ rows.Scan(&r.ID, &r.Count)
+
+ reported = append(reported, r)
+ }
+
+ return reported
+}
+
+func CloseLocalReportDB(db *sql.DB, id string, board string) {
+ query := fmt.Sprintf("delete from reported where id='%s' and board='%s'", id, board)
+
+ _, err := db.Exec(query)
+
+ 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) []Actor {
+ var followingActors []Actor
+ follow := GetActorCollection(actor.Following)
+
+ re := regexp.MustCompile("\\w+?$")
+
+ for _, e := range follow.Items {
+ if re.FindString(e.Id) == name {
+ actor := GetActor(e.Id)
+ followingActors = append(followingActors, actor)
+ }
+ }
+
+ return followingActors
+}
+
+func GetActorsFollowPostFromId(actors []Actor, id string) Collection{
+ var collection Collection
+
+ for _, e := range actors {
+ tempCol := GetActorCollection(e.Id + "/" + id)
+ if len(tempCol.OrderedItems) > 0 {
+ collection = tempCol
+ }
+ }
+
+ return collection
+}
+
+func CreateClientKey() string{
+
+ file, err := os.Create("clientkey")
+
+ CheckError(err, "could not create client key in file")
+
+ defer file.Close()
+
+ key := CreateKey(32)
+ file.WriteString(key)
+ return key
+}
+
+type ObjectBaseSortDesc []ObjectBase
+func (a ObjectBaseSortDesc) Len() int { return len(a) }
+func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated > 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 < a[j].Published }
+func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
diff --git a/clientkey b/clientkey
new file mode 100644
index 0000000..3438bee
--- /dev/null
+++ b/clientkey
@@ -0,0 +1 @@
+166070629d7087e34b0cce5dadaf0a2d \ No newline at end of file
diff --git a/databaseschema.psql b/databaseschema.psql
index d2544cc..84eaf9d 100644
--- a/databaseschema.psql
+++ b/databaseschema.psql
@@ -204,4 +204,9 @@ formerType varchar(100) default '',
size int default NULL,
public boolean default false,
CONSTRAINT fk_object FOREIGN KEY (object) REFERENCES cacheactivitystream(id)
+);
+
+CREATE TABLE IF NOT EXISTS removed(
+id varchar(100),
+type varchar(25)
); \ No newline at end of file
diff --git a/go.mod b/go.mod
index e5b24f7..f3d9a97 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,7 @@
module github.com/FChannel/Server
-require github.com/lib/pq v1.9.0
+require (
+ github.com/gofrs/uuid v4.0.0+incompatible
+ github.com/gomodule/redigo v2.0.0+incompatible
+ github.com/lib/pq v1.9.0
+)
diff --git a/go.sum b/go.sum
index a4a764e..40d0876 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,6 @@
+github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
diff --git a/main.go b/main.go
index be84a4b..60f5701 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,7 @@ import "net/url"
import "database/sql"
import _ "github.com/lib/pq"
import "math/rand"
+import "html/template"
import "time"
import "regexp"
import "os/exec"
@@ -17,6 +18,8 @@ import "io/ioutil"
import "mime/multipart"
import "os"
import "bufio"
+import "io"
+import "github.com/gofrs/uuid"
var Port = ":" + GetConfigValue("instanceport")
var TP = GetConfigValue("instancetp")
@@ -31,22 +34,29 @@ var SiteEmailPassword = GetConfigValue("emailpass")
var SiteEmailServer = GetConfigValue("emailserver") //mail.fchan.xyz
var SiteEmailPort = GetConfigValue("emailport") //587
-type BoardAccess struct {
- boards []string
-}
+var ldjson = "application/ld+json"
+var activitystreams = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
func main() {
if _, err := os.Stat("./public"); os.IsNotExist(err) {
os.Mkdir("./public", 0755)
- }
+ }
+
+ InitCache()
db := ConnectDB();
defer db.Close()
go MakeCaptchas(db, 100)
+
+ *Key = CreateClientKey()
+
+ following := GetActorFollowingDB(db, Domain)
+ Boards = &following
+
// root actor is used to follow remote feeds that are not local
//name, prefname, summary, auth requirements, restricted
if GetConfigValue("instancename") != "" {
@@ -55,7 +65,10 @@ func main() {
// Allow access to public media folder
fileServer := http.FileServer(http.Dir("./public"))
- http.Handle("/public/", http.StripPrefix("/public", neuter(fileServer)))
+ http.Handle("/public/", http.StripPrefix("/public", neuter(fileServer)))
+
+ javascriptFiles := http.FileServer(http.Dir("./static"))
+ http.Handle("/static/", http.StripPrefix("/static", neuter(javascriptFiles)))
// main routing
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
@@ -79,12 +92,15 @@ func main() {
var actorMain bool
var actorInbox bool
+ var actorCatalog bool
var actorOutbox bool
+ var actorPost bool
var actorFollowing bool
var actorFollowers bool
var actorReported bool
var actorVerification bool
+ var accept = r.Header.Get("Accept")
if(actor.Id != ""){
if actor.Name == "main" {
@@ -96,16 +112,26 @@ func main() {
} else {
actorMain = (path == "/" + actor.Name)
actorInbox = (path == "/" + actor.Name + "/inbox")
+ actorCatalog = (path == "/" + actor.Name + "/catalog")
actorOutbox = (path == "/" + actor.Name + "/outbox")
actorFollowing = (path == "/" + actor.Name + "/following")
actorFollowers = (path == "/" + actor.Name + "/followers")
actorReported = (path == "/" + actor.Name + "/reported")
- actorVerification = (path == "/" + actor.Name + "/verification")
+ actorVerification = (path == "/" + actor.Name + "/verification")
+
+ re := regexp.MustCompile("/" + actor.Name + "/\\w+")
+ actorPost = re.MatchString(path)
}
}
if mainActor {
- GetActorInfo(w, db, Domain)
+ if accept == activitystreams || accept == ldjson {
+ GetActorInfo(w, db, Domain)
+ return
+ }
+
+ IndexGet(w, r, db)
+
return
}
@@ -123,6 +149,7 @@ func main() {
if method == "GET" {
GetActorOutbox(w, r, db)
} else if method == "POST" {
+ fmt.Println("parsing outbox req")
ParseOutboxRequest(w, r, db)
} else {
w.WriteHeader(http.StatusForbidden)
@@ -142,10 +169,29 @@ func main() {
}
if actorMain {
- GetActorInfo(w, db, actor.Id)
+ if accept == activitystreams || accept == ldjson {
+ GetActorInfo(w, db, actor.Id)
+ return
+ }
+
+ collection, valid := WantToServe(db, actor.Id)
+ if valid {
+ OutboxGet(w, r, db, collection)
+ }
+
+ return
+ }
+
+ if actorFollowing {
+ GetActorFollowing(w, db, actor.Id)
return
}
+ if actorFollowers {
+ GetActorFollowers(w, db, actor.Id)
+ return
+ }
+
if actorInbox {
if method == "POST" {
ParseInboxRequest(w, r, db)
@@ -156,6 +202,14 @@ func main() {
return
}
+ if actorCatalog {
+ collection, valid := WantToServe(db, actor.Id)
+ if valid {
+ CatalogGet(w, r, db, collection)
+ }
+ return
+ }
+
if actorOutbox {
if method == "GET" {
GetActorOutbox(w, r, db)
@@ -168,22 +222,12 @@ func main() {
return
}
- if actorFollowing {
- GetActorFollowing(w, db, actor.Id)
- return
- }
-
- if actorFollowers {
- GetActorFollowers(w, db, actor.Id)
- return
- }
-
if actorReported {
GetActorReported(w, r, db, actor.Id)
return
}
- if actorVerification {
+ if actorVerification {
if method == "POST" {
p, _ := url.ParseQuery(r.URL.RawQuery)
if len(p["email"]) > 0 {
@@ -214,109 +258,518 @@ func main() {
}
return
}
+
+ //catch all
+ if actorPost {
+ if accept == activitystreams || accept == ldjson {
+ GetActorPost(w, db, path)
+ return
+ }
+
+ PostGet(w, r, db)
+ return
+ }
+
+ w.WriteHeader(http.StatusForbidden)
+ w.Write([]byte("404 no path"))
+
+ })
+
+ http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request){
+
+ r.ParseMultipartForm(10 << 20)
+
+ file, header, _ := r.FormFile("file")
+
+ if(file != nil && header.Size > (7 << 20)){
+ w.Write([]byte("7MB max file size"))
+ return
+ }
+
+ if(r.FormValue("inReplyTo") == "" || file == nil) {
+ if(r.FormValue("comment") == "" && r.FormValue("subject") == ""){
+ w.Write([]byte("Comment or Subject required"))
+ return
+ }
+ }
+
+ if(r.FormValue("captcha") == "") {
+ w.Write([]byte("Captcha required"))
+ return
+ }
+
+ b := bytes.Buffer{}
+ we := multipart.NewWriter(&b)
+
+ if(file != nil){
+ var fw io.Writer
+
+ fw, err := we.CreateFormFile("file", header.Filename)
+
+ CheckError(err, "error with form file create")
+
+ _, err = io.Copy(fw, file)
+
+ CheckError(err, "error with form file copy")
+ }
+
+ reply := ParseCommentForReply(r.FormValue("comment"))
+
+ for key, r0 := range r.Form {
+ if(key == "captcha") {
+ err := we.WriteField(key, r.FormValue("captchaCode") + ":" + r.FormValue("captcha"))
+ CheckError(err, "error with writing field")
+ }else{
+ err := we.WriteField(key, r0[0])
+ CheckError(err, "error with writing field")
+ }
+ }
+
+ if(r.FormValue("inReplyTo") == "" && reply != ""){
+ err := we.WriteField("inReplyTo", reply)
+ CheckError(err, "error with writing inReplyTo field")
+ }
+
+ we.Close()
+
+ req, err := http.NewRequest("POST", r.FormValue("sendTo"), &b)
+
+ CheckError(err, "error with post form req")
+
+ req.Header.Set("Content-Type", we.FormDataContentType())
+ req.Header.Set("Authorization", "basic: " + *Key)
+
+ resp, err := http.DefaultClient.Do(req)
+
+ CheckError(err, "error with post form resp")
+
+ defer resp.Body.Close()
+
+ if(resp.StatusCode == 200){
+
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ var obj ObjectBase
+ obj = ParseOptions(r, obj)
+ for _, e := range obj.Option {
+ if(e == "noko" || e == "nokosage"){
+ http.Redirect(w, r, TP + "" + Domain + "/" + r.FormValue("boardName") + "/" + string(body) , http.StatusMovedPermanently)
+ break
+ }
+ }
+
+ http.Redirect(w, r, Domain + "/" + r.FormValue("boardName"), http.StatusMovedPermanently)
+ return
+ }
+
+ if(resp.StatusCode == 403){
+ w.Write([]byte("Wrong Captcha"))
+ return
+ }
- collection := GetCollectionFromPath(db, Domain + "" + path)
- if len(collection.OrderedItems) > 0 {
- enc, _ := json.MarshalIndent(collection, "", "\t")
- w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
- w.Write(enc)
+ http.Redirect(w, r, Domain + "/" + r.FormValue("boardName"), http.StatusMovedPermanently)
+ })
+
+ http.HandleFunc("/" + *Key + "/", func(w http.ResponseWriter, r *http.Request) {
+
+ id, _ := GetPasswordFromSession(r)
+
+ actor := GetActorFromPath(db, r.URL.Path, "/" + *Key + "/")
+
+ if actor.Id == "" {
+ actor = GetActorFromDB(db, Domain)
+ }
+
+ if id == "" || (id != actor.Id && id != Domain) {
+ t := template.Must(template.ParseFiles("./static/verify.html"))
+ t.Execute(w, "")
+ return
+ }
+
+ re := regexp.MustCompile("/" + *Key + "/" + actor.Name + "/follow")
+ follow := re.MatchString(r.URL.Path)
+
+ re = regexp.MustCompile("/" + *Key + "/" + actor.Name)
+ manage := re.MatchString(r.URL.Path)
+
+ re = regexp.MustCompile("/" + *Key )
+ admin := re.MatchString(r.URL.Path)
+
+ re = regexp.MustCompile("/" + *Key + "/follow" )
+ adminFollow := re.MatchString(r.URL.Path)
+
+ if follow || adminFollow {
+ r.ParseForm()
+
+ var followActivity Activity
+
+ followActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams"
+ followActivity.Type = "Follow"
+ var nactor Actor
+ var obj ObjectBase
+ followActivity.Actor = &nactor
+ followActivity.Object = &obj
+ followActivity.Actor.Id = r.FormValue("actor")
+ var mactor Actor
+ followActivity.Object.Actor = &mactor
+ followActivity.Object.Actor.Id = r.FormValue("follow")
+ followActivity.To = append(followActivity.To, r.FormValue("follow"))
+
+ enc, _ := json.Marshal(followActivity)
+
+ req, err := http.NewRequest("POST", actor.Outbox, bytes.NewBuffer(enc))
+
+ CheckError(err, "error with follow req")
+
+ _, pass := GetPasswordFromSession(r)
+
+ req.Header.Set("Authorization", "Basic " + pass)
+
+ req.Header.Set("Content-Type", activitystreams)
+
+ _, err = http.DefaultClient.Do(req)
+
+ CheckError(err, "error with add board follow resp")
+
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+
+ } else if manage && actor.Name != "" {
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/manage.html"))
+
+ follow := GetActorCollection(actor.Following)
+ follower := GetActorCollection(actor.Followers)
+ reported := GetActorCollectionReq(r, actor.Id + "/reported")
+
+ var following []string
+ var followers []string
+ var reports []Report
+
+ for _, e := range follow.Items {
+ following = append(following, e.Id)
+ }
+
+ for _, e := range follower.Items {
+ followers = append(followers, e.Id)
+ }
+
+ for _, e := range reported.Items {
+ var r Report
+ r.Count = int(e.Size)
+ r.ID = e.Id
+ reports = append(reports, r)
+ }
+
+ localReports := GetLocalReportDB(db, actor.Name)
+
+ for _, e := range localReports {
+ var r Report
+ r.Count = e.Count
+ r.ID = e.ID
+ reports = append(reports, r)
+ }
+
+ var adminData AdminPage
+ adminData.Following = following
+ adminData.Followers = followers
+ adminData.Reported = reports
+ adminData.Domain = Domain
+
+ var boardCollection []Board
+
+ boardCollection = GetBoardCollection(db)
+
+ adminData.Title = "Manage /" + actor.Name + "/"
+ adminData.Boards = boardCollection
+ adminData.Board.Name = actor.Name
+ adminData.Actor = actor.Id
+ adminData.Key = *Key
+ adminData.Board.TP = TP
+ t.ExecuteTemplate(w, "layout", adminData)
+
+ } else if admin || actor.Id == Domain {
+
+ t := template.Must(template.ParseFiles("./static/main.html", "./static/nadmin.html"))
+
+ actor := GetActor(Domain)
+ follow := GetActorCollection(actor.Following).Items
+ follower := GetActorCollection(actor.Followers).Items
+
+ var following []string
+ var followers []string
+
+ for _, e := range follow {
+ following = append(following, e.Id)
+ }
+
+ for _, e := range follower {
+ followers = append(followers, e.Id)
+ }
+
+ var adminData AdminPage
+ adminData.Following = following
+ adminData.Followers = followers
+ adminData.Actor = actor.Id
+ adminData.Key = *Key
+ adminData.Domain = Domain
+ adminData.Board.ModCred,_ = GetPasswordFromSession(r)
+
+ var boardCollection []Board
+
+ boardCollection = GetBoardCollection(db)
+ adminData.Boards = boardCollection
+
+ t.ExecuteTemplate(w, "layout", adminData)
+ }
+ })
+
+ http.HandleFunc("/" + *Key + "/addboard", func(w http.ResponseWriter, r *http.Request) {
+
+ var newActorActivity Activity
+ var board Actor
+ r.ParseForm()
+
+ actor := GetActorFromDB(db, Domain)
+
+ var restrict bool
+ if r.FormValue("restricted") == "True" {
+ restrict = true
} else {
- w.WriteHeader(http.StatusForbidden)
- w.Write([]byte("404 no path"))
+ restrict = false
}
+
+ board.Name = r.FormValue("name")
+ board.PreferredUsername = r.FormValue("prefname")
+ board.Summary = r.FormValue("summary")
+ board.Restricted = restrict
+
+ newActorActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams"
+ newActorActivity.Type = "New"
+ var nactor Actor
+ var nobj ObjectBase
+ newActorActivity.Actor = &nactor
+ newActorActivity.Object = &nobj
+ newActorActivity.Actor.Id = actor.Id
+ newActorActivity.Object.Actor = &board
+
+
+ enc, _ := json.Marshal(newActorActivity)
+
+ req, err := http.NewRequest("POST", actor.Outbox, bytes.NewBuffer(enc))
+
+ CheckError(err, "error with add board follow req")
+
+ _, pass := GetPasswordFromSession(r)
+ req.Header.Set("Authorization", "Basic " + pass)
+ req.Header.Set("Content-Type", activitystreams)
+
+ resp, err := http.DefaultClient.Do(req)
+
+ CheckError(err, "error with add board follow resp")
+
+ defer resp.Body.Close()
+
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ var respActor Actor
+
+ err = json.Unmarshal(body, &respActor)
+
+ CheckError(err, "error getting actor from body in new board")
+ //update board list with new instances following
+ if resp.StatusCode == 200 {
+ var board []ObjectBase
+ var item ObjectBase
+ var removed bool = false
+
+ item.Id = respActor.Id
+ for _, e := range *Boards {
+ if e.Id != item.Id {
+ board = append(board, e)
+ } else {
+ removed = true
+ }
+ }
+
+ if !removed {
+ board = append(board, item)
+ }
+
+ *Boards = board
+ }
+
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
})
+ http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){
+ if(r.Method == "POST") {
+ r.ParseForm()
+ identifier := r.FormValue("id")
+ code := r.FormValue("code")
+
+ var verify Verify
+ verify.Identifier = identifier
+ verify.Code = code
+
+ j, _ := json.Marshal(&verify)
+
+ req, err := http.NewRequest("POST", Domain + "/auth", bytes.NewBuffer(j))
+
+ CheckError(err, "error making verify req")
+
+ req.Header.Set("Content-Type", activitystreams)
+
+ resp, err := http.DefaultClient.Do(req)
+
+ CheckError(err, "error getting verify resp")
+
+ defer resp.Body.Close()
+
+ rBody, _ := ioutil.ReadAll(resp.Body)
+
+ body := string(rBody)
+
+ if(resp.StatusCode != 200) {
+ t := template.Must(template.ParseFiles("./static/verify.html"))
+ t.Execute(w, "wrong password " + verify.Code)
+ } else {
+
+ sessionToken, _ := uuid.NewV4()
+
+ _, err := cache.Do("SETEX", sessionToken, "86400", body + "|" + verify.Code)
+ if err != nil {
+ t := template.Must(template.ParseFiles("./static/verify.html"))
+ t.Execute(w, "")
+ return
+ }
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "session_token",
+ Value: sessionToken.String(),
+ Expires: time.Now().Add(60 * 60 * 24 * 7 * time.Second),
+ })
+
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ }
+ } else {
+ t := template.Must(template.ParseFiles("./static/verify.html"))
+ t.Execute(w, "")
+ }
+ })
+
http.HandleFunc("/getcaptcha", func(w http.ResponseWriter, r *http.Request){
w.Write([]byte(GetRandomCaptcha(db)))
})
http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request){
- values := r.URL.Query().Get("id")
-
- header := r.Header.Get("Authorization")
-
- auth := strings.Split(header, " ")
+ id := r.URL.Query().Get("id")
+ board := r.URL.Query().Get("board")
+ actor := GetActorFromPath(db, id, "/")
+ _, auth := GetPasswordFromSession(r)
- if len(values) < 1 || !IsIDLocal(db, values) || len(auth) < 2 {
+ if id == "" || auth == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(""))
return
}
- actor := GetActorFromPath(db, values, "/")
-
- if !HasAuth(db, auth[1], actor.Id) {
+ if !HasAuth(db, auth, actor.Id) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(""))
return
- }
+ }
+
+ if !IsIDLocal(db, id) {
+ CreateLocalDeleteDB(db, id, "post")
+ CloseLocalReportDB(db, id, board)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
+ }
+
var obj ObjectBase
- obj.Id = values
+ obj.Id = id
+
+ count, _ := GetObjectRepliesDBCount(db, obj)
- count, _ := GetObjectRepliesDBCount(db, obj)
if count == 0 {
DeleteObject(db, obj.Id)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
} else {
DeleteObjectAndReplies(db, obj.Id)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
}
- w.Write([]byte(""))
+
+
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(""))
})
http.HandleFunc("/deleteattach", func(w http.ResponseWriter, r *http.Request){
- values := r.URL.Query().Get("id")
-
- header := r.Header.Get("Authorization")
+ id := r.URL.Query().Get("id")
- auth := strings.Split(header, " ")
+ _, auth := GetPasswordFromSession(r)
- if len(values) < 1 || !IsIDLocal(db, values) || len(auth) < 2 {
+ if id == "" || auth == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(""))
return
}
- actor := GetActorFromPath(db, values, "/")
+ actor := GetActorFromPath(db, id, "/")
- if !HasAuth(db, auth[1], actor.Id) {
+ if !HasAuth(db, auth, actor.Id) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(""))
return
+ }
+
+ if !IsIDLocal(db, id) {
+ CreateLocalDeleteDB(db, id, "attachment")
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
}
- id := values
DeleteAttachmentFromFile(db, id)
- DeletePreviewFromFile(db, id)
- w.Write([]byte(""))
+ DeletePreviewFromFile(db, id)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
})
http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request){
id := r.URL.Query().Get("id")
close := r.URL.Query().Get("close")
- header := r.Header.Get("Authorization")
+ board := r.URL.Query().Get("board")
+ actor := GetActorFromPath(db, id, "/")
+ _, auth := GetPasswordFromSession(r)
- auth := strings.Split(header, " ")
- if close == "1" {
- if !IsIDLocal(db, id) || len(auth) < 2 {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(""))
- return
- }
+ fmt.Println(actor.Id)
- actor := GetActorFromPath(db, id, "/")
+ if id == "" || auth == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(""))
+ return
+ }
- if !HasAuth(db, auth[1], actor.Id) {
+ if close == "1" {
+ if !HasAuth(db, auth, actor.Id) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(""))
return
}
+ if !IsIDLocal(db, id) {
+ CloseLocalReportDB(db, id, board)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
+ }
+
reported := DeleteReportActivity(db, id)
if reported {
- w.Write([]byte(""))
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
@@ -324,11 +777,17 @@ func main() {
w.Write([]byte(""))
return
}
+
+ if !IsIDLocal(db, id) {
+ fmt.Println("not local")
+ CreateLocalReportDB(db, id, board)
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
+ return
+ }
reported := ReportActivity(db, id)
-
if reported {
- w.Write([]byte(""))
+ http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther)
return
}
@@ -336,7 +795,7 @@ func main() {
w.Write([]byte(""))
})
- http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){
+ http.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request){
var verify Verify
defer r.Body.Close()
@@ -358,7 +817,8 @@ func main() {
})
fmt.Println("Server for " + Domain + " running on port " + Port)
-
+
+ fmt.Println("Mod key: " + *Key)
PrintAdminAuth(db)
http.ListenAndServe(Port, nil)
@@ -543,6 +1003,15 @@ func GetActorInfo(w http.ResponseWriter, db *sql.DB, id string) {
w.Write(enc)
}
+func GetActorPost(w http.ResponseWriter, db *sql.DB, path string) {
+ collection := GetCollectionFromPath(db, Domain + "" + path)
+ if len(collection.OrderedItems) > 0 {
+ enc, _ := json.MarshalIndent(collection, "", "\t")
+ w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ w.Write(enc)
+ }
+}
+
func CreateObject(objType string) ObjectBase {
var nObj ObjectBase
@@ -694,17 +1163,16 @@ func ParseCommentForReplies(comment string) []ObjectBase {
return validLinks
}
-
func CheckValidActivity(id string) (Collection, bool) {
req, err := http.NewRequest("GET", id, nil)
-
+
if err != nil {
fmt.Println("error with request")
panic(err)
}
- req.Header.Set("Accept", "json/application/activity+json")
+ req.Header.Set("Accept", activitystreams)
resp, err := http.DefaultClient.Do(req)
@@ -736,14 +1204,20 @@ func GetActor(id string) Actor {
var respActor Actor
+ if id == "" {
+ return respActor
+ }
+
req, err := http.NewRequest("GET", id, nil)
CheckError(err, "error with getting actor req")
+ req.Header.Set("Accept", activitystreams)
+
resp, err := http.DefaultClient.Do(req)
if err != nil {
- fmt.Println("error with getting actor resp")
+ fmt.Println("error with getting actor resp " + id)
return respActor
}
@@ -761,23 +1235,34 @@ func GetActor(id string) Actor {
func GetActorCollection(collection string) Collection {
var nCollection Collection
+ if collection == "" {
+ return nCollection
+ }
+
req, err := http.NewRequest("GET", collection, nil)
CheckError(err, "error with getting actor collection req " + collection)
+ req.Header.Set("Accept", activitystreams)
+
resp, err := http.DefaultClient.Do(req)
- CheckError(err, "error with getting actor collection resp " + collection)
+ if err != nil {
+ fmt.Println("error with getting actor collection resp " + collection)
+ return nCollection
+ }
- if resp.StatusCode == 200 {
- defer resp.Body.Close()
+ defer resp.Body.Close()
+ if resp.StatusCode == 200 {
body, _ := ioutil.ReadAll(resp.Body)
- err = json.Unmarshal(body, &nCollection)
-
- CheckError(err, "error getting actor collection from body " + collection)
+ if len(body) > 0 {
+ err = json.Unmarshal(body, &nCollection)
+
+ CheckError(err, "error getting actor collection from body " + collection)
+ }
}
return nCollection
@@ -789,18 +1274,18 @@ func IsValidActor(id string) (Actor, bool) {
CheckError(err, "error with valid actor request")
- req.Header.Set("Accept", "json/application/activity+json")
+ req.Header.Set("Accept", activitystreams)
resp, err := http.DefaultClient.Do(req)
CheckError(err, "error with valid actor response")
+ defer resp.Body.Close()
+
if resp.StatusCode == 403 {
return respCollection, false;
}
- defer resp.Body.Close()
-
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &respCollection)
@@ -816,8 +1301,6 @@ func IsValidActor(id string) (Actor, bool) {
return respCollection, false;
}
-
-
func IsActivityLocal(db *sql.DB, activity Activity) bool {
for _, e := range activity.To {
if GetActorFromDB(db, e).Id != "" {
@@ -839,12 +1322,8 @@ func IsActivityLocal(db *sql.DB, activity Activity) bool {
}
func IsIDLocal(db *sql.DB, id string) bool {
-
- if GetActivityFromDB(db, id).OrderedItems != nil {
- return true
- }
-
- return false
+ activity := GetActivityFromDB(db, id)
+ return len(activity.OrderedItems) > 0
}
func IsObjectLocal(db *sql.DB, id string) bool {
@@ -995,7 +1474,7 @@ func GetActorReported(w http.ResponseWriter, r *http.Request, db *sql.DB, id str
following.Items = GetActorReportedDB(db, id)
enc, _ := json.MarshalIndent(following, "", "\t")
- w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ w.Header().Set("Content-Type", activitystreams)
w.Write(enc)
}
@@ -1008,11 +1487,13 @@ func MakeActivityRequest(activity Activity) {
actor := GetActor(e)
if actor.Inbox != "" {
- req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j))
+ req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j))
- CheckError(err, "error with sending activity req to")
+ req.Header.Set("Content-Type", activitystreams)
- _, err = http.DefaultClient.Do(req)
+ CheckError(err, "error with sending activity req to")
+
+ _, err = http.DefaultClient.Do(req)
CheckError(err, "error with sending activity resp to")
}
@@ -1026,6 +1507,8 @@ func GetCollectionFromID(id string) Collection {
CheckError(err, "could not get collection from id req")
+ req.Header.Set("Accept", activitystreams)
+
resp, err := http.DefaultClient.Do(req)
if err != nil {
@@ -1052,6 +1535,8 @@ func GetActorFromID(id string) Actor {
CheckError(err, "error getting actor from id req")
+ req.Header.Set("Accept", activitystreams)
+
resp, err := http.DefaultClient.Do(req)
CheckError(err, "error getting actor from id resp")
@@ -1088,7 +1573,6 @@ func GetConfigValue(value string) string{
return ""
}
-
func PrintAdminAuth(db *sql.DB){
query := fmt.Sprintf("select identifier, code from boardaccess where board='%s' and type='admin'", Domain)
@@ -1114,7 +1598,6 @@ func IsInStringArray(array []string, value string) bool {
return false
}
-
func GetUniqueFilename(_type string) string {
id := RandomID(8)
file := "/public/" + id + "." + _type
@@ -1242,3 +1725,73 @@ func UpdateObjectWithPreview(db *sql.DB, id string, preview string) {
CheckError(err, "could not update activity stream with preview")
}
+
+func ParseCommentForReply(comment string) string {
+
+ re := regexp.MustCompile("(>>)(https://|http://)?(www\\.)?.+\\/\\w+")
+ match := re.FindAllStringSubmatch(comment, -1)
+
+ var links []string
+
+ for i:= 0; i < len(match); i++ {
+ str := strings.Replace(match[i][0], ">>", "", 1)
+ links = append(links, str)
+ }
+
+ if(len(links) > 0){
+ _, isValid := CheckValidActivity(links[0])
+
+ if(isValid) {
+ return links[0]
+ }
+ }
+
+ return ""
+}
+
+func GetActorByName(db *sql.DB, name string) Actor {
+ var actor Actor
+ for _, e := range *Boards {
+ boardActor := GetActorFromDB(db, e.Id)
+ if boardActor.Id == "" {
+ boardActor = GetRemoteActor(e.Id)
+ }
+
+ if boardActor.Name == name {
+ actor = boardActor
+ }
+ }
+
+ return actor
+}
+
+func GetActorCollectionReq(r *http.Request, collection string) Collection {
+ var nCollection Collection
+
+ req, err := http.NewRequest("GET", collection, nil)
+
+ CheckError(err, "error with getting actor collection req " + collection)
+
+ _, pass := GetPasswordFromSession(r)
+
+ req.Header.Set("Accept", activitystreams)
+
+ req.Header.Set("Authorization", "Basic " + pass)
+
+ resp, err := http.DefaultClient.Do(req)
+
+ CheckError(err, "error with getting actor collection resp " + collection)
+
+ if resp.StatusCode == 200 {
+
+ defer resp.Body.Close()
+
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ err = json.Unmarshal(body, &nCollection)
+
+ CheckError(err, "error getting actor collection from body " + collection)
+ }
+
+ return nCollection
+}
diff --git a/session.go b/session.go
new file mode 100644
index 0000000..3478802
--- /dev/null
+++ b/session.go
@@ -0,0 +1,90 @@
+package main
+
+
+import (
+ "fmt"
+ "net/http"
+ "bufio"
+ "os"
+ "strings"
+ "github.com/gomodule/redigo/redis"
+)
+
+var cache redis.Conn
+
+func InitCache() {
+ conn, err := redis.DialURL("redis://localhost")
+ if err != nil {
+ panic(err)
+ }
+ cache = conn
+}
+
+func CheckSession(w http.ResponseWriter, r *http.Request) (interface{}, error){
+
+ c, err := r.Cookie("session_token")
+
+ if err != nil {
+ if err == http.ErrNoCookie {
+ w.WriteHeader(http.StatusUnauthorized)
+ return nil, err
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+ return nil, err
+ }
+
+ sessionToken := c.Value
+
+ response, err := cache.Do("GET", sessionToken)
+
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return nil, err
+ }
+ if response == nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ return nil, err
+ }
+
+ return response, nil
+}
+
+func GetClientKey() string{
+ file, err := os.Open("clientkey")
+
+ CheckError(err, "could not open client key in file")
+
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ var line string
+ for scanner.Scan() {
+ line = fmt.Sprintf("%s", scanner.Text())
+ }
+
+ return line
+}
+
+func GetPasswordFromSession(r *http.Request) (string, string) {
+
+ c, err := r.Cookie("session_token")
+
+ if err != nil {
+ return "", ""
+ }
+
+ sessionToken := c.Value
+
+ response, err := cache.Do("GET", sessionToken)
+
+ if CheckError(err, "could not get session from cache") != nil {
+ return "", ""
+ }
+
+ token := fmt.Sprintf("%s", response)
+
+ parts := strings.Split(token, "|")
+
+ return parts[0], parts[1]
+}
diff --git a/static/admin.html b/static/admin.html
new file mode 100644
index 0000000..f7db70c
--- /dev/null
+++ b/static/admin.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ <div style="margin: 0 auto; width: 400px;">
+ <h3>Add Board</h3>
+ <form id="new-post" action="/{{ .Key }}/addboard" method="post" enctype="application/x-www-form-urlencoded">
+ <label>Name:</label><br>
+ <input type="text" name="name" placeholder="g" required><br>
+ <label>Prefered Name:</label><br>
+ <input type="text" name="prefname" placeholder="Technology" required><input type="submit" value="Add"><br>
+ <label>Summary:</label><br>
+ <textarea name="summary" rows="8" cols="50"></textarea><br>
+ <label>Restricted:</label><br>
+ <select name="restricted">
+ <option value="True">True</option>
+ <option value="False">False</option>
+ </select>
+ </form>
+
+ <ul style="display: inline-block; padding: 0;">
+ <li style="display: inline-block;"><a href="javascript:show('following')">Subscribed</a></li>
+ <!-- <li style="display: inline-block;"><a href="javascript:show('followers')">Followers</a></li> -->
+ <li style="display: inline-block;"><a href="javascript:show('reported')">Reported</a></li>
+ </ul>
+
+ </div>
+
+ <div id="following">
+ <h4>Following</h4>
+ <form id="follow-form" action="/{{ .Key }}/follow" method="post" enctype="application/x-www-form-urlencoded">
+ <label>Subscribe:</label><br>
+ <input id="follow" name="follow" style="margin-bottom: 12px;" placeholder="http://localhost:3000/g"></input><input type="submit" value="Subscribe"><br>
+ <input type="hidden" name="actor" value="{{ .Actor }}">
+ </form>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ $actor := .Actor }}
+ {{ $key := .Key }}
+ {{ range .Following }}
+ <li><a href="/{{ $key }}/follow?follow={{ . }}&actor={{ $actor }}">[Unfollow]</a><a href="{{ . }}">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+ </div>
+
+ <div id="followers" style="display: none;">
+ <h4>Followers</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ range .Followers }}
+ <li><a href="http://localhost:3000/g">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+ </div>
+
+ <div id="reported" style="display: none;">
+ <h4>Reported</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ </ul>
+ </div>
+ </body>
+</html>
+
+<script>
+ function show(element)
+ {
+ var following = document.getElementById("following");
+ // var followers = document.getElementById("followers");
+ var reported = document.getElementById("reported");
+
+ following.style.display = "none";
+ // followers.style.display = "none";
+ reported.style.display = "none";
+
+ document.getElementById(element).style.display = "block";
+ }
+</script>
diff --git a/static/bottom.html b/static/bottom.html
new file mode 100644
index 0000000..0542c41
--- /dev/null
+++ b/static/bottom.html
@@ -0,0 +1,23 @@
+{{ define "bottom" }}
+<div id="reply-box" style="display: none; ">
+ <div id="reply-header" style="display: inline-block; width: 370px; z-index: 0; cursor: move;"></div><div id="reply-close" style="display: inline-block; float: right;"><a href="javascript:closeReply()">[X]</a></div>
+ <form id="reply-post" action="/post" method="post" enctype="multipart/form-data">
+ <input id="reply-name" name="name" size="43" type="text" placeholder="Name">
+ <input id="reply-options" name="options" size="43" type="text" placeholder="Options">
+ <textarea id="reply-comment" name="comment" rows="12" cols="54" style="width: 396px;"></textarea>
+ <input id="reply-file" name="file" type="file">
+ <input id="reply-submit" type="submit" value="Reply" style="float: right;">
+ <input type="hidden" id="inReplyTo-box" name="inReplyTo" value="{{ .Board.InReplyTo }}">
+ <input type="hidden" id="sendTo" name="sendTo" value="{{ .Board.To }}">
+ <input type="hidden" id="boardName" name="boardName" value="{{ .Board.Name }}">
+ <input type="hidden" id="captchaCode" name="captchaCode" value="{{ .Board.CaptchaCode }}">
+ <div style="width: 202px; margin: 0 auto; padding-top: 12px;">
+ <label for="captcha">Captcha:</label><br>
+ <input style="display: inline-block;" type="text" id="captcha" name="captcha" autocomplete="off"><br>
+ </div>
+ <div style="width: 230px; margin: 0 auto;">
+ <img src="{{ .Board.Captcha }}">
+ </div>
+ </form>
+</div>
+{{ end }}
diff --git a/static/catalog.html b/static/catalog.html
new file mode 100644
index 0000000..b5b361e
--- /dev/null
+++ b/static/catalog.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>{{ .Title }}</title>
+ </head>
+ <style>
+ a, a:link, a:visited, a:hover, a:active {
+ text-decoration: none
+ }
+
+ a:link, a:visited, a:active {
+ color: black;
+ }
+
+ a:hover {
+ color: #de0808;
+ }
+ </style>
+ <script>
+
+ function getMIMEType(type)
+ {
+ re = /\/.+/g
+ return type.replace(re, "")
+ }
+
+ function shortURL(url)
+ {
+ var check = url.replace("{{.Board.Actor}}/", "")
+ re = /.+\//g;
+ temp = re.exec(url)
+ if(temp[0] == "{{ .Board.Actor }}/")
+ {
+ var short = url.replace("https://", "");
+ short = short.replace("http://", "");
+ short = short.replace("www.", "");
+
+ var re = /^.{3}/g;
+
+ var u = re.exec(short);
+
+ re = /\w+$/g;
+
+ u = re.exec(short);
+
+ return u;
+ }else{
+ var short = url.replace("https://", "");
+ short = short.replace("http://", "");
+ short = short.replace("www.", "");
+
+ var re = /^.{3}/g;
+
+ var u = re.exec(short);
+
+ re = /\w+$/g;
+
+ u = re.exec(short);
+
+
+ replace = short.replace(/\/+/g, " ")
+ replace = replace.replace(u, " ").trim()
+ re = /\w+$/;
+ v = re.exec(replace)
+
+ v = "f" + v[0] + "-" + u
+
+ return v;
+ }
+ }
+
+ </script>
+ <body style="background-color: #eef2fe;">
+ <ul id="top" style="padding:0; display: inline;">
+ {{range .Boards}}
+ <li style="display: inline;"><a href="{{.Location}}">{{.Name }}</a></li>
+ {{end}}
+ </ul>
+ {{ $board := .Board }}
+ {{ if $board.IsMod }}
+ <span style="float: right;"><a href="/{{ .Key }}/{{ .Board.Name }}">[Manage Board]</a></span>
+ {{ end }}
+ <div style="margin: 0 auto; width: 400px; margin-bottom: 100px;">
+ <h1 style="color: #af0a0f;">/{{ $board.Name }}/ - {{ $board.PrefName }}</h1>
+ <form id="new-post" action="/post" method="post" enctype="multipart/form-data">
+ <label for="name">Name:</label><br>
+ <input type="text" id="name" name="name" placeholder="Anonymous"><br>
+ <label for="options">Options:</label><br>
+ <input type="text" id="options" name="options"><br>
+ <label for="subject">Subject:</label><br>
+ <input type="text" id="subject" name="subject"><input type="submit" value="Post"><br>
+ <label for="comment">Comment:</label><br>
+ <textarea rows="10" cols="50" id="comment" name="comment"></textarea><br>
+ <input type="hidden" id="inReplyTo" name="inReplyTo" value="{{ $board.InReplyTo }}">
+ <input type="hidden" id="sendTo" name="sendTo" value="{{ $board.To }}">
+ <input type="hidden" id="boardName" name="boardName" value="{{ $board.Name }}">
+ <input type="hidden" id="captchaCode" name="captchaCode" value="{{ $board.CaptchaCode }}">
+ <input type="file" id="file" name="file"><br><br>
+ <label stye="display: inline-block;" for="captcha">Captcha:</label><br>
+ <input style="display: inline-block;" type="text" id="captcha" name="captcha"><br>
+ <div style="height: 65px;">
+ <img src="{{ $board.Captcha }}">
+ </div>
+ </form>
+ </div>
+
+ <hr>
+ <ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a href="#bottom">[Bottom]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+ </ul>
+ <hr>
+
+ <div style="padding: 10px; text-align: center;">
+ {{ range .Posts }}
+ <div style="overflow: hidden; vertical-align: top; margin: 0 auto; display: inline-block; width: 180px; max-height: 320px; margin-bottom: 10px;">
+ {{ if $board.IsMod }}
+ <a href="/delete?id={{ .Id }}">[Delete Post]</a>
+ {{ end }}
+ {{ if .Attachment }}
+ {{ if $board.IsMod }}
+ <a href="/deleteattach?id={{ .Id }}">[Delete Attachment]</a>
+ {{ end }}
+ <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/">
+ <div id="media-{{ .Id }}"></div>
+ <script>
+ media = document.getElementById("media-{{ .Id }}")
+ if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "image"){
+ var img = document.createElement("img");
+ img.style = "float: left; margin-right: 10px; margin-bottom: 10px; max-width: 150px; max-height: 150px; cursor: move;"
+ img.setAttribute("id", "img")
+ img.setAttribute("main", "1")
+ img.setAttribute("src", "{{ (index .Attachment 0).Href }}")
+ media.appendChild(img)
+ }
+
+ if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "audio"){
+ var audio = document.createElement("audio")
+ audio.controls = 'controls'
+ audio.muted = 'muted'
+ audio.src = '{{ (index .Attachment 0).Href }}'
+ audio.type = '{{ (index .Attachment 0).MediaType }}'
+ audio.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 150px;"
+ 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.muted = 'muted'
+ video.src = '{{ (index .Attachment 0).Href }}'
+ video.type = '{{ (index .Attachment 0).MediaType }}'
+ video.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 150px;"
+ video.innerText = 'Video is not supported.'
+ media.appendChild(video)
+ }
+ </script>
+
+
+ {{ end }}
+ <div>
+ {{ $replies := .Replies }}
+ <span style="display: block">R: {{ $replies.TotalItems }}{{ if $replies.TotalImgs }}/ A: {{ $replies.TotalImgs }}{{ end }}</span>
+ {{ if .Name }}
+ <span style="display: block; color: #0f0c5d;"><b>{{ .Name }}</b></span>
+ {{ end }}
+ {{ if .Content }}
+ <span style="display: block">{{.Content}}</span>
+ {{ end }}
+ </div>
+ </a>
+ </div>
+ <script>
+ document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{ .Id }}")
+ </script>
+ {{ end }}
+ </div>
+ <hr>
+ <ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a id="bottom" href="#top">[Top]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+ </ul>
+ <hr>
+ </body>
+</html>
+
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..2840a83
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,14 @@
+{{ define "header" }}
+{{ end }}
+
+{{ define "top" }}{{ end }}
+{{ define "content" }}
+<div style="text-align: center; max-width: 800px; margin: 0 auto;">
+ <h1>{{ .Title }}</h1>
+ <p style="text-align: justify">{{.Message}}</p>
+</div>
+{{ end }}
+{{ define "bottom" }}{{ end }}
+
+{{ define "script" }}
+{{ end }}
diff --git a/static/js/footerscript.js b/static/js/footerscript.js
new file mode 100644
index 0000000..bd36daa
--- /dev/null
+++ b/static/js/footerscript.js
@@ -0,0 +1,40 @@
+var imgs = document.querySelectorAll('#img');
+var imgArray = [].slice.call(imgs);
+
+imgArray.forEach(function(img, i){
+ img.addEventListener("click", function(e){
+ if(img.getAttribute("enlarge") == "0")
+ {
+ var attachment = img.getAttribute("attachment")
+ img.setAttribute("enlarge", "1");
+ img.setAttribute("style", "float: left; margin-right: 10px; cursor: move;");
+ img.src = attachment
+ }
+ else
+ {
+ var preview = img.getAttribute("preview")
+ img.setAttribute("enlarge", "0");
+ if(img.getAttribute("main") == 1)
+ {
+ img.setAttribute("style", "float: left; margin-right: 10px; max-width: 250px; max-height: 250px; cursor: move;");
+ img.src = preview
+ }
+ else
+ {
+ img.setAttribute("style", "float: left; margin-right: 10px; max-width: 125px; max-height: 125px; cursor: move;");
+ img.src = preview
+ }
+ }
+ });
+})
+
+
+function viewLink(board, actor) {
+ var posts = document.querySelectorAll('#view');
+ var postsArray = [].slice.call(posts);
+
+ postsArray.forEach(function(p, i){
+ var id = p.getAttribute("post")
+ p.href = "/" + board + "/" + shortURL(actor, id)
+ })
+}
diff --git a/static/js/posts.js b/static/js/posts.js
new file mode 100644
index 0000000..7c1a3e8
--- /dev/null
+++ b/static/js/posts.js
@@ -0,0 +1,239 @@
+function newpost()
+{
+ var el = document.getElementById("newpostbtn")
+ var state = el.getAttribute("state")
+ if(state = "0")
+ {
+ el.style="display:none;"
+ el.setAttribute("state", "1")
+ document.getElementById("newpost").style = "display: block;";
+ }
+ else
+ {
+ el.style="display:block;"
+ el.setAttribute("state", "0")
+ document.getElementById("newpost").style = "display: hidden;";
+ }
+}
+
+function getMIMEType(type)
+{
+ re = /\/.+/g
+ return type.replace(re, "")
+}
+
+function shortURL(actorName, url)
+{
+ var check = url.replace(actorName + "/", "")
+ re = /.+\//g;
+ temp = re.exec(url)
+
+
+ if(stripTransferProtocol(temp[0]) == stripTransferProtocol(actorName) + "/")
+ {
+ var short = url.replace("https://", "");
+ short = short.replace("http://", "");
+ short = short.replace("www.", "");
+
+ var re = /^.{3}/g;
+
+ var u = re.exec(short);
+
+ re = /\w+$/g;
+
+ u = re.exec(short);
+
+ return u;
+ }else{
+
+ var short = url.replace("https://", "");
+ short = short.replace("http://", "");
+ short = short.replace("www.", "");
+
+ var re = /^.{3}/g;
+
+ var u = re.exec(short);
+
+ re = /\w+$/g;
+
+ u = re.exec(short);
+
+
+ replace = short.replace(/\/+/g, " ")
+ replace = replace.replace(u, " ").trim()
+ re = /\w+$/;
+ v = re.exec(replace)
+
+ v = "f" + v[0] + "-" + u
+
+ return v;
+ }
+}
+
+function shortImg(url)
+{
+ var u = url;
+ if(url.length > 26)
+ {
+ var re = /^.{26}/g;
+
+ u = re.exec(url);
+
+ re = /\..+$/g;
+
+ var v = re.exec(url);
+
+ u += "(...)" + v;
+ }
+ return u;
+}
+
+function convertSize(size)
+{
+ var convert = size / 1024.0;
+ if(convert > 1024)
+ {
+ convert = convert / 1024.0
+ convert = convert.toFixed(2) + " MB"
+ }
+ else
+ {
+ convert = convert.toFixed(2) + " KB"
+ }
+
+ return convert
+}
+
+function getBoardId(url)
+{
+ var re = /\/([^/\n]+)(.+)?/gm
+ var matches = re.exec(url);
+ return matches[1]
+}
+
+function convertContent(actorName, content, opid)
+{
+ var re = /(>>)(https:\/\/|http:\/\/)?(www\.)?.+\/\w+/gm;
+ var match = content.match(re);
+ var newContent = content;
+ if(match)
+ {
+ match.forEach(function(quote, i){
+ var link = quote.replace('>>', '')
+ var isOP = ""
+ if(link == opid)
+ {
+ isOP = " (OP)";
+ }
+
+ newContent = newContent.replace(quote, '<a title="' + link + '" href="/'+ getBoardId(actorName) + "/" + shortURL(actorName, opid) + '#' + shortURL(actorName, link) + '"style="color:#af0a0f;">>>' + shortURL(actorName, link) + isOP + '</a>');
+
+ })
+ }
+
+ re = /^>.+/gm;
+
+ match = newContent.match(re);
+ if(match)
+ {
+ match.forEach(function(quote, i) {
+ newContent = newContent.replace(quote, '<span style="color: green;">' + quote + '</span>');
+ })
+ }
+
+ return newContent
+}
+
+function closeReply()
+{
+ document.getElementById("reply-box").style.display = "none";
+ document.getElementById("reply-comment").value = "";
+}
+
+
+function previous(actorName, page)
+{
+ var prev = parseInt(page) - 1;
+ if(prev < 0)
+ prev = 0;
+ window.location.href = "/" + actorName + "/" + prev;
+}
+
+function next(actorName, totalPage, page)
+{
+ var next = parseInt(page) + 1;
+ if(next > parseInt(totalPage))
+ next = parseInt(totalPage);
+ window.location.href = "/" + actorName + "/" + next;
+}
+
+function quote(actorName, opid, id)
+{
+ var box = document.getElementById("reply-box");
+ var header = document.getElementById("reply-header");
+ var comment = document.getElementById("reply-comment");
+ var inReplyTo = document.getElementById("inReplyTo-box");
+
+ var w = window.innerWidth / 2 - 200;
+ var h = document.getElementById(id + "-content").offsetTop - 448;
+
+ box.setAttribute("style", "display: block; position: absolute; background-color: #eff5ff; width: 400px; height: 550px; z-index: 9; top: " + h + "px; left: " + w + "px; padding: 5px; border: 4px solid #d3caf0;");
+
+
+ if (inReplyTo.value != opid)
+ comment.value = "";
+
+ header.innerText = "Replying to Thread No. " + shortURL(actorName, opid);
+ inReplyTo.value = opid;
+
+ if(comment.value != "")
+ comment.value += "\n>>" + id;
+ else
+ comment.value += ">>" + id;
+
+ dragElement(header);
+
+}
+
+function dragElement(elmnt) {
+ var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
+
+ elmnt.onmousedown = dragMouseDown;
+
+ function dragMouseDown(e) {
+ e = e || window.event;
+ e.preventDefault();
+ // get the mouse cursor position at startup:
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ document.onmouseup = closeDragElement;
+ // call a function whenever the cursor moves:
+ document.onmousemove = elementDrag;
+ }
+
+ function elementDrag(e) {
+ e = e || window.event;
+ e.preventDefault();
+ // calculate the new cursor position:
+ pos1 = pos3 - e.clientX;
+ pos2 = pos4 - e.clientY;
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ // set the element's new position:
+ elmnt.parentElement.style.top = (elmnt.parentElement.offsetTop - pos2) + "px";
+ elmnt.parentElement.style.left = (elmnt.parentElement.offsetLeft - pos1) + "px";
+ }
+
+ function closeDragElement() {
+ // stop moving when mouse button is released:
+ document.onmouseup = null;
+ document.onmousemove = null;
+ }
+}
+
+function stripTransferProtocol(value){
+ var re = /(https:\/\/|http:\/\/)?(www.)?/
+
+ return value.replace(re, "")
+}
+
diff --git a/static/main.html b/static/main.html
new file mode 100644
index 0000000..f4007d1
--- /dev/null
+++ b/static/main.html
@@ -0,0 +1,42 @@
+{{ define "layout" }}
+<!DOCTYPE html>
+<html>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <head>
+ <title>{{ .Title }}</title>
+ <style>
+ a, a:link, a:visited, a:hover, a:active {
+ text-decoration: none
+ }
+
+ a:link, a:visited, a:active {
+ color: black;
+ }
+
+ a:hover {
+ color: #de0808;
+ }
+ </style>
+ {{ template "header" . }}
+ </head>
+ <body style="background-color: #eef2fe;">
+ <ul style="display: inline; padding:0;">
+ {{range .Boards}}
+ <li style="display: inline;"><a href="{{.Location}}">{{.Name}}</a></li>
+ {{end}}
+ </ul>
+ {{ if .Board.ModCred }}
+ {{ if eq .Board.ModCred .Board.Domain .Board.Actor }}
+ <span style="float: right;"><a href="/{{ .Key }}/{{ .Board.Name }}">[Manage Board]</a></span>
+ {{ end }}
+ {{ end }}
+ {{ template "top" . }}
+
+ {{ template "content" . }}
+
+ {{ template "bottom" . }}
+ </body>
+</html>
+
+{{ template "script" . }}
+{{ end }}
diff --git a/static/manage.html b/static/manage.html
new file mode 100644
index 0000000..43ff766
--- /dev/null
+++ b/static/manage.html
@@ -0,0 +1,81 @@
+{{ define "title" }}{{ .Title }}{{ end }}
+{{ define "header" }}
+<script src="/static/js/posts.js"></script>
+{{ end }}
+{{ define "top" }}{{ end }}
+{{ define "content" }}
+<div style="text-align: center; margin: 0 auto; width: 400px;">
+ <h1>Manage /{{ .Board.Name }}/</h1>
+ <!-- <div><a href="/{{ .Key }}/deleteboard?name={{ .Board.Name }}">[Delete Board]</a></div> -->
+ <ul style="display: inline-block; padding: 0;">
+ <li style="display: inline-block;"><a href="javascript:show('following')">[ Subscribed ]</a></li>
+ <li style="display: inline-block;"><a href="javascript:show('followers')">[ Subscribers ]</a></li>
+ <li style="display: inline-block;"><a href="javascript:show('reported')">[ Reported ]</a></li>
+ </ul>
+</div>
+<a href="/{{ .Board.Name }}">[Return]</a>
+<div id="following">
+ <h4>Subscribed</h4>
+ <form id="follow-form" action="/{{ .Key }}/{{ .Board.Name }}/follow" method="post" enctype="application/x-www-form-urlencoded">
+ <label>Subscribe:</label><br>
+ <input id="follow" name="follow" style="margin-bottom: 12px;" placeholder="https://localhost:3000/g"></input>
+ <input type="submit" value="Subscribe"><br>
+ <input type="hidden" name="actor" value="{{ .Actor }}">
+ </form>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ $actor := .Actor }}
+ {{ $board := .Board }}
+ {{ $key := .Key }}
+ {{ range .Following }}
+ <li><a href="/{{ $key }}/{{ $board.Name }}/follow?follow={{ . }}&actor={{ $actor }}">[Unsubscribe]</a><a href="{{ . }}">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+</div>
+
+<div id="followers" style="display: none;">
+ <h4>Subscribers</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ range .Followers }}
+ <li><a href="{{ . }}">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+</div>
+
+<div id="reported" style="display: none;">
+ <h4>Reported</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+
+ {{ $domain := .Domain }}
+ {{ range .Reported }}
+ <li><a id="rpost" post="{{ .ID }}" href=""></a> - <b>{{ .Count }}</b> <a href="/delete?id={{ .ID }}&board={{ $board.Name }}">[Remove Post]</a> <a href="/deleteattach?id={{ .ID }}">[Remove Attachment]</a> <a href="/report?id={{ .ID }}&close=1&board={{ $board.Name }}">[Close]</a></li>
+ {{ end }}
+ </ul>
+</div>
+{{ end }}
+{{ define "bottom" }}{{ end }}
+
+{{ define "script" }}
+<script>
+ function show(element)
+ {
+ var following = document.getElementById("following");
+ var followers = document.getElementById("followers");
+ var reported = document.getElementById("reported");
+
+ following.style.display = "none";
+ followers.style.display = "none";
+ reported.style.display = "none";
+
+ document.getElementById(element).style.display = "block";
+ }
+
+ var reported = document.querySelectorAll('#rpost');
+ var reportedArray = [].slice.call(reported);
+
+ reportedArray.forEach(function(r, i){
+ var id = r.getAttribute("post")
+ r.innerText = "/" + {{ .Board.Name }} + "/" + shortURL("{{ .Actor }}", id)
+ r.href = {{ .Domain }} + "/" + {{ .Board.Name }} + "/" + shortURL("{{ .Actor }}", id)
+ })
+</script>
+{{ end }}
diff --git a/static/nadmin.html b/static/nadmin.html
new file mode 100644
index 0000000..0583efc
--- /dev/null
+++ b/static/nadmin.html
@@ -0,0 +1,76 @@
+{{ define "title" }}{{ .Title }}{{ end }}
+{{ define "header" }} {{ end }}
+{{ define "top" }}{{ end }}
+{{ define "content" }}
+<div style="margin: 0 auto; width: 400px;">
+ <h3>Add Board</h3>
+ <form id="new-post" action="/{{ .Key }}/addboard" method="post" enctype="application/x-www-form-urlencoded">
+ <label>Name:</label><br>
+ <input type="text" name="name" placeholder="g" required><br>
+ <label>Prefered Name:</label><br>
+ <input type="text" name="prefname" placeholder="Technology" required><input type="submit" value="Add"><br>
+ <label>Summary:</label><br>
+ <textarea name="summary" rows="8" cols="50"></textarea><br>
+ <label>Restricted:</label><br>
+ <select name="restricted">
+ <option value="True">True</option>
+ <option value="False">False</option>
+ </select>
+ </form>
+
+ <ul style="display: inline-block; padding: 0;">
+ <li style="display: inline-block;"><a href="javascript:show('following')">Subscribed</a></li>
+ <!-- <li style="display: inline-block;"><a href="javascript:show('followers')">Followers</a></li> -->
+ <li style="display: inline-block;"><a href="javascript:show('reported')">Reported</a></li>
+ </ul>
+</div>
+
+<div id="following">
+ <h4>Following</h4>
+ <form id="follow-form" action="/{{ .Key }}/follow" method="post" enctype="application/x-www-form-urlencoded">
+ <label>Subscribe:</label><br>
+ <input id="follow" name="follow" style="margin-bottom: 12px;" placeholder="http://localhost:3000/g"></input><input type="submit" value="Subscribe"><br>
+ <input type="hidden" name="actor" value="{{ .Actor }}">
+ </form>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ $actor := .Actor }}
+ {{ $key := .Key }}
+ {{ range .Following }}
+ <li><a href="/{{ $key }}/follow?follow={{ . }}&actor={{ $actor }}">[Unfollow]</a><a href="{{ . }}">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+</div>
+
+<div id="followers" style="display: none;">
+ <h4>Followers</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ {{ range .Followers }}
+ <li><a href="http://localhost:3000/g">{{ . }}</a></li>
+ {{ end }}
+ </ul>
+</div>
+
+<div id="reported" style="display: none;">
+ <h4>Reported</h4>
+ <ul style="display: inline-block; padding: 0; margin: 0;">
+ </ul>
+</div>
+{{ end }}
+{{ define "bottom" }}{{ end }}
+
+{{ define "script" }}
+<script>
+ function show(element)
+ {
+ var following = document.getElementById("following");
+ // var followers = document.getElementById("followers");
+ var reported = document.getElementById("reported");
+
+ following.style.display = "none";
+ // followers.style.display = "none";
+ reported.style.display = "none";
+
+ document.getElementById(element).style.display = "block";
+ }
+</script>
+{{ end }}
diff --git a/static/ncatalog.html b/static/ncatalog.html
new file mode 100644
index 0000000..a57e291
--- /dev/null
+++ b/static/ncatalog.html
@@ -0,0 +1,93 @@
+{{ define "header" }}
+<script src="/static/js/posts.js"></script>
+{{ end }}
+
+{{ define "content" }}
+{{ $board := .Board }}
+<hr>
+<ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a href="#bottom">[Bottom]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+<hr>
+
+<div style="padding: 10px; text-align: center;">
+ {{ range .Posts }}
+ <div style="overflow: hidden; vertical-align: top; padding-right: 24px; padding-bottom: 24px; display: inline-block; width: 180px; max-height: 320px; margin-bottom: 10px;">
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/delete?id={{ .Id }}">[Delete Post]</a>
+ {{ end }}
+ {{ if .Attachment }}
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/deleteattach?id={{ .Id }}">[Delete Attachment]</a>
+ {{ end }}
+ <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/">
+ <div id="media-{{ .Id }}" style="width:180px;"></div>
+ <script>
+ media = document.getElementById("media-{{ .Id }}")
+ if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "image"){
+ var img = document.createElement("img");
+ img.style = "max-width: 180px; max-height: 180px; cursor: move;"
+ img.setAttribute("id", "img")
+ img.setAttribute("main", "1")
+ img.setAttribute("src", "{{ (index .Attachment 0).Href }}")
+ media.appendChild(img)
+ }
+
+ if(getMIMEType({{ (index .Attachment 0).MediaType }}) == "audio"){
+ var audio = document.createElement("audio")
+ audio.controls = 'controls'
+ audio.preload = 'none'
+ audio.src = '{{ (index .Attachment 0).Href }}'
+ audio.type = '{{ (index .Attachment 0).MediaType }}'
+ audio.style = "margin-right: 10px; margin-bottom: 10px; width: 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 = 'none'
+ video.muted = 'muted'
+ video.src = '{{ (index .Attachment 0).Href }}'
+ video.type = '{{ (index .Attachment 0).MediaType }}'
+ video.style = "margin-right: 10px; margin-bottom: 10px; width: 180px;"
+ video.innerText = 'Video is not supported.'
+ media.appendChild(video)
+ }
+ </script>
+ {{ end }}
+ <div>
+ {{ $replies := .Replies }}
+ <span style="display: block;">R: {{ $replies.TotalItems }}{{ if $replies.TotalImgs }}/ A: {{ $replies.TotalImgs }}{{ end }}</span>
+ {{ if .Name }}
+ <span style="display: block; color: #0f0c5d;"><b>{{ .Name }}</b></span>
+ {{ end }}
+
+ {{ if .Content }}
+ <span style="display: block">{{.Content}}</span>
+ {{ end }}
+
+ </div>
+ </a>
+ </div>
+ <script>
+ document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor}}", "{{ .Id }}")
+ </script>
+ {{ end }}
+</div>
+<hr>
+<ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a id="bottom" href="#top">[Top]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+<hr>
+{{ end }}
+{{ define "bottom" }}
+{{ end }}
+
+{{ define "script" }}
+{{ end }}
diff --git a/static/npost.html b/static/npost.html
new file mode 100644
index 0000000..bf07c63
--- /dev/null
+++ b/static/npost.html
@@ -0,0 +1,39 @@
+{{ define "header" }}
+<script src="/static/js/posts.js"></script>
+{{ end }}
+
+{{ define "content" }}
+{{ $board := .Board }}
+
+<hr>
+<ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a href="/{{ $board.Name }}/catalog">[Catalog]</a></li>
+ <li style="display: inline"><a href="#bottom">[Bottom]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+<hr>
+
+{{ template "posts" . }}
+
+<hr>
+<ul style="position: absolute; left: 5px; margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}">[Return]</a></li>
+ <li style="display: inline"><a href="/{{ $board.Name }}/catalog">[Catalog]</a></li>
+ <li style="display: inline"><a id="bottom" href="#top">[Top]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+<div style=": inline; text-align: center;">
+ <span><a href="javascript:quote('{{ $board.Actor }}', '{{ (index .Posts 0).Id }}', '{{ (index .Posts 0).Id }}')">[Post a Reply]</a></span>
+ {{ $replies := (index .Posts 0).Replies }}
+ <span style="float: right;">{{ $replies.TotalItems }} / {{ $replies.TotalImgs }}</span>
+</div>
+<hr>
+{{ end }}
+
+{{ define "script" }}
+<script src="/static/js/footerscript.js"></script>
+<script>
+ viewLink("{{ .Board.Name }}", "{{ .Board.Actor }}")
+</script>
+{{ end }}
diff --git a/static/nposts.html b/static/nposts.html
new file mode 100644
index 0000000..2296df5
--- /dev/null
+++ b/static/nposts.html
@@ -0,0 +1,51 @@
+{{ define "header" }}
+<script src="/static/js/posts.js"></script>
+{{ end }}
+
+
+{{ define "content" }}
+{{ $board := .Board }}
+<hr>
+<ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}/catalog">[Catalog]</a></li>
+ <li style="display: inline"><a href="#bottom">[Bottom]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+
+{{ template "posts" . }}
+
+<hr>
+<ul style="margin: 0; padding: 0; display: inline">
+ <li style="display: inline"><a href="/{{ $board.Name }}/catalog">[Catalog]</a></li>
+ <li style="display: inline"><a id="bottom" href="#top">[Top]</a></li>
+ <li style="display: inline"><a href="javascript:location.reload()">[Refresh]</a></li>
+</ul>
+<hr>
+{{ if gt .TotalPage 0 }}
+{{ $totalPage := .TotalPage }}
+<ul style="float: right; margin: 0; padding: 0; display: inline">
+ {{ $page := .CurrentPage }}
+ {{ if gt $page 0 }}
+ <li style="display: inline"><button onclick="previous('{{$board.Name }}', '{{ $page }}')">Previous</button></li>
+ {{ end }}
+ {{ range $i, $e := .Pages }}
+ {{ if eq $i $page}}
+ <li style="display: inline"><a href="/{{ $board.Name }}/{{ $i }}"><b>[{{ $i }}]</b></a></li>
+ {{ else }}
+ <li style="display: inline"><a href="/{{ $board.Name }}/{{ $i }}">[{{ $i }}]</a></li>
+ {{ end }}
+ {{ end }}
+ {{ if lt .CurrentPage .TotalPage }}
+ <li style="display: inline"><button onclick="next('{{ $board.Name }}','{{ $totalPage }}' ,'{{ $page }}')">next</button></li>
+ {{ end }}
+</ul>
+{{ end }}
+{{ end }}
+
+{{ define "script" }}
+<script src="/static/js/footerscript.js"></script>
+<script>
+ viewLink("{{ .Board.Name }}", "{{ .Board.Actor }}")
+
+</script>
+{{ end }}
diff --git a/static/posts.html b/static/posts.html
new file mode 100644
index 0000000..ebe8f4e
--- /dev/null
+++ b/static/posts.html
@@ -0,0 +1,179 @@
+{{ define "posts" }}
+{{ $board := .Board }}
+{{ $len := len .Posts }}
+{{ range .Posts }}
+{{ $opId := .Id }}
+{{ if eq $board.InReplyTo "" }}
+<hr>
+{{ end }}
+<div style="overflow: auto;">
+ <div id="{{ .Id }}" style="overflow: visible; margin-bottom: 12px;">
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/delete?id={{ .Id }}">[Delete Post]</a>
+ {{ end }}
+ {{ if .Attachment }}
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/deleteattach?id={{ .Id }}">[Delete Attachment]</a>
+ {{ end }}
+ <span style="display: block;">File: <a id="{{ .Id }}-img" href="{{ (index .Attachment 0).Href}}">{{ (index .Attachment 0).Name }}</a><span id="{{ .Id }}-size">({{ (index .Attachment 0).Size }})</span></span>
+ <div id="media-{{ .Id }}"></div>
+ <script>
+ media = document.getElementById("media-{{ .Id }}")
+ 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; cursor: move;"
+ img.setAttribute("id", "img")
+ img.setAttribute("main", "1")
+ img.setAttribute("enlarge", "0")
+ img.setAttribute("attachment", "{{ (index .Attachment 0).Href }}")
+ {{ if .Preview.Href }}
+ img.setAttribute("src", "{{ .Preview.Href }}")
+ img.setAttribute("preview", "{{ .Preview.Href }}")
+ {{ else }}
+ img.setAttribute("src", "{{ (index .Attachment 0).Href }}")
+ img.setAttribute("preview", "{{ (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 = 'none'
+ audio.src = '{{ (index .Attachment 0).Href }}'
+ audio.type = '{{ (index .Attachment 0).MediaType }}'
+ audio.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 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 = 'none'
+ video.muted = 'muted'
+ video.src = '{{ (index .Attachment 0).Href }}'
+ video.type = '{{ (index .Attachment 0).MediaType }}'
+ video.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 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>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/">No.</a> <a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor }}', '{{ $opId }}', '{{ .Id }}')">{{ .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="/report?id={{ .Id }}&board={{ $board.Name }}">[Report]</a>{{ end }}</span>
+ <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.Content}}</p>
+ {{ if .Replies }}
+ {{ $replies := .Replies }}
+ {{ if gt $replies.TotalItems 5 }}
+ <span>{{ $replies.TotalItems }} replies{{ if gt $replies.TotalImgs 0}} and {{ $replies.TotalImgs }} images{{ end }}, Click <a id="view" post="{{.Id}}" href="/{{ $board.Name }}/{{ .Id }}">here</a> to view all.</span>
+ {{ end }}
+ {{ range $replies.OrderedItems }}
+ <div id="{{ .Id }}">
+ <div style="display: inline-block; overflow: auto;">
+ <div style="float: left; display: block; margin-right: 5px;">>></div>
+ <div style="overflow: auto;background-color: #d5daf0; padding: 5px; margin-bottom: 2px;">
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/delete?id={{ .Id }}">[Delete Post]</a>
+ {{ end }}
+ {{ if .Attachment }}
+ {{ if eq $board.ModCred $board.Domain $board.Actor }}
+ <a href="/deleteattach?id={{ .Id }}">[Delete Attachment]</a>
+ {{ end }}
+ <span style="display: block;">File <a id="{{ .Id }}-img" href="{{ (index .Attachment 0).Href}}">{{ (index .Attachment 0).Name }}</a> <span id="{{ .Id }}-size">({{ (index .Attachment 0).Size }})</span></span>
+ <div id="media-{{ .Id }}"></div>
+ <script>
+ media = document.getElementById("media-{{ .Id }}")
+ 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; cursor: move;"
+ img.setAttribute("id", "img")
+ img.setAttribute("main", "1")
+ img.setAttribute("enlarge", "0")
+ img.setAttribute("attachment", "{{ (index .Attachment 0).Href }}")
+ {{ if .Preview.Href }}
+ img.setAttribute("src", "{{ .Preview.Href }}")
+ img.setAttribute("preview", "{{ .Preview.Href }}")
+ {{ else }}
+ img.setAttribute("src", "{{ (index .Attachment 0).Href }}")
+ img.setAttribute("preview", "{{ (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 = 'none'
+ audio.src = '{{ (index .Attachment 0).Href }}'
+ audio.type = '{{ (index .Attachment 0).MediaType }}'
+ audio.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 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 = 'none'
+ video.muted = 'muted'
+ video.src = '{{ (index .Attachment 0).Href }}'
+ video.type = '{{ (index .Attachment 0).MediaType }}'
+ video.style = "float: left; margin-right: 10px; margin-bottom: 10px; width: 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>{{ .Published }} <a id="{{ .Id }}-anchor" href="/{{ $board.Name }}/post/{{ $opId }}#{{ .Id }}">No. </a><a id="{{ .Id }}-link" title="{{ .Id }}" href="javascript:quote('{{ $board.Actor }}', '{{ $opId }}', '{{ .Id }}')">{{ .Id }}</a> {{ if ne .Type "Tombstone" }}<a href="/report?id={{ .Id }}&board={{ $board.Name }}">[Report]</a>{{ end }}</span>
+ {{ $parentId := .Id }}
+ {{ if .Replies.OrderedItems }}
+ {{ range .Replies.OrderedItems }}
+ <span id="{{$parentId}}-replyto-{{.Id}}"></span>
+ <script>document.getElementById("{{ $parentId }}-replyto-{{.Id}}").innerHTML = "<a title='{{ .Id }}' href='/{{ $board.Name }}/" + shortURL("{{ $board.Actor }}", "{{ $opId }}") + "#" + shortURL("{{ $board.Actor }}", "{{ .Id }}") + "'>>>" + shortURL("{{ $board.Actor }}", "{{ .Id }}") + "</a>";</script>
+ {{ end }}
+ {{ end }}
+ <p id="{{ .Id }}-content" style="white-space: pre-wrap; margin: 10px 30px 10px 30px;">{{.Content}}</p>
+ </div>
+ </div>
+ </div>
+ <script>
+ {{ if .Attachment }}
+ 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 }}");
+
+ document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor}}", "{{ $opId }}") +
+ "#" + shortURL("{{$board.Actor}}", "{{ .Id }}");
+ document.getElementById("{{ .Id }}").setAttribute("id", shortURL("{{$board.Actor}}", "{{ .Id }}"));
+
+ var content = document.getElementById("{{ .Id }}-content");
+
+ content.innerHTML = convertContent('{{$board.Actor}}', content.innerText, '{{ $opId }}')
+
+ </script>
+ {{ end }}
+ {{ end }}
+ </div>
+</div>
+<script>
+ {{ if .Attachment }}
+ 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 }}");
+
+ document.getElementById("{{ .Id }}").setAttribute("id", shortURL("{{ $board.Actor }}", "{{ .Id }}"));
+
+ document.getElementById("{{ .Id }}-anchor").href = "/{{ $board.Name }}/" + shortURL("{{$board.Actor}}", "{{ $opId }}") +
+ "#" + shortURL("{{$board.Actor}}", "{{ .Id }}");
+
+ var content = document.getElementById("{{ .Id }}-content");
+
+ content.innerHTML = convertContent('{{$board.Actor}}', content.innerText, '{{ $opId }}')
+
+</script>
+{{ end }}
+{{ end }}
diff --git a/static/top.html b/static/top.html
new file mode 100644
index 0000000..d28043f
--- /dev/null
+++ b/static/top.html
@@ -0,0 +1,37 @@
+{{ define "top" }}
+<div style="margin: 0 auto; width: 400px; margin-bottom: 100px;">
+ <h1 style="text-align: center; color: #af0a0f;">/{{ .Board.Name }}/ - {{ .Board.PrefName }}</h1>
+ <p style="text-align: center;">{{ .Board.Summary }}</p>
+ {{ $len := len .Posts }}
+ {{ if .Board.InReplyTo }}
+ <h3 id="newpostbtn" state="0" style="text-align: center; margin-top: 80px;"><a href="javascript:newpost()">[Post a Reply]</a></h3>
+ {{ else }}
+ <h3 id="newpostbtn" state="0" style="text-align: center; margin-top: 80px;"><a href="javascript:newpost()">[Start a New Thread]</a></h3>
+ {{ end }}
+ <div id="newpost" style="display: none;">
+ <form id="new-post" action="/post" method="post" enctype="multipart/form-data">
+ <label for="name">Name:</label><br>
+ <input type="text" id="name" name="name" placeholder="Anonymous"><br>
+ <label for="options">Options:</label><br>
+ <input type="text" id="options" name="options">{{ if .Board.InReplyTo }}<input type="submit" value="Post">{{ end }}<br>
+ {{ if eq .Board.InReplyTo "" }}
+ <label for="subject">Subject:</label><br>
+ <input type="text" id="subject" name="subject"><input type="submit" value="Post"><br>
+ {{ end }}
+ <label for="comment">Comment:</label><br>
+ <textarea rows="10" cols="50" id="comment" name="comment"></textarea><br>
+ <input type="hidden" id="inReplyTo" name="inReplyTo" value="{{ .Board.InReplyTo }}">
+ <input type="hidden" id="sendTo" name="sendTo" value="{{ .Board.To }}">
+ <input type="hidden" id="boardName" name="boardName" value="{{ .Board.Name }}">
+ <input type="hidden" id="captchaCode" name="captchaCode" value="{{ .Board.CaptchaCode }}">
+ <input type="file" id="file" name="file" {{ if gt $len 1 }} required {{ else }} {{ if eq $len 0 }} required {{ end }} {{ end }} ><br><br>
+ <label stye="display: inline-block;" for="captcha">Captcha:</label>
+ <br>
+ <input style="display: inline-block;" type="text" id="captcha" name="captcha" autocomplete="off"><br>
+ <div style="height: 65px;">
+ <img src="{{ .Board.Captcha }}">
+ </div>
+ </form>
+ </div>
+</div>
+{{ end }}
diff --git a/static/verify.html b/static/verify.html
new file mode 100644
index 0000000..fb3fb3d
--- /dev/null
+++ b/static/verify.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <head>
+ </head>
+ <body>
+ <div style="width: 200px; margin: 0 auto;">
+ <form action="/verify" method="post">
+ <label>Identifier</label>
+ <input type="text" id="identifier" name="id" required><br>
+ <label>Code</label>
+ <input type="text" id="verify" name="code" required><br>
+ <input type="submit" value="Verify">
+ </form>
+ </div>
+ </body>
+</html>