diff options
-rw-r--r-- | Follow.go | 4 | ||||
-rw-r--r-- | OutboxPost.go | 5 | ||||
-rw-r--r-- | client.go | 731 | ||||
-rw-r--r-- | clientkey | 1 | ||||
-rw-r--r-- | databaseschema.psql | 5 | ||||
-rw-r--r-- | go.mod | 6 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | main.go | 735 | ||||
-rw-r--r-- | session.go | 90 | ||||
-rw-r--r-- | static/admin.html | 77 | ||||
-rw-r--r-- | static/bottom.html | 23 | ||||
-rw-r--r-- | static/catalog.html | 189 | ||||
-rw-r--r-- | static/index.html | 14 | ||||
-rw-r--r-- | static/js/footerscript.js | 40 | ||||
-rw-r--r-- | static/js/posts.js | 239 | ||||
-rw-r--r-- | static/main.html | 42 | ||||
-rw-r--r-- | static/manage.html | 81 | ||||
-rw-r--r-- | static/nadmin.html | 76 | ||||
-rw-r--r-- | static/ncatalog.html | 93 | ||||
-rw-r--r-- | static/npost.html | 39 | ||||
-rw-r--r-- | static/nposts.html | 51 | ||||
-rw-r--r-- | static/posts.html | 179 | ||||
-rw-r--r-- | static/top.html | 37 | ||||
-rw-r--r-- | static/verify.html | 17 |
24 files changed, 2682 insertions, 96 deletions
@@ -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 @@ -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 +) @@ -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= @@ -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> |