From 2566e314e8351ffbceae9a2688d90a5c811d49e5 Mon Sep 17 00:00:00 2001 From: FChannel <=> Date: Fri, 22 Jan 2021 21:12:26 -0800 Subject: client to server conversion --- Follow.go | 4 +- OutboxPost.go | 5 +- client.go | 731 +++++++++++++++++++++++++++++++++++++++++++++ clientkey | 1 + databaseschema.psql | 5 + go.mod | 6 +- go.sum | 4 + main.go | 735 ++++++++++++++++++++++++++++++++++++++++------ session.go | 90 ++++++ static/admin.html | 77 +++++ static/bottom.html | 23 ++ static/catalog.html | 189 ++++++++++++ static/index.html | 14 + static/js/footerscript.js | 40 +++ static/js/posts.js | 239 +++++++++++++++ static/main.html | 42 +++ static/manage.html | 81 +++++ static/nadmin.html | 76 +++++ static/ncatalog.html | 93 ++++++ static/npost.html | 39 +++ static/nposts.html | 51 ++++ static/posts.html | 179 +++++++++++ static/top.html | 37 +++ static/verify.html | 17 ++ 24 files changed, 2682 insertions(+), 96 deletions(-) create mode 100644 client.go create mode 100644 clientkey create mode 100644 session.go create mode 100644 static/admin.html create mode 100644 static/bottom.html create mode 100644 static/catalog.html create mode 100644 static/index.html create mode 100644 static/js/footerscript.js create mode 100644 static/js/posts.js create mode 100644 static/main.html create mode 100644 static/manage.html create mode 100644 static/nadmin.html create mode 100644 static/ncatalog.html create mode 100644 static/npost.html create mode 100644 static/nposts.html create mode 100644 static/posts.html create mode 100644 static/top.html create mode 100644 static/verify.html diff --git a/Follow.go b/Follow.go index dd5a321..ca281ae 100644 --- a/Follow.go +++ b/Follow.go @@ -14,7 +14,7 @@ func GetActorFollowing(w http.ResponseWriter, db *sql.DB, id string) { following.Items = GetActorFollowingDB(db, id) enc, _ := json.MarshalIndent(following, "", "\t") - w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Header().Set("Content-Type", activitystreams) w.Write(enc) } @@ -27,7 +27,7 @@ func GetActorFollowers(w http.ResponseWriter, db *sql.DB, id string) { following.Items = GetActorFollowDB(db, id) enc, _ := json.MarshalIndent(following, "", "\t") - w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Header().Set("Content-Type", activitystreams) w.Write(enc) } diff --git a/OutboxPost.go b/OutboxPost.go index 84810c9..9fb8710 100644 --- a/OutboxPost.go +++ b/OutboxPost.go @@ -66,9 +66,9 @@ func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { w.Write([]byte(id)) return } - + w.WriteHeader(http.StatusForbidden) - w.Write([]byte("could not authenticate")) + w.Write([]byte("captcha could not auth")) } else { activity = GetActivityFromJson(r, db) @@ -528,6 +528,7 @@ func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { case "Follow": for _, e := range activity.To { + if GetActorFromDB(db, e).Id != "" { response := AcceptFollow(activity) response = SetActorFollowerDB(db, response) diff --git a/client.go b/client.go new file mode 100644 index 0000000..976eb31 --- /dev/null +++ b/client.go @@ -0,0 +1,731 @@ +package main + +import "fmt" +import "net/http" +import "html/template" +import "database/sql" +import _ "github.com/lib/pq" +import "strings" +import "strconv" +import "math" +import "sort" +import "regexp" +import "io/ioutil" +import "encoding/json" +import "os" + +var Key *string = new(string) + +var Boards *[]ObjectBase = new([]ObjectBase) + +type Board struct{ + Name string + Actor string + Summary string + PrefName string + InReplyTo string + Location string + To string + RedirectTo string + Captcha string + CaptchaCode string + ModCred string + Domain string + TP string +} + +type PageData struct { + Title string + Message string + Board Board + Pages []int + CurrentPage int + TotalPage int + Boards []Board + Posts []ObjectBase + Key string +} + +type AdminPage struct { + Title string + Board Board + Key string + Actor string + Boards []Board + Following []string + Followers []string + Reported []Report + Domain string +} + +type Report struct { + ID string + Count int +} + +type Removed struct { + ID string + Type string + Board string +} + +func IndexGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { + t := template.Must(template.ParseFiles("./static/main.html", "./static/index.html")) + + actor := GetActorFromDB(db, Domain) + + var boardCollection []Board + + for _, e := range *Boards { + var board Board + boardActor := GetActor(e.Id) + board.Name = "/" + boardActor.Name + "/" + board.PrefName = boardActor.PreferredUsername + board.Location = "/" + boardActor.Name + boardCollection = append(boardCollection, board) + } + + var data PageData + data.Title = "Welcome to " + actor.PreferredUsername + data.Message = fmt.Sprintf("This is the client for the image board %s. The current version of the code running the server and client is volatile, expect a bumpy ride for the time being. Get the server and client code here https://github.com/FChannel0", Domain) + data.Boards = boardCollection + data.Board.Name = "" + data.Key = *Key + data.Board.Domain = Domain + data.Board.ModCred, _ = GetPasswordFromSession(r) + + t.ExecuteTemplate(w, "layout", data) +} + +func OutboxGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection){ + + t := template.Must(template.ParseFiles("./static/main.html", "./static/nposts.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) + + actor := collection.Actor + + postNum := strings.Replace(r.URL.EscapedPath(), "/" + actor.Name + "/", "", 1) + + page, _ := strconv.Atoi(postNum) + + var returnData PageData + + returnData.Board.Name = actor.Name + returnData.Board.PrefName = actor.PreferredUsername + returnData.Board.Summary = actor.Summary + returnData.Board.InReplyTo = "" + returnData.Board.To = actor.Outbox + returnData.Board.Actor = actor.Id + returnData.Board.ModCred, _ = GetPasswordFromSession(r) + returnData.Board.Domain = Domain + returnData.CurrentPage = page + + returnData.Board.Captcha = GetCaptcha(*actor) + returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) + + returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername + + returnData.Key = *Key + + var mergeCollection Collection + + for _, e := range collection.OrderedItems { + if e.Type != "Tombstone" { + mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e) + } + } + + domainURL := GetDomainURL(*actor) + + if domainURL == Domain { + followCol := GetObjectsFromFollow(*actor) + for _, e := range followCol { + if e.Type != "Tombstone" { + mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e) + } + } + } + + DeleteRemovedPosts(db, &mergeCollection) + DeleteTombstoneReplies(&mergeCollection) + + for i, _ := range mergeCollection.OrderedItems { + sort.Sort(ObjectBaseSortAsc(mergeCollection.OrderedItems[i].Replies.OrderedItems)) + } + + DeleteTombstonePosts(&mergeCollection) + sort.Sort(ObjectBaseSortDesc(mergeCollection.OrderedItems)) + + + + returnData.Boards = GetBoardCollection(db) + + offset := 8 + start := page * offset + for i := 0; i < offset; i++ { + length := len(mergeCollection.OrderedItems) + current := start + i + if(current < length) { + returnData.Posts = append(returnData.Posts, mergeCollection.OrderedItems[current]) + } + } + + + for i, e := range returnData.Posts { + var replies []ObjectBase + for i := 0; i < 5; i++ { + cur := len(e.Replies.OrderedItems) - i - 1 + if cur > -1 { + replies = append(replies, e.Replies.OrderedItems[cur]) + } + } + + var orderedReplies []ObjectBase + for i := 0; i < 5; i++ { + cur := len(replies) - i - 1 + if cur > -1 { + orderedReplies = append(orderedReplies, replies[cur]) + } + } + returnData.Posts[i].Replies.OrderedItems = orderedReplies + } + + var pages []int + pageLimit := math.Round(float64(len(mergeCollection.OrderedItems) / offset)) + for i := 0.0; i <= pageLimit; i++ { + pages = append(pages, int(i)) + } + + returnData.Pages = pages + returnData.TotalPage = len(returnData.Pages) - 1 + + t.ExecuteTemplate(w, "layout", returnData) +} + +func CatalogGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection){ + + t := template.Must(template.ParseFiles("./static/main.html", "./static/ncatalog.html", "./static/top.html")) + + actor := collection.Actor + + var mergeCollection Collection + + for _, e := range collection.OrderedItems { + mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e) + } + + domainURL := GetDomainURL(*actor) + + if domainURL == Domain { + followCol := GetObjectsFromFollow(*actor) + for _, e := range followCol { + + mergeCollection.OrderedItems = append(mergeCollection.OrderedItems, e) + } + } + + DeleteRemovedPosts(db, &mergeCollection) + DeleteTombstonePosts(&mergeCollection) + + sort.Sort(ObjectBaseSortDesc(mergeCollection.OrderedItems)) + + var returnData PageData + returnData.Board.Name = actor.Name + returnData.Board.PrefName = actor.PreferredUsername + returnData.Board.InReplyTo = "" + returnData.Board.To = actor.Outbox + returnData.Board.Actor = actor.Id + returnData.Board.Summary = actor.Summary + returnData.Board.ModCred, _ = GetPasswordFromSession(r) + returnData.Board.Domain = Domain + returnData.Key = *Key + + returnData.Board.Captcha = GetCaptcha(*actor) + returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) + + returnData.Title = "/" + actor.Name + "/ - " + actor.PreferredUsername + + returnData.Boards = GetBoardCollection(db) + + returnData.Posts = mergeCollection.OrderedItems + + t.ExecuteTemplate(w, "layout", returnData) +} + +func PostGet(w http.ResponseWriter, r *http.Request, db *sql.DB){ + + t := template.Must(template.ParseFiles("./static/main.html", "./static/npost.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) + + path := r.URL.Path + actor := GetActorFromPath(db, path, "/") + re := regexp.MustCompile("\\w+$") + postId := re.FindString(path) + + inReplyTo := actor.Id + "/" + postId + + var returnData PageData + + returnData.Board.Name = actor.Name + returnData.Board.PrefName = actor.PreferredUsername + returnData.Board.To = actor.Outbox + returnData.Board.Actor = actor.Id + returnData.Board.Summary = actor.Summary + returnData.Board.ModCred, _ = GetPasswordFromSession(r) + returnData.Board.Domain = Domain + + + if GetDomainURL(actor) != "" { + returnData.Board.Captcha = GetCaptcha(actor) + returnData.Board.CaptchaCode = GetCaptchaCode(returnData.Board.Captcha) + } + + returnData.Title = "/" + returnData.Board.Name + "/ - " + returnData.Board.PrefName + + returnData.Key = *Key + + returnData.Boards = GetBoardCollection(db) + + re = regexp.MustCompile("f\\w+-\\w+") + + if re.MatchString(path) { + name := GetActorFollowNameFromPath(path) + followActors := GetActorsFollowFromName(actor, name) + followCollection := GetActorsFollowPostFromId(followActors, postId) + + DeleteRemovedPosts(db, &followCollection) + DeleteTombstoneReplies(&followCollection) + + for i, _ := range followCollection.OrderedItems { + sort.Sort(ObjectBaseSortAsc(followCollection.OrderedItems[i].Replies.OrderedItems)) + } + + if len(followCollection.OrderedItems) > 0 { + returnData.Board.InReplyTo = followCollection.OrderedItems[0].Id + returnData.Posts = append(returnData.Posts, followCollection.OrderedItems[0]) + sort.Sort(ObjectBaseSortAsc(returnData.Posts[0].Replies.OrderedItems)) + } + + } else { + + returnData.Board.InReplyTo = inReplyTo + collection := GetActorCollection(inReplyTo) + + DeleteRemovedPosts(db, &collection) + + for i, e := range collection.OrderedItems { + var replies CollectionBase + for _, k := range e.Replies.OrderedItems { + if k.Type != "Tombstone" { + replies.OrderedItems = append(replies.OrderedItems, k) + } else { + collection.OrderedItems[i].Replies.TotalItems = collection.OrderedItems[i].Replies.TotalItems - 1 + if k.Preview.Id != "" { + collection.OrderedItems[i].Replies.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs - 1 + } + } + } + collection.TotalItems = collection.OrderedItems[i].Replies.TotalItems + collection.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs + collection.OrderedItems[i].Replies = &replies + sort.Sort(ObjectBaseSortAsc(e.Replies.OrderedItems)) + } + + if len(collection.OrderedItems) > 0 { + returnData.Posts = append(returnData.Posts, collection.OrderedItems[0]) + sort.Sort(ObjectBaseSortAsc(returnData.Posts[0].Replies.OrderedItems)) + } + } + + t.ExecuteTemplate(w, "layout", returnData) +} + +func GetRemoteActor(id string) Actor { + + var respActor Actor + + id = StripTransferProtocol(id) + + req, err := http.NewRequest("GET", "http://" + id, nil) + + CheckError(err, "error with getting actor req") + + req.Header.Set("Accept", activitystreams) + + resp, err := http.DefaultClient.Do(req) + + if err != nil || resp.StatusCode != 200 { + fmt.Println("could not get actor from " + id) + return respActor + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &respActor) + + CheckError(err, "error getting actor from body") + + return respActor +} + +func GetBoardCollection(db *sql.DB) []Board { + var collection []Board + for _, e := range *Boards { + var board Board + boardActor := GetActorFromDB(db, e.Id) + if boardActor.Id == "" { + boardActor = GetRemoteActor(e.Id) + } + board.Name = "/" + boardActor.Name + "/" + board.PrefName = boardActor.PreferredUsername + board.Location = "/" + boardActor.Name + collection = append(collection, board) + } + + return collection +} + +func WantToServe(db *sql.DB, actorName string) (Collection, bool) { + + var collection Collection + serve := false + + for _, e := range *Boards { + boardActor := GetActorFromDB(db, e.Id) + + if boardActor.Id == "" { + boardActor = GetActor(e.Id) + } + + if boardActor.Id == actorName { + serve = true + collection = GetActorCollection(boardActor.Outbox) + collection.Actor = &boardActor + } + } + + return collection, serve +} + +func StripTransferProtocol(value string) string { + re := regexp.MustCompile("(http://|https://)?(www.)?") + + value = re.ReplaceAllString(value, "") + + return value +} + +func GetCaptcha(actor Actor) string { + re := regexp.MustCompile("(https://|http://)?(www)?.+/") + + domainURL := re.FindString(actor.Id) + + re = regexp.MustCompile("/$") + + domainURL = re.ReplaceAllString(domainURL, "") + + resp, err := http.Get(domainURL + "/getcaptcha") + + CheckError(err, "error getting captcha") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + return domainURL + "/" + string(body) +} + +func GetCaptchaCode(captcha string) string { + re := regexp.MustCompile("\\w+\\.\\w+$") + + code := re.FindString(captcha) + + re = regexp.MustCompile("\\w+") + + code = re.FindString(code) + + return code +} + +func GetDomainURL(actor Actor) string { + re := regexp.MustCompile("(https://|http://)?(www)?.+/") + + domainURL := re.FindString(actor.Id) + + re = regexp.MustCompile("/$") + + domainURL = re.ReplaceAllString(domainURL, "") + + return domainURL +} + +func DeleteTombstoneReplies(collection *Collection) { + + for i, e := range collection.OrderedItems { + var replies CollectionBase + for _, k := range e.Replies.OrderedItems { + if k.Type != "Tombstone" { + replies.OrderedItems = append(replies.OrderedItems, k) + } + } + + replies.TotalItems = collection.OrderedItems[i].Replies.TotalItems + replies.TotalImgs = collection.OrderedItems[i].Replies.TotalImgs + collection.OrderedItems[i].Replies = &replies + } +} + +func DeleteTombstonePosts(collection *Collection) { + var nColl Collection + + for _, e := range collection.OrderedItems { + if e.Type != "Tombstone" { + nColl.OrderedItems = append(nColl.OrderedItems, e) + } + } + collection.OrderedItems = nColl.OrderedItems +} + +func DeleteRemovedPosts(db *sql.DB, collection *Collection) { + + removed := GetLocalDeleteDB(db) + + for p, e := range collection.OrderedItems { + for _, j := range removed { + if e.Id == j.ID { + if j.Type == "attachment" { + collection.OrderedItems[p].Preview.Href = "/public/removed.png" + collection.OrderedItems[p].Preview.Name = "deleted" + collection.OrderedItems[p].Preview.MediaType = "image/png" + collection.OrderedItems[p].Attachment[0].Href = "/public/removed.png" + collection.OrderedItems[p].Attachment[0].Name = "deleted" + collection.OrderedItems[p].Attachment[0].MediaType = "image/png" + } else { + collection.OrderedItems[p].AttributedTo = "deleted" + collection.OrderedItems[p].Content = "" + collection.OrderedItems[p].Type = "Tombstone" + if collection.OrderedItems[p].Attachment != nil { + collection.OrderedItems[p].Preview.Href = "/public/removed.png" + collection.OrderedItems[p].Preview.Name = "deleted" + collection.OrderedItems[p].Preview.MediaType = "image/png" + collection.OrderedItems[p].Attachment[0].Href = "/public/removed.png" + collection.OrderedItems[p].Attachment[0].Name = "deleted" + collection.OrderedItems[p].Attachment[0].MediaType = "image/png" + } + } + } + } + + for i, r := range e.Replies.OrderedItems { + for _, k := range removed { + if r.Id == k.ID { + if k.Type == "attachment" { + e.Replies.OrderedItems[i].Preview.Href = "/public/removed.png" + e.Replies.OrderedItems[i].Preview.Name = "deleted" + e.Replies.OrderedItems[i].Preview.MediaType = "image/png" + e.Replies.OrderedItems[i].Attachment[0].Href = "/public/removed.png" + e.Replies.OrderedItems[i].Attachment[0].Name = "deleted" + e.Replies.OrderedItems[i].Attachment[0].MediaType = "image/png" + collection.OrderedItems[p].Replies.TotalImgs = collection.OrderedItems[p].Replies.TotalImgs - 1 + } else { + e.Replies.OrderedItems[i].AttributedTo = "deleted" + e.Replies.OrderedItems[i].Content = "" + e.Replies.OrderedItems[i].Type = "Tombstone" + if e.Replies.OrderedItems[i].Attachment != nil { + e.Replies.OrderedItems[i].Preview.Href = "/public/removed.png" + e.Replies.OrderedItems[i].Preview.Name = "deleted" + e.Replies.OrderedItems[i].Preview.MediaType = "image/png" + e.Replies.OrderedItems[i].Attachment[0].Name = "deleted" + e.Replies.OrderedItems[i].Attachment[0].Href = "/public/removed.png" + e.Replies.OrderedItems[i].Attachment[0].MediaType = "image/png" + collection.OrderedItems[p].Replies.TotalImgs = collection.OrderedItems[p].Replies.TotalImgs - 1 + } + collection.OrderedItems[p].Replies.TotalItems = collection.OrderedItems[p].Replies.TotalItems - 1 + } + } + } + } + } +} + +func CreateLocalDeleteDB(db *sql.DB, id string, _type string) { + query := fmt.Sprintf("select id from removed where id='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "could not query removed") + + defer rows.Close() + + if rows.Next() { + var i string + + rows.Scan(&i) + + if i != "" { + query := fmt.Sprintf("update removed set type='%s' where id='%s'", _type, id) + + _, err := db.Exec(query) + + CheckError(err, "Could not update removed post") + + } + } else { + query := fmt.Sprintf("insert into removed (id, type) values ('%s', '%s')", id, _type) + + _, err := db.Exec(query) + + CheckError(err, "Could not insert removed post") + } +} + +func GetLocalDeleteDB(db *sql.DB) []Removed { + var deleted []Removed + + query := fmt.Sprintf("select id, type from removed") + + rows, err := db.Query(query) + + CheckError(err, "could not query removed") + + defer rows.Close() + + for rows.Next() { + var r Removed + + rows.Scan(&r.ID, &r.Type) + + deleted = append(deleted, r) + } + + return deleted +} + +func CreateLocalReportDB(db *sql.DB, id string, board string) { + query := fmt.Sprintf("select id, count from reported where id='%s' and board='%s'", id, board) + + rows, err := db.Query(query) + + CheckError(err, "could not query reported") + + defer rows.Close() + + if rows.Next() { + var i string + var count int + + rows.Scan(&i, &count) + + if i != "" { + count = count + 1 + query := fmt.Sprintf("update reported set count='%d' where id='%s'", count, id) + + _, err := db.Exec(query) + + CheckError(err, "Could not update reported post") + } + } else { + query := fmt.Sprintf("insert into reported (id, count, board) values ('%s', '%d', '%s')", id, 1, board) + + _, err := db.Exec(query) + + CheckError(err, "Could not insert reported post") + } + +} + +func GetLocalReportDB(db *sql.DB, board string) []Report { + var reported []Report + + query := fmt.Sprintf("select id, count from reported where board='%s'", board) + + rows, err := db.Query(query) + + CheckError(err, "could not query reported") + + defer rows.Close() + + for rows.Next() { + var r Report + + rows.Scan(&r.ID, &r.Count) + + reported = append(reported, r) + } + + return reported +} + +func CloseLocalReportDB(db *sql.DB, id string, board string) { + query := fmt.Sprintf("delete from reported where id='%s' and board='%s'", id, board) + + _, err := db.Exec(query) + + CheckError(err, "Could not delete local report from db") +} + +func GetActorFollowNameFromPath(path string) string{ + var actor string + + re := regexp.MustCompile("f\\w+-") + + actor = re.FindString(path) + + actor = strings.Replace(actor, "f", "", 1) + actor = strings.Replace(actor, "-", "", 1) + + return actor +} + +func GetActorsFollowFromName(actor Actor, name string) []Actor { + var followingActors []Actor + follow := GetActorCollection(actor.Following) + + re := regexp.MustCompile("\\w+?$") + + for _, e := range follow.Items { + if re.FindString(e.Id) == name { + actor := GetActor(e.Id) + followingActors = append(followingActors, actor) + } + } + + return followingActors +} + +func GetActorsFollowPostFromId(actors []Actor, id string) Collection{ + var collection Collection + + for _, e := range actors { + tempCol := GetActorCollection(e.Id + "/" + id) + if len(tempCol.OrderedItems) > 0 { + collection = tempCol + } + } + + return collection +} + +func CreateClientKey() string{ + + file, err := os.Create("clientkey") + + CheckError(err, "could not create client key in file") + + defer file.Close() + + key := CreateKey(32) + file.WriteString(key) + return key +} + +type ObjectBaseSortDesc []ObjectBase +func (a ObjectBaseSortDesc) Len() int { return len(a) } +func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated > a[j].Updated } +func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type ObjectBaseSortAsc []ObjectBase +func (a ObjectBaseSortAsc) Len() int { return len(a) } +func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published < a[j].Published } +func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + diff --git a/clientkey b/clientkey new file mode 100644 index 0000000..3438bee --- /dev/null +++ b/clientkey @@ -0,0 +1 @@ +166070629d7087e34b0cce5dadaf0a2d \ No newline at end of file diff --git a/databaseschema.psql b/databaseschema.psql index d2544cc..84eaf9d 100644 --- a/databaseschema.psql +++ b/databaseschema.psql @@ -204,4 +204,9 @@ formerType varchar(100) default '', size int default NULL, public boolean default false, CONSTRAINT fk_object FOREIGN KEY (object) REFERENCES cacheactivitystream(id) +); + +CREATE TABLE IF NOT EXISTS removed( +id varchar(100), +type varchar(25) ); \ No newline at end of file diff --git a/go.mod b/go.mod index e5b24f7..f3d9a97 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module github.com/FChannel/Server -require github.com/lib/pq v1.9.0 +require ( + github.com/gofrs/uuid v4.0.0+incompatible + github.com/gomodule/redigo v2.0.0+incompatible + github.com/lib/pq v1.9.0 +) diff --git a/go.sum b/go.sum index a4a764e..40d0876 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/main.go b/main.go index be84a4b..60f5701 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import "net/url" import "database/sql" import _ "github.com/lib/pq" import "math/rand" +import "html/template" import "time" import "regexp" import "os/exec" @@ -17,6 +18,8 @@ import "io/ioutil" import "mime/multipart" import "os" import "bufio" +import "io" +import "github.com/gofrs/uuid" var Port = ":" + GetConfigValue("instanceport") var TP = GetConfigValue("instancetp") @@ -31,22 +34,29 @@ var SiteEmailPassword = GetConfigValue("emailpass") var SiteEmailServer = GetConfigValue("emailserver") //mail.fchan.xyz var SiteEmailPort = GetConfigValue("emailport") //587 -type BoardAccess struct { - boards []string -} +var ldjson = "application/ld+json" +var activitystreams = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" func main() { if _, err := os.Stat("./public"); os.IsNotExist(err) { os.Mkdir("./public", 0755) - } + } + + InitCache() db := ConnectDB(); defer db.Close() go MakeCaptchas(db, 100) + + *Key = CreateClientKey() + + following := GetActorFollowingDB(db, Domain) + Boards = &following + // root actor is used to follow remote feeds that are not local //name, prefname, summary, auth requirements, restricted if GetConfigValue("instancename") != "" { @@ -55,7 +65,10 @@ func main() { // Allow access to public media folder fileServer := http.FileServer(http.Dir("./public")) - http.Handle("/public/", http.StripPrefix("/public", neuter(fileServer))) + http.Handle("/public/", http.StripPrefix("/public", neuter(fileServer))) + + javascriptFiles := http.FileServer(http.Dir("./static")) + http.Handle("/static/", http.StripPrefix("/static", neuter(javascriptFiles))) // main routing http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ @@ -79,12 +92,15 @@ func main() { var actorMain bool var actorInbox bool + var actorCatalog bool var actorOutbox bool + var actorPost bool var actorFollowing bool var actorFollowers bool var actorReported bool var actorVerification bool + var accept = r.Header.Get("Accept") if(actor.Id != ""){ if actor.Name == "main" { @@ -96,16 +112,26 @@ func main() { } else { actorMain = (path == "/" + actor.Name) actorInbox = (path == "/" + actor.Name + "/inbox") + actorCatalog = (path == "/" + actor.Name + "/catalog") actorOutbox = (path == "/" + actor.Name + "/outbox") actorFollowing = (path == "/" + actor.Name + "/following") actorFollowers = (path == "/" + actor.Name + "/followers") actorReported = (path == "/" + actor.Name + "/reported") - actorVerification = (path == "/" + actor.Name + "/verification") + actorVerification = (path == "/" + actor.Name + "/verification") + + re := regexp.MustCompile("/" + actor.Name + "/\\w+") + actorPost = re.MatchString(path) } } if mainActor { - GetActorInfo(w, db, Domain) + if accept == activitystreams || accept == ldjson { + GetActorInfo(w, db, Domain) + return + } + + IndexGet(w, r, db) + return } @@ -123,6 +149,7 @@ func main() { if method == "GET" { GetActorOutbox(w, r, db) } else if method == "POST" { + fmt.Println("parsing outbox req") ParseOutboxRequest(w, r, db) } else { w.WriteHeader(http.StatusForbidden) @@ -142,10 +169,29 @@ func main() { } if actorMain { - GetActorInfo(w, db, actor.Id) + if accept == activitystreams || accept == ldjson { + GetActorInfo(w, db, actor.Id) + return + } + + collection, valid := WantToServe(db, actor.Id) + if valid { + OutboxGet(w, r, db, collection) + } + + return + } + + if actorFollowing { + GetActorFollowing(w, db, actor.Id) return } + if actorFollowers { + GetActorFollowers(w, db, actor.Id) + return + } + if actorInbox { if method == "POST" { ParseInboxRequest(w, r, db) @@ -156,6 +202,14 @@ func main() { return } + if actorCatalog { + collection, valid := WantToServe(db, actor.Id) + if valid { + CatalogGet(w, r, db, collection) + } + return + } + if actorOutbox { if method == "GET" { GetActorOutbox(w, r, db) @@ -168,22 +222,12 @@ func main() { return } - if actorFollowing { - GetActorFollowing(w, db, actor.Id) - return - } - - if actorFollowers { - GetActorFollowers(w, db, actor.Id) - return - } - if actorReported { GetActorReported(w, r, db, actor.Id) return } - if actorVerification { + if actorVerification { if method == "POST" { p, _ := url.ParseQuery(r.URL.RawQuery) if len(p["email"]) > 0 { @@ -214,109 +258,518 @@ func main() { } return } + + //catch all + if actorPost { + if accept == activitystreams || accept == ldjson { + GetActorPost(w, db, path) + return + } + + PostGet(w, r, db) + return + } + + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + + }) + + http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request){ + + r.ParseMultipartForm(10 << 20) + + file, header, _ := r.FormFile("file") + + if(file != nil && header.Size > (7 << 20)){ + w.Write([]byte("7MB max file size")) + return + } + + if(r.FormValue("inReplyTo") == "" || file == nil) { + if(r.FormValue("comment") == "" && r.FormValue("subject") == ""){ + w.Write([]byte("Comment or Subject required")) + return + } + } + + if(r.FormValue("captcha") == "") { + w.Write([]byte("Captcha required")) + return + } + + b := bytes.Buffer{} + we := multipart.NewWriter(&b) + + if(file != nil){ + var fw io.Writer + + fw, err := we.CreateFormFile("file", header.Filename) + + CheckError(err, "error with form file create") + + _, err = io.Copy(fw, file) + + CheckError(err, "error with form file copy") + } + + reply := ParseCommentForReply(r.FormValue("comment")) + + for key, r0 := range r.Form { + if(key == "captcha") { + err := we.WriteField(key, r.FormValue("captchaCode") + ":" + r.FormValue("captcha")) + CheckError(err, "error with writing field") + }else{ + err := we.WriteField(key, r0[0]) + CheckError(err, "error with writing field") + } + } + + if(r.FormValue("inReplyTo") == "" && reply != ""){ + err := we.WriteField("inReplyTo", reply) + CheckError(err, "error with writing inReplyTo field") + } + + we.Close() + + req, err := http.NewRequest("POST", r.FormValue("sendTo"), &b) + + CheckError(err, "error with post form req") + + req.Header.Set("Content-Type", we.FormDataContentType()) + req.Header.Set("Authorization", "basic: " + *Key) + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with post form resp") + + defer resp.Body.Close() + + if(resp.StatusCode == 200){ + + body, _ := ioutil.ReadAll(resp.Body) + + var obj ObjectBase + obj = ParseOptions(r, obj) + for _, e := range obj.Option { + if(e == "noko" || e == "nokosage"){ + http.Redirect(w, r, TP + "" + Domain + "/" + r.FormValue("boardName") + "/" + string(body) , http.StatusMovedPermanently) + break + } + } + + http.Redirect(w, r, Domain + "/" + r.FormValue("boardName"), http.StatusMovedPermanently) + return + } + + if(resp.StatusCode == 403){ + w.Write([]byte("Wrong Captcha")) + return + } - collection := GetCollectionFromPath(db, Domain + "" + path) - if len(collection.OrderedItems) > 0 { - enc, _ := json.MarshalIndent(collection, "", "\t") - w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") - w.Write(enc) + http.Redirect(w, r, Domain + "/" + r.FormValue("boardName"), http.StatusMovedPermanently) + }) + + http.HandleFunc("/" + *Key + "/", func(w http.ResponseWriter, r *http.Request) { + + id, _ := GetPasswordFromSession(r) + + actor := GetActorFromPath(db, r.URL.Path, "/" + *Key + "/") + + if actor.Id == "" { + actor = GetActorFromDB(db, Domain) + } + + if id == "" || (id != actor.Id && id != Domain) { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + return + } + + re := regexp.MustCompile("/" + *Key + "/" + actor.Name + "/follow") + follow := re.MatchString(r.URL.Path) + + re = regexp.MustCompile("/" + *Key + "/" + actor.Name) + manage := re.MatchString(r.URL.Path) + + re = regexp.MustCompile("/" + *Key ) + admin := re.MatchString(r.URL.Path) + + re = regexp.MustCompile("/" + *Key + "/follow" ) + adminFollow := re.MatchString(r.URL.Path) + + if follow || adminFollow { + r.ParseForm() + + var followActivity Activity + + followActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + followActivity.Type = "Follow" + var nactor Actor + var obj ObjectBase + followActivity.Actor = &nactor + followActivity.Object = &obj + followActivity.Actor.Id = r.FormValue("actor") + var mactor Actor + followActivity.Object.Actor = &mactor + followActivity.Object.Actor.Id = r.FormValue("follow") + followActivity.To = append(followActivity.To, r.FormValue("follow")) + + enc, _ := json.Marshal(followActivity) + + req, err := http.NewRequest("POST", actor.Outbox, bytes.NewBuffer(enc)) + + CheckError(err, "error with follow req") + + _, pass := GetPasswordFromSession(r) + + req.Header.Set("Authorization", "Basic " + pass) + + req.Header.Set("Content-Type", activitystreams) + + _, err = http.DefaultClient.Do(req) + + CheckError(err, "error with add board follow resp") + + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + + } else if manage && actor.Name != "" { + t := template.Must(template.ParseFiles("./static/main.html", "./static/manage.html")) + + follow := GetActorCollection(actor.Following) + follower := GetActorCollection(actor.Followers) + reported := GetActorCollectionReq(r, actor.Id + "/reported") + + var following []string + var followers []string + var reports []Report + + for _, e := range follow.Items { + following = append(following, e.Id) + } + + for _, e := range follower.Items { + followers = append(followers, e.Id) + } + + for _, e := range reported.Items { + var r Report + r.Count = int(e.Size) + r.ID = e.Id + reports = append(reports, r) + } + + localReports := GetLocalReportDB(db, actor.Name) + + for _, e := range localReports { + var r Report + r.Count = e.Count + r.ID = e.ID + reports = append(reports, r) + } + + var adminData AdminPage + adminData.Following = following + adminData.Followers = followers + adminData.Reported = reports + adminData.Domain = Domain + + var boardCollection []Board + + boardCollection = GetBoardCollection(db) + + adminData.Title = "Manage /" + actor.Name + "/" + adminData.Boards = boardCollection + adminData.Board.Name = actor.Name + adminData.Actor = actor.Id + adminData.Key = *Key + adminData.Board.TP = TP + t.ExecuteTemplate(w, "layout", adminData) + + } else if admin || actor.Id == Domain { + + t := template.Must(template.ParseFiles("./static/main.html", "./static/nadmin.html")) + + actor := GetActor(Domain) + follow := GetActorCollection(actor.Following).Items + follower := GetActorCollection(actor.Followers).Items + + var following []string + var followers []string + + for _, e := range follow { + following = append(following, e.Id) + } + + for _, e := range follower { + followers = append(followers, e.Id) + } + + var adminData AdminPage + adminData.Following = following + adminData.Followers = followers + adminData.Actor = actor.Id + adminData.Key = *Key + adminData.Domain = Domain + adminData.Board.ModCred,_ = GetPasswordFromSession(r) + + var boardCollection []Board + + boardCollection = GetBoardCollection(db) + adminData.Boards = boardCollection + + t.ExecuteTemplate(w, "layout", adminData) + } + }) + + http.HandleFunc("/" + *Key + "/addboard", func(w http.ResponseWriter, r *http.Request) { + + var newActorActivity Activity + var board Actor + r.ParseForm() + + actor := GetActorFromDB(db, Domain) + + var restrict bool + if r.FormValue("restricted") == "True" { + restrict = true } else { - w.WriteHeader(http.StatusForbidden) - w.Write([]byte("404 no path")) + restrict = false } + + board.Name = r.FormValue("name") + board.PreferredUsername = r.FormValue("prefname") + board.Summary = r.FormValue("summary") + board.Restricted = restrict + + newActorActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + newActorActivity.Type = "New" + var nactor Actor + var nobj ObjectBase + newActorActivity.Actor = &nactor + newActorActivity.Object = &nobj + newActorActivity.Actor.Id = actor.Id + newActorActivity.Object.Actor = &board + + + enc, _ := json.Marshal(newActorActivity) + + req, err := http.NewRequest("POST", actor.Outbox, bytes.NewBuffer(enc)) + + CheckError(err, "error with add board follow req") + + _, pass := GetPasswordFromSession(r) + req.Header.Set("Authorization", "Basic " + pass) + req.Header.Set("Content-Type", activitystreams) + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with add board follow resp") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var respActor Actor + + err = json.Unmarshal(body, &respActor) + + CheckError(err, "error getting actor from body in new board") + //update board list with new instances following + if resp.StatusCode == 200 { + var board []ObjectBase + var item ObjectBase + var removed bool = false + + item.Id = respActor.Id + for _, e := range *Boards { + if e.Id != item.Id { + board = append(board, e) + } else { + removed = true + } + } + + if !removed { + board = append(board, item) + } + + *Boards = board + } + + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) }) + http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){ + if(r.Method == "POST") { + r.ParseForm() + identifier := r.FormValue("id") + code := r.FormValue("code") + + var verify Verify + verify.Identifier = identifier + verify.Code = code + + j, _ := json.Marshal(&verify) + + req, err := http.NewRequest("POST", Domain + "/auth", bytes.NewBuffer(j)) + + CheckError(err, "error making verify req") + + req.Header.Set("Content-Type", activitystreams) + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error getting verify resp") + + defer resp.Body.Close() + + rBody, _ := ioutil.ReadAll(resp.Body) + + body := string(rBody) + + if(resp.StatusCode != 200) { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "wrong password " + verify.Code) + } else { + + sessionToken, _ := uuid.NewV4() + + _, err := cache.Do("SETEX", sessionToken, "86400", body + "|" + verify.Code) + if err != nil { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: sessionToken.String(), + Expires: time.Now().Add(60 * 60 * 24 * 7 * time.Second), + }) + + http.Redirect(w, r, "/", http.StatusSeeOther) + } + } else { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + } + }) + http.HandleFunc("/getcaptcha", func(w http.ResponseWriter, r *http.Request){ w.Write([]byte(GetRandomCaptcha(db))) }) http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request){ - values := r.URL.Query().Get("id") - - header := r.Header.Get("Authorization") - - auth := strings.Split(header, " ") + id := r.URL.Query().Get("id") + board := r.URL.Query().Get("board") + actor := GetActorFromPath(db, id, "/") + _, auth := GetPasswordFromSession(r) - if len(values) < 1 || !IsIDLocal(db, values) || len(auth) < 2 { + if id == "" || auth == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return } - actor := GetActorFromPath(db, values, "/") - - if !HasAuth(db, auth[1], actor.Id) { + if !HasAuth(db, auth, actor.Id) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return - } + } + + if !IsIDLocal(db, id) { + CreateLocalDeleteDB(db, id, "post") + CloseLocalReportDB(db, id, board) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return + } + var obj ObjectBase - obj.Id = values + obj.Id = id + + count, _ := GetObjectRepliesDBCount(db, obj) - count, _ := GetObjectRepliesDBCount(db, obj) if count == 0 { DeleteObject(db, obj.Id) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return } else { DeleteObjectAndReplies(db, obj.Id) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return } - w.Write([]byte("")) + + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) }) http.HandleFunc("/deleteattach", func(w http.ResponseWriter, r *http.Request){ - values := r.URL.Query().Get("id") - - header := r.Header.Get("Authorization") + id := r.URL.Query().Get("id") - auth := strings.Split(header, " ") + _, auth := GetPasswordFromSession(r) - if len(values) < 1 || !IsIDLocal(db, values) || len(auth) < 2 { + if id == "" || auth == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return } - actor := GetActorFromPath(db, values, "/") + actor := GetActorFromPath(db, id, "/") - if !HasAuth(db, auth[1], actor.Id) { + if !HasAuth(db, auth, actor.Id) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return + } + + if !IsIDLocal(db, id) { + CreateLocalDeleteDB(db, id, "attachment") + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return } - id := values DeleteAttachmentFromFile(db, id) - DeletePreviewFromFile(db, id) - w.Write([]byte("")) + DeletePreviewFromFile(db, id) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) }) http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request){ id := r.URL.Query().Get("id") close := r.URL.Query().Get("close") - header := r.Header.Get("Authorization") + board := r.URL.Query().Get("board") + actor := GetActorFromPath(db, id, "/") + _, auth := GetPasswordFromSession(r) - auth := strings.Split(header, " ") - if close == "1" { - if !IsIDLocal(db, id) || len(auth) < 2 { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("")) - return - } + fmt.Println(actor.Id) - actor := GetActorFromPath(db, id, "/") + if id == "" || auth == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } - if !HasAuth(db, auth[1], actor.Id) { + if close == "1" { + if !HasAuth(db, auth, actor.Id) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return } + if !IsIDLocal(db, id) { + CloseLocalReportDB(db, id, board) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return + } + reported := DeleteReportActivity(db, id) if reported { - w.Write([]byte("")) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) return } @@ -324,11 +777,17 @@ func main() { w.Write([]byte("")) return } + + if !IsIDLocal(db, id) { + fmt.Println("not local") + CreateLocalReportDB(db, id, board) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) + return + } reported := ReportActivity(db, id) - if reported { - w.Write([]byte("")) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) return } @@ -336,7 +795,7 @@ func main() { w.Write([]byte("")) }) - http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){ + http.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request){ var verify Verify defer r.Body.Close() @@ -358,7 +817,8 @@ func main() { }) fmt.Println("Server for " + Domain + " running on port " + Port) - + + fmt.Println("Mod key: " + *Key) PrintAdminAuth(db) http.ListenAndServe(Port, nil) @@ -543,6 +1003,15 @@ func GetActorInfo(w http.ResponseWriter, db *sql.DB, id string) { w.Write(enc) } +func GetActorPost(w http.ResponseWriter, db *sql.DB, path string) { + collection := GetCollectionFromPath(db, Domain + "" + path) + if len(collection.OrderedItems) > 0 { + enc, _ := json.MarshalIndent(collection, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) + } +} + func CreateObject(objType string) ObjectBase { var nObj ObjectBase @@ -694,17 +1163,16 @@ func ParseCommentForReplies(comment string) []ObjectBase { return validLinks } - func CheckValidActivity(id string) (Collection, bool) { req, err := http.NewRequest("GET", id, nil) - + if err != nil { fmt.Println("error with request") panic(err) } - req.Header.Set("Accept", "json/application/activity+json") + req.Header.Set("Accept", activitystreams) resp, err := http.DefaultClient.Do(req) @@ -736,14 +1204,20 @@ func GetActor(id string) Actor { var respActor Actor + if id == "" { + return respActor + } + req, err := http.NewRequest("GET", id, nil) CheckError(err, "error with getting actor req") + req.Header.Set("Accept", activitystreams) + resp, err := http.DefaultClient.Do(req) if err != nil { - fmt.Println("error with getting actor resp") + fmt.Println("error with getting actor resp " + id) return respActor } @@ -761,23 +1235,34 @@ func GetActor(id string) Actor { func GetActorCollection(collection string) Collection { var nCollection Collection + if collection == "" { + return nCollection + } + req, err := http.NewRequest("GET", collection, nil) CheckError(err, "error with getting actor collection req " + collection) + req.Header.Set("Accept", activitystreams) + resp, err := http.DefaultClient.Do(req) - CheckError(err, "error with getting actor collection resp " + collection) + if err != nil { + fmt.Println("error with getting actor collection resp " + collection) + return nCollection + } - if resp.StatusCode == 200 { - defer resp.Body.Close() + defer resp.Body.Close() + if resp.StatusCode == 200 { body, _ := ioutil.ReadAll(resp.Body) - err = json.Unmarshal(body, &nCollection) - - CheckError(err, "error getting actor collection from body " + collection) + if len(body) > 0 { + err = json.Unmarshal(body, &nCollection) + + CheckError(err, "error getting actor collection from body " + collection) + } } return nCollection @@ -789,18 +1274,18 @@ func IsValidActor(id string) (Actor, bool) { CheckError(err, "error with valid actor request") - req.Header.Set("Accept", "json/application/activity+json") + req.Header.Set("Accept", activitystreams) resp, err := http.DefaultClient.Do(req) CheckError(err, "error with valid actor response") + defer resp.Body.Close() + if resp.StatusCode == 403 { return respCollection, false; } - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) err = json.Unmarshal(body, &respCollection) @@ -816,8 +1301,6 @@ func IsValidActor(id string) (Actor, bool) { return respCollection, false; } - - func IsActivityLocal(db *sql.DB, activity Activity) bool { for _, e := range activity.To { if GetActorFromDB(db, e).Id != "" { @@ -839,12 +1322,8 @@ func IsActivityLocal(db *sql.DB, activity Activity) bool { } func IsIDLocal(db *sql.DB, id string) bool { - - if GetActivityFromDB(db, id).OrderedItems != nil { - return true - } - - return false + activity := GetActivityFromDB(db, id) + return len(activity.OrderedItems) > 0 } func IsObjectLocal(db *sql.DB, id string) bool { @@ -995,7 +1474,7 @@ func GetActorReported(w http.ResponseWriter, r *http.Request, db *sql.DB, id str following.Items = GetActorReportedDB(db, id) enc, _ := json.MarshalIndent(following, "", "\t") - w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Header().Set("Content-Type", activitystreams) w.Write(enc) } @@ -1008,11 +1487,13 @@ func MakeActivityRequest(activity Activity) { actor := GetActor(e) if actor.Inbox != "" { - req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j)) + req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j)) - CheckError(err, "error with sending activity req to") + req.Header.Set("Content-Type", activitystreams) - _, err = http.DefaultClient.Do(req) + CheckError(err, "error with sending activity req to") + + _, err = http.DefaultClient.Do(req) CheckError(err, "error with sending activity resp to") } @@ -1026,6 +1507,8 @@ func GetCollectionFromID(id string) Collection { CheckError(err, "could not get collection from id req") + req.Header.Set("Accept", activitystreams) + resp, err := http.DefaultClient.Do(req) if err != nil { @@ -1052,6 +1535,8 @@ func GetActorFromID(id string) Actor { CheckError(err, "error getting actor from id req") + req.Header.Set("Accept", activitystreams) + resp, err := http.DefaultClient.Do(req) CheckError(err, "error getting actor from id resp") @@ -1088,7 +1573,6 @@ func GetConfigValue(value string) string{ return "" } - func PrintAdminAuth(db *sql.DB){ query := fmt.Sprintf("select identifier, code from boardaccess where board='%s' and type='admin'", Domain) @@ -1114,7 +1598,6 @@ func IsInStringArray(array []string, value string) bool { return false } - func GetUniqueFilename(_type string) string { id := RandomID(8) file := "/public/" + id + "." + _type @@ -1242,3 +1725,73 @@ func UpdateObjectWithPreview(db *sql.DB, id string, preview string) { CheckError(err, "could not update activity stream with preview") } + +func ParseCommentForReply(comment string) string { + + re := regexp.MustCompile("(>>)(https://|http://)?(www\\.)?.+\\/\\w+") + match := re.FindAllStringSubmatch(comment, -1) + + var links []string + + for i:= 0; i < len(match); i++ { + str := strings.Replace(match[i][0], ">>", "", 1) + links = append(links, str) + } + + if(len(links) > 0){ + _, isValid := CheckValidActivity(links[0]) + + if(isValid) { + return links[0] + } + } + + return "" +} + +func GetActorByName(db *sql.DB, name string) Actor { + var actor Actor + for _, e := range *Boards { + boardActor := GetActorFromDB(db, e.Id) + if boardActor.Id == "" { + boardActor = GetRemoteActor(e.Id) + } + + if boardActor.Name == name { + actor = boardActor + } + } + + return actor +} + +func GetActorCollectionReq(r *http.Request, collection string) Collection { + var nCollection Collection + + req, err := http.NewRequest("GET", collection, nil) + + CheckError(err, "error with getting actor collection req " + collection) + + _, pass := GetPasswordFromSession(r) + + req.Header.Set("Accept", activitystreams) + + req.Header.Set("Authorization", "Basic " + pass) + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with getting actor collection resp " + collection) + + if resp.StatusCode == 200 { + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &nCollection) + + CheckError(err, "error getting actor collection from body " + collection) + } + + return nCollection +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..3478802 --- /dev/null +++ b/session.go @@ -0,0 +1,90 @@ +package main + + +import ( + "fmt" + "net/http" + "bufio" + "os" + "strings" + "github.com/gomodule/redigo/redis" +) + +var cache redis.Conn + +func InitCache() { + conn, err := redis.DialURL("redis://localhost") + if err != nil { + panic(err) + } + cache = conn +} + +func CheckSession(w http.ResponseWriter, r *http.Request) (interface{}, error){ + + c, err := r.Cookie("session_token") + + if err != nil { + if err == http.ErrNoCookie { + w.WriteHeader(http.StatusUnauthorized) + return nil, err + } + + w.WriteHeader(http.StatusBadRequest) + return nil, err + } + + sessionToken := c.Value + + response, err := cache.Do("GET", sessionToken) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return nil, err + } + if response == nil { + w.WriteHeader(http.StatusUnauthorized) + return nil, err + } + + return response, nil +} + +func GetClientKey() string{ + file, err := os.Open("clientkey") + + CheckError(err, "could not open client key in file") + + defer file.Close() + + scanner := bufio.NewScanner(file) + var line string + for scanner.Scan() { + line = fmt.Sprintf("%s", scanner.Text()) + } + + return line +} + +func GetPasswordFromSession(r *http.Request) (string, string) { + + c, err := r.Cookie("session_token") + + if err != nil { + return "", "" + } + + sessionToken := c.Value + + response, err := cache.Do("GET", sessionToken) + + if CheckError(err, "could not get session from cache") != nil { + return "", "" + } + + token := fmt.Sprintf("%s", response) + + parts := strings.Split(token, "|") + + return parts[0], parts[1] +} diff --git a/static/admin.html b/static/admin.html new file mode 100644 index 0000000..f7db70c --- /dev/null +++ b/static/admin.html @@ -0,0 +1,77 @@ + + + + + + +
+

Add Board

+
+
+
+
+
+
+
+
+ +
+ + + +
+ +
+

Following

+
+
+
+ +
+ +
+ + + + + + + + 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" }} + +{{ 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 @@ + + + + {{ .Title }} + + + + + + {{ $board := .Board }} + {{ if $board.IsMod }} + [Manage Board] + {{ end }} +
+

/{{ $board.Name }}/ - {{ $board.PrefName }}

+
+
+
+
+
+
+
+
+
+ + + + +

+
+
+
+ +
+
+
+ +
+ +
+ +
+ {{ range .Posts }} +
+ {{ if $board.IsMod }} + [Delete Post] + {{ end }} + {{ if .Attachment }} + {{ if $board.IsMod }} + [Delete Attachment] + {{ end }} + +
+ + + + {{ end }} +
+ {{ $replies := .Replies }} + R: {{ $replies.TotalItems }}{{ if $replies.TotalImgs }}/ A: {{ $replies.TotalImgs }}{{ end }} + {{ if .Name }} + {{ .Name }} + {{ end }} + {{ if .Content }} + {{.Content}} + {{ end }} +
+
+
+ + {{ end }} +
+
+ +
+ + + 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" }} +
+

{{ .Title }}

+

{{.Message}}

+
+{{ 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, '>>' + shortURL(actorName, link) + isOP + ''); + + }) + } + + re = /^>.+/gm; + + match = newContent.match(re); + if(match) + { + match.forEach(function(quote, i) { + newContent = newContent.replace(quote, '' + quote + ''); + }) + } + + 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" }} + + + + + {{ .Title }} + + {{ template "header" . }} + + + + {{ if .Board.ModCred }} + {{ if eq .Board.ModCred .Board.Domain .Board.Actor }} + [Manage Board] + {{ end }} + {{ end }} + {{ template "top" . }} + + {{ template "content" . }} + + {{ template "bottom" . }} + + + +{{ 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" }} + +{{ end }} +{{ define "top" }}{{ end }} +{{ define "content" }} +
+

Manage /{{ .Board.Name }}/

+ + +
+[Return] +
+

Subscribed

+
+
+ +
+ +
+ +
+ + + + +{{ end }} +{{ define "bottom" }}{{ end }} + +{{ define "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" }} +
+

Add Board

+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+

Following

+
+
+
+ +
+ +
+ + + + +{{ end }} +{{ define "bottom" }}{{ end }} + +{{ define "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" }} + +{{ end }} + +{{ define "content" }} +{{ $board := .Board }} +
+ +
+ +
+ {{ range .Posts }} +
+ {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Post] + {{ end }} + {{ if .Attachment }} + {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Attachment] + {{ end }} + +
+ + {{ end }} +
+ {{ $replies := .Replies }} + R: {{ $replies.TotalItems }}{{ if $replies.TotalImgs }}/ A: {{ $replies.TotalImgs }}{{ end }} + {{ if .Name }} + {{ .Name }} + {{ end }} + + {{ if .Content }} + {{.Content}} + {{ end }} + +
+
+
+ + {{ end }} +
+
+ +
+{{ 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" }} + +{{ end }} + +{{ define "content" }} +{{ $board := .Board }} + +
+ +
+ +{{ template "posts" . }} + +
+ +
+ [Post a Reply] + {{ $replies := (index .Posts 0).Replies }} + {{ $replies.TotalItems }} / {{ $replies.TotalImgs }} +
+
+{{ end }} + +{{ define "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" }} + +{{ end }} + + +{{ define "content" }} +{{ $board := .Board }} +
+ + +{{ template "posts" . }} + +
+ +
+{{ if gt .TotalPage 0 }} +{{ $totalPage := .TotalPage }} + +{{ end }} +{{ end }} + +{{ define "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 "" }} +
+{{ end }} +
+
+ {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Post] + {{ end }} + {{ if .Attachment }} + {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Attachment] + {{ end }} + File: {{ (index .Attachment 0).Name }}({{ (index .Attachment 0).Size }}) +
+ + {{ end }} + {{ .Name }}{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}{{ .Published }} No. {{ .Id }} {{ if ne .Type "Tombstone" }}[Report]{{ end }} +

{{.Content}}

+ {{ if .Replies }} + {{ $replies := .Replies }} + {{ if gt $replies.TotalItems 5 }} + {{ $replies.TotalItems }} replies{{ if gt $replies.TotalImgs 0}} and {{ $replies.TotalImgs }} images{{ end }}, Click here to view all. + {{ end }} + {{ range $replies.OrderedItems }} +
+
+
>>
+
+ {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Post] + {{ end }} + {{ if .Attachment }} + {{ if eq $board.ModCred $board.Domain $board.Actor }} + [Delete Attachment] + {{ end }} + File {{ (index .Attachment 0).Name }} ({{ (index .Attachment 0).Size }}) +
+ + {{ end }} + {{ .Name }}{{ if .AttributedTo }} {{.AttributedTo }} {{ else }} Anonymous {{ end }}{{ .Published }} No. {{ .Id }} {{ if ne .Type "Tombstone" }}[Report]{{ end }} + {{ $parentId := .Id }} + {{ if .Replies.OrderedItems }} + {{ range .Replies.OrderedItems }} + + + {{ end }} + {{ end }} +

{{.Content}}

+
+
+
+ + {{ end }} + {{ end }} +
+
+ +{{ 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" }} +
+

/{{ .Board.Name }}/ - {{ .Board.PrefName }}

+

{{ .Board.Summary }}

+ {{ $len := len .Posts }} + {{ if .Board.InReplyTo }} +

[Post a Reply]

+ {{ else }} +

[Start a New Thread]

+ {{ end }} + +
+{{ 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 @@ + + + + + + +
+
+ +
+ +
+ +
+
+ + -- cgit v1.2.3