diff options
-rw-r--r-- | client.go | 102 | ||||
-rw-r--r-- | database.go | 136 | ||||
-rw-r--r-- | databaseschema.psql | 9 | ||||
-rw-r--r-- | main.go | 99 | ||||
-rw-r--r-- | static/admin.html | 2 | ||||
-rw-r--r-- | static/anews.html | 46 | ||||
-rw-r--r-- | static/bottom.html | 8 | ||||
-rw-r--r-- | static/faq.html | 4 | ||||
-rw-r--r-- | static/index.html | 55 | ||||
-rw-r--r-- | static/js/footerscript.js | 24 | ||||
-rw-r--r-- | static/js/posts.js | 273 | ||||
-rw-r--r-- | static/js/timer.js | 38 | ||||
-rw-r--r-- | static/main.html | 23 | ||||
-rw-r--r-- | static/manage.html | 2 | ||||
-rw-r--r-- | static/nadmin.html | 15 | ||||
-rw-r--r-- | static/news.html | 32 | ||||
-rw-r--r-- | static/npost.html | 2 | ||||
-rw-r--r-- | static/top.html | 2 | ||||
-rw-r--r-- | verification.go | 105 |
19 files changed, 810 insertions, 167 deletions
@@ -8,6 +8,7 @@ import "strings" import "strconv" import "sort" import "regexp" +import "time" var Key *string = new(string) @@ -35,8 +36,7 @@ type Board struct{ type PageData struct { Title string - Message string - MessageHTML template.HTML + PreferredUsername string Board Board Pages []int CurrentPage int @@ -48,6 +48,8 @@ type PageData struct { Instance Actor InstanceIndex []ObjectBase ReturnTo string + NewsItems []NewsItem + BoardRemainer []int } type AdminPage struct { @@ -66,6 +68,7 @@ type AdminPage struct { type Report struct { ID string Count int + Reason string } type Removed struct { @@ -74,8 +77,18 @@ type Removed struct { Board string } + +type NewsItem struct { + Title string + Content template.HTML + Time int +} + func IndexGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { - t := template.Must(template.ParseFiles("./static/main.html", "./static/index.html")) + t := template.Must(template.New("").Funcs(template.FuncMap{ + "mod": func(i, j int) bool { return i%j == 0 }, + "sub": func (i, j int) int { return i - j }, + "unixtoreadable": func(u int) string { return time.Unix(int64(u), 0).Format("Jan 02, 2006") }}).ParseFiles("./static/main.html", "./static/index.html")) actor := GetActorFromDB(db, Domain) @@ -91,16 +104,79 @@ func IndexGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { data.Board.Actor = actor data.Board.Post.Actor = actor.Id data.Board.Restricted = actor.Restricted + //almost certainly there is a better algorithm for this but the old one was wrong + //and I suck at math. This works at least. + data.BoardRemainer = make([]int, 3-(len(data.Boards) % 3)) + if(len(data.BoardRemainer) == 3){ + data.BoardRemainer = make([]int, 0) + } data.InstanceIndex = GetCollectionFromReq("https://fchan.xyz/followers").Items + data.NewsItems = getNewsFromDB(db, 3) + + t.ExecuteTemplate(w, "layout", data) +} + +func NewsGet(w http.ResponseWriter, r *http.Request, db *sql.DB, timestamp int) { + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }, + "unixtoreadable": func(u int) string { return time.Unix(int64(u), 0).Format("Jan 02, 2006") }}).ParseFiles("./static/main.html", "./static/news.html")) + + actor := GetActorFromDB(db, Domain) + + var data PageData + data.PreferredUsername = actor.PreferredUsername + data.Boards = Boards + data.Board.Name = "" + data.Key = *Key + data.Board.Domain = Domain + data.Board.ModCred, _ = GetPasswordFromSession(r) + data.Board.Actor = actor + data.Board.Post.Actor = actor.Id + data.Board.Restricted = actor.Restricted + data.NewsItems = []NewsItem{NewsItem{}} + + var err error + data.NewsItems[0], err = getNewsItemFromDB(db, timestamp) + + if err != nil { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + return + } + + data.Title = actor.PreferredUsername + ": " + data.NewsItems[0].Title + t.ExecuteTemplate(w, "layout", data) +} +func AllNewsGet(w http.ResponseWriter, r *http.Request, db *sql.DB) { + t := template.Must(template.New("").Funcs(template.FuncMap{ + "mod": func(i, j int) bool { return i%j == 0 }, + "sub": func (i, j int) int { return i - j }, + "unixtoreadable": func(u int) string { return time.Unix(int64(u), 0).Format("Jan 02, 2006") }}).ParseFiles("./static/main.html", "./static/anews.html")) - t.ExecuteTemplate(w, "layout", data) + actor := GetActorFromDB(db, Domain) + + var data PageData + data.PreferredUsername = actor.PreferredUsername + data.Title = actor.PreferredUsername + " News" + data.Boards = Boards + data.Board.Name = "" + data.Key = *Key + data.Board.Domain = Domain + data.Board.ModCred, _ = GetPasswordFromSession(r) + data.Board.Actor = actor + data.Board.Post.Actor = actor.Id + data.Board.Restricted = actor.Restricted + data.NewsItems = getNewsFromDB(db, 0) + + t.ExecuteTemplate(w, "layout", data) } func OutboxGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Collection){ + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/nposts.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) - t := template.Must(template.ParseFiles("./static/main.html", "./static/nposts.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) actor := collection.Actor @@ -148,9 +224,8 @@ func OutboxGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection Co } 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")) - + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/ncatalog.html", "./static/top.html")) actor := collection.Actor var returnData PageData @@ -183,8 +258,8 @@ func CatalogGet(w http.ResponseWriter, r *http.Request, db *sql.DB, collection C } 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")) + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/npost.html", "./static/top.html", "./static/bottom.html", "./static/posts.html")) path := r.URL.Path actor := GetActorFromPath(db, path, "/") @@ -256,10 +331,11 @@ func GetBoardCollection(db *sql.DB) []Board { if boardActor.Id == "" { boardActor = FingerActor(e.Id) } - board.Name = "/" + boardActor.Name + "/" + board.Name = boardActor.Name board.PrefName = boardActor.PreferredUsername board.Location = "/" + boardActor.Name board.Actor = boardActor + board.Restricted = boardActor.Restricted collection = append(collection, board) } @@ -409,7 +485,7 @@ func CreateLocalReportDB(db *sql.DB, id string, board string, reason string) { func GetLocalReportDB(db *sql.DB, board string) []Report { var reported []Report - query := `select id, count from reported where board=$1` + query := `select id, count, reason from reported where board=$1` rows, err := db.Query(query, board) @@ -420,7 +496,7 @@ func GetLocalReportDB(db *sql.DB, board string) []Report { for rows.Next() { var r Report - rows.Scan(&r.ID, &r.Count) + rows.Scan(&r.ID, &r.Count, &r.Reason) reported = append(reported, r) } diff --git a/database.go b/database.go index 9d5d721..5668a4d 100644 --- a/database.go +++ b/database.go @@ -1,17 +1,21 @@ package main -import "fmt" -import "database/sql" -import _ "github.com/lib/pq" -import "time" -import "os" -import "strings" -import "sort" +import ( + "database/sql" + "fmt" + "os" + "sort" + "strings" + "time" + "html/template" + + _ "github.com/lib/pq" +) func GetActorFromDB(db *sql.DB, id string) Actor { - var nActor Actor + var nActor Actor - query :=`select type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary, publickeypem from actor where id=$1` + query :=`select type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary, publickeypem from actor where id=$1` rows, err := db.Query(query, id) @@ -27,6 +31,10 @@ func GetActorFromDB(db *sql.DB, id string) Actor { } nActor.PublicKey = GetActorPemFromDB(db, publicKeyPem) + if nActor.Id != "" && nActor.PublicKey.PublicKeyPem == ""{ + err = CreatePublicKeyFromPrivate(db, &nActor, publicKeyPem) + CheckError(err, "error creating public key from private") + } return nActor } @@ -49,7 +57,10 @@ func GetActorByNameFromDB(db *sql.DB, name string) Actor { CheckError(err, "error with actor from db scan ") } - nActor.PublicKey = GetActorPemFromDB(db, publicKeyPem) + if nActor.Id != "" && nActor.PublicKey.PublicKeyPem == ""{ + err = CreatePublicKeyFromPrivate(db, &nActor, publicKeyPem) + CheckError(err, "error creating public key from private") + } return nActor } @@ -1440,7 +1451,7 @@ func GetActorReportedTotal(db *sql.DB, id string) int { func GetActorReportedDB(db *sql.DB, id string) []ObjectBase { var nObj []ObjectBase - query := `select id, count from reported where board=$1` + query := `select id, count, reason from reported where board=$1` rows, err := db.Query(query, id) @@ -1451,7 +1462,7 @@ func GetActorReportedDB(db *sql.DB, id string) []ObjectBase { for rows.Next() { var obj ObjectBase - rows.Scan(&obj.Id, &obj.Size) + rows.Scan(&obj.Id, &obj.Size, &obj.Content) nObj = append(nObj, obj) } @@ -1470,13 +1481,32 @@ func GetActorPemFromDB(db *sql.DB, pemID string) PublicKeyPem { defer rows.Close() rows.Next() rows.Scan(&pem.Id, &pem.Owner, &pem.PublicKeyPem) - f, _ := os.ReadFile(pem.PublicKeyPem) + f, err := os.ReadFile(pem.PublicKeyPem) + if err != nil{ + pem.PublicKeyPem = "" + return pem + } pem.PublicKeyPem = strings.ReplaceAll(string(f), "\r\n", `\n`) return pem } +func GetActorPemFileFromDB(db *sql.DB, pemID string) string{ + query := `select file from publickeypem where id=$1` + rows, err := db.Query(query, pemID) + + CheckError(err, "could not get public key filename from database") + + var file string + + defer rows.Close() + rows.Next() + rows.Scan(&file) + + return file +} + func MarkObjectSensitive(db *sql.DB, id string, sensitive bool) { var query = `update activitystream set sensitive=$1 where id=$2` _, err := db.Exec(query, sensitive, id) @@ -1488,3 +1518,83 @@ func MarkObjectSensitive(db *sql.DB, id string, sensitive bool) { CheckError(err, "error updating sensitive object in cacheactivitystream") } + +//if limit less than 1 return all news items +func getNewsFromDB(db *sql.DB, limit int) []NewsItem { + var news []NewsItem + + var query string + if(limit > 0) { + query =`select title, content, time from newsItem order by time desc limit $1` + } else { + query =`select title, content, time from newsItem order by time desc` + } + + var rows *sql.Rows + var err error + if(limit > 0) { + rows, err = db.Query(query, limit) + } else { + rows, err = db.Query(query) + } + + + if CheckError(err, "could not get news from db query") != nil { + return news + } + + defer rows.Close() + for rows.Next() { + n := NewsItem{} + var content string + err = rows.Scan(&n.Title, &content, &n.Time) + if CheckError(err, "error scanning news from db") != nil { + return news + } + + content = strings.ReplaceAll(content, "\n", "<br>") + n.Content = template.HTML(content) + + news = append(news, n) + } + + return news +} + +func getNewsItemFromDB(db *sql.DB, timestamp int) (NewsItem, error) { + var news NewsItem + var content string + query := `select title, content, time from newsItem where time=$1 limit 1` + + rows, err := db.Query(query, timestamp) + + if err != nil { + return news, err + } + + defer rows.Close() + rows.Next() + err = rows.Scan(&news.Title, &content, &news.Time) + + if err != nil { + return news, err + } + + content = strings.ReplaceAll(content, "\n", "<br>") + news.Content = template.HTML(content) + + return news, nil +} + +func deleteNewsItemFromDB(db *sql.DB, timestamp int) { + query := `delete from newsItem where time=$1` + db.Exec(query, timestamp) +} + +func WriteNewsToDB(db *sql.DB, news NewsItem) { + query := `insert into newsItem (title, content, time) values ($1, $2, $3)` + + _, err := db.Exec(query, news.Title, news.Content, time.Now().Unix()) + + CheckError(err, "error writing news item") +} diff --git a/databaseschema.psql b/databaseschema.psql index e12813e..02c229b 100644 --- a/databaseschema.psql +++ b/databaseschema.psql @@ -216,7 +216,14 @@ owner varchar(100), file varchar(100) ); +CREATE TABLE IF NOT EXISTS newsItem( +title text, +content text, +time bigint +); + ALTER TABLE actor ADD COLUMN IF NOT EXISTS publicKeyPem varchar(100) default ''; ALTER TABLE activitystream ADD COLUMN IF NOT EXISTS sensitive boolean default false; -ALTER TABLE cacheactivitystream ADD COLUMN IF NOT EXISTS sensitive boolean default false;
\ No newline at end of file +ALTER TABLE cacheactivitystream ADD COLUMN IF NOT EXISTS sensitive boolean default false; + @@ -290,6 +290,27 @@ func main() { w.WriteHeader(http.StatusForbidden) w.Write([]byte("404 no path")) }) + + http.HandleFunc("/news/", func(w http.ResponseWriter, r *http.Request){ + timestamp := r.URL.Path[6:] + + if(len(timestamp) < 2) { + AllNewsGet(w, r, db) + return + } + + if timestamp[len(timestamp)-1:] == "/" { + timestamp = timestamp[:len(timestamp)-1] + } + + ts, err := strconv.Atoi(timestamp) + if err != nil { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } else { + NewsGet(w, r, db, ts) + } + }) http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request){ @@ -480,7 +501,8 @@ func main() { http.Redirect(w, r, "/" + *Key + "/" + redirect, http.StatusSeeOther) } else if manage && actor.Name != "" { - t := template.Must(template.ParseFiles("./static/main.html", "./static/manage.html")) + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/manage.html")) follow := GetActorCollection(actor.Following) follower := GetActorCollection(actor.Followers) @@ -502,6 +524,7 @@ func main() { var r Report r.Count = int(e.Size) r.ID = e.Id + r.Reason = e.Content reports = append(reports, r) } @@ -511,6 +534,7 @@ func main() { var r Report r.Count = e.Count r.ID = e.ID + r.Reason = e.Reason reports = append(reports, r) } @@ -533,8 +557,8 @@ func main() { t.ExecuteTemplate(w, "layout", adminData) } else if admin || actor.Id == Domain { - - t := template.Must(template.ParseFiles("./static/main.html", "./static/nadmin.html")) + t := template.Must(template.New("").Funcs(template.FuncMap{ + "sub": func (i, j int) int { return i - j }}).ParseFiles("./static/main.html", "./static/nadmin.html")) actor := GetActor(Domain) follow := GetActorCollection(actor.Following).Items @@ -569,12 +593,21 @@ func main() { http.HandleFunc("/" + *Key + "/addboard", func(w http.ResponseWriter, r *http.Request) { + id, _ := GetPasswordFromSession(r) + + actor := GetActorFromDB(db, Domain) + + + if id == "" || (id != actor.Id && id != Domain) { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + return + } + var newActorActivity Activity var board Actor r.ParseForm() - actor := GetActorFromDB(db, Domain) - var restrict bool if r.FormValue("restricted") == "True" { restrict = true @@ -602,6 +635,56 @@ func main() { MakeActivityRequestOutbox(db, newActorActivity) http.Redirect(w, r, "/" + *Key, http.StatusSeeOther) }) + + http.HandleFunc("/" + *Key + "/postnews", func(w http.ResponseWriter, r *http.Request) { + + id, _ := GetPasswordFromSession(r) + + actor := GetActorFromDB(db, Domain) + + + if id == "" || (id != actor.Id && id != Domain) { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + return + } + + var newsitem NewsItem + + newsitem.Title = r.FormValue("title") + newsitem.Content = template.HTML(r.FormValue("summary")) + + WriteNewsToDB(db, newsitem) + + http.Redirect(w, r, "/", http.StatusSeeOther) + }) + + http.HandleFunc("/" + *Key + "/newsdelete/", func(w http.ResponseWriter, r *http.Request){ + + id, _ := GetPasswordFromSession(r) + + actor := GetActorFromDB(db, Domain) + + + if id == "" || (id != actor.Id && id != Domain) { + t := template.Must(template.ParseFiles("./static/verify.html")) + t.Execute(w, "") + return + } + + timestamp := r.URL.Path[13+len(*Key):] + + tsint, err := strconv.Atoi(timestamp) + + if(err != nil){ + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + return + } else { + deleteNewsItemFromDB(db, tsint) + http.Redirect(w, r, "/news/", http.StatusSeeOther) + } + }) http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){ if(r.Method == "POST") { @@ -1852,7 +1935,8 @@ func MakeActivityRequestOutbox(db *sql.DB, activity Activity) { path = re.ReplaceAllString(path, "") sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) - encSig := ActivitySign(db, *activity.Actor, sig) + encSig, err := ActivitySign(db, *activity.Actor, sig) + CheckError(err, "unable to sign activity response") signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) req.Header.Set("Content-Type", activitystreams) @@ -1890,7 +1974,8 @@ func MakeActivityRequest(db *sql.DB, activity Activity) { path = re.ReplaceAllString(path, "") sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) - encSig := ActivitySign(db, *activity.Actor, sig) + encSig, err := ActivitySign(db, *activity.Actor, sig) + CheckError(err, "unable to sign activity response") signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) req.Header.Set("Content-Type", activitystreams) diff --git a/static/admin.html b/static/admin.html index a4c61c1..f238ec0 100644 --- a/static/admin.html +++ b/static/admin.html @@ -6,7 +6,7 @@ <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"> + <form id="new-board" 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> diff --git a/static/anews.html b/static/anews.html new file mode 100644 index 0000000..08bfdfa --- /dev/null +++ b/static/anews.html @@ -0,0 +1,46 @@ +{{ define "header" }} +<title>{{ .Title }}</title> +<meta name="description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> + +<meta property="og:locale" content="en_US" /> +<meta property="og:type" content="website" /> +<meta property="og:url" content="{{ .Board.Domain }}"> +<meta property="og:site_name" content="{{ .Board.Actor.PreferredUsername }}" /> + +<meta property="og:title" content="{{ .Title }}"> +<meta property="og:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> + +<meta name="twitter:title" content="{{ .Title }}"> +<meta name="twitter:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> +<meta name="twitter:card" content="summary_large_image"> + +{{ end }} + +{{ define "top" }}{{ end }} +{{ define "content" }} +<div style="text-align: center; max-width: 800px; margin: 0 auto;"> + <h1>{{ .Title }}</h1> + + <div style="margin-top:50px;"> + <table style="text-align: left;"> + + {{ range $i, $e := .NewsItems }} + <tr> + <td> + <div class="box" style="width:800px; padding: 25px; margin-bottom: 25px;"> + {{ if $.Board.ModCred }}<a href="/{{ $.Key }}/newsdelete/{{ $e.Time }}">[Delete] </a>{{end}} + <a href="/news/{{.Time}}">{{unixtoreadable $e.Time}} - {{$e.Title}}</a> + <br><p style="margin-left: 25px;">{{$e.Content}}</p> + </div> + </td> + </tr> + {{ end }} + </table> + </div> + +</div> +{{ end }} +{{ define "bottom" }}{{ end }} + +{{ define "script" }} +{{ end }} diff --git a/static/bottom.html b/static/bottom.html index db9606d..9d920c0 100644 --- a/static/bottom.html +++ b/static/bottom.html @@ -1,10 +1,10 @@ {{ define "bottom" }} <div id="reply-box" class="popup-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"> + <form onsubmit="sessionStorage.setItem('element-closed-reply', true)" id="reply-post" action="/post" method="post" enctype="multipart/form-data"> <input id="reply-name" name="name" size="43" type="text" placeholder="Name" maxlength="100"> <input id="reply-options" name="options" size="43" type="text" placeholder="Options" maxlength="100"> - <textarea id="reply-comment" name="comment" rows="12" cols="54" style="width: 396px;" maxlength="2000"></textarea> + <textarea id="reply-comment" name="comment" rows="12" cols="54" style="width: 396px;" maxlength="2000" oninput="sessionStorage.setItem('element-reply-comment', document.getElementById('reply-comment').value)"></textarea> <input id="reply-file" name="file" type="file"> <input id="reply-submit" type="submit" value="Reply" style="float: right;"><br><br> <input type="hidden" id="inReplyTo-box" name="inReplyTo" value="{{ .Board.InReplyTo }}"> @@ -25,9 +25,9 @@ <div id="report-box" class="popup-box" style="display: none; "> <div id="report-header" style="text-align: center; display: inline-block; width: 370px; z-index: 0; cursor: move;"></div><div id="report-close" style="display: inline-block; float: right;"><a href="javascript:closeReport()">[X]</a></div> - <form id="report-post" action="/report" method="post"> + <form onsubmit="sessionStorage.setItem('element-closed-report', true)" id="report-post" action="/report" method="post"> <label for="comment">Reason:</label> - <textarea id="report-comment" name="comment" rows="12" cols="54" style="width: 396px;" maxlength="100"></textarea> + <textarea id="report-comment" name="comment" rows="12" cols="54" style="width: 396px;" maxlength="100" oninput="sessionStorage.setItem('element-report-comment', document.getElementById('report-comment').value)"></textarea> <input id="report-submit" type="submit" value="Report" style="float: right;"> <input type="hidden" id="report-inReplyTo-box" name="id" value="{{ .Board.InReplyTo }}"> <input type="hidden" id="sendTo" name="sendTo" value="{{ .Board.To }}"> diff --git a/static/faq.html b/static/faq.html index 4c240f7..9313360 100644 --- a/static/faq.html +++ b/static/faq.html @@ -52,11 +52,13 @@ <h4 id="seqnum">Why do the posts not have sequential ID numbers?</h4> <p>Sequential ID numbers have run their course. Random base36 is better against repelling script kiddies.</p> + <h4 id="seqnum">Why are the posts not sequential numbers?</h4> + <p>sequential numbers have run their course. random base 16 (now base 36) is better.</p> <h4 id="pubexamp">ActivityPub specific examples</h4> <p>Soon™.</p> <h4 id="version">What's the version of this fchannel instance?</h4> - <p>v0.0.5c</p> + <p>v0.0.6-dev</p> </div> <div style="width: 500px; margin:0 auto; margin-top: 50px; text-align: center;"> <a href="/">[Home]</a><a href="/static/rules.html">[Rules]</a><a href="/static/faq.html">[FAQ]</a> diff --git a/static/index.html b/static/index.html index ab0bea0..d7374f1 100644 --- a/static/index.html +++ b/static/index.html @@ -1,6 +1,6 @@ {{ define "header" }} <title>{{ .Title }}</title> -<meta name="description" content="{{ .Message }}"> +<meta name="description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> <meta property="og:locale" content="en_US" /> <meta property="og:type" content="website" /> @@ -8,10 +8,10 @@ <meta property="og:site_name" content="{{ .Board.Actor.PreferredUsername }}" /> <meta property="og:title" content="{{ .Title }}"> -<meta property="og:description" content="{{ .Message }}"> +<meta property="og:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> <meta name="twitter:title" content="{{ .Title }}"> -<meta name="twitter:description" content="{{ .Message }}"> +<meta name="twitter:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> <meta name="twitter:card" content="summary_large_image"> {{ end }} @@ -20,12 +20,53 @@ {{ define "content" }} <div style="text-align: center; max-width: 800px; margin: 0 auto;"> <h1>{{ .Title }}</h1> - <p style="text-align: justify">{{.MessageHTML}}</p> - + <p style="text-align: justify">{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code at <a href="https://github.com/FChannel0">https://github.com/FChannel0</a>.</p> + + {{ if .Boards }} + {{ $l := len .Boards }} <div style="margin-top:50px;"> - <table align="center" style="text-align: left;"> + <div style="display: grid;border-right: 2px solid #820404"> + {{ if lt $l 2 }} + <div style="display: inline-grid; border-bottom: 2px solid #820404;border-left: 2px solid #820404;border-top: 2px solid #820404;"><span style="font-size: 1.5em;font-weight: bold;">Local boards</span></div> + {{ else if eq $l 2 }} + <div style="display: inline-grid; grid-column: 1 / 3; border-bottom: 2px solid #820404;border-left: 2px solid #820404;border-top: 2px solid #820404;"><span style="font-size: 1.5em;font-weight: bold;">Local boards</span></div> + {{ else }} + <div style="display: inline-grid;grid-column: 1 / 4;border-bottom: 2px solid #820404;border-left: 2px solid #820404;border-top: 2px solid #820404;"><span style="font-size: 1.5em;font-weight: bold;">Local boards</span></div> + {{ end }} + {{ range .Boards }} + <div style="whitespace: nowrap;display: inline-grid;text-align: left;padding: 5px;border-bottom: 2px solid #820404;border-left: 2px solid #820404;"><a href="{{.Location}}"><b>/{{.Name}}/</b> - {{.PrefName}} {{ if not .Restricted }} [NSFW] {{ end }}</a></div> + {{ end }} + {{ if gt $l 2 }} + {{ range .BoardRemainer }} + <div style="whitespace: nowrap;display: inline-grid;text-align: left;padding: 5px;border-bottom: 2px solid #820404;border-left: 2px solid #820404;"></div> + {{ end }} + {{ end }} + </div> + </div> + {{ end }} + + {{ if .NewsItems }} + <div class="popup-box" style="margin-top:50px;"> + <table style="text-align: left; margin: 25px;"> + <th> + <tr><h4><a href="/news">{{ .PreferredUsername }} News</a></h4></tr> + </th> + {{ range $i, $e := .NewsItems }} + <tr> + <td>{{ if $.Board.ModCred }}<a href="/{{ $.Key }}/newsdelete/{{ $e.Time }}">[Delete] </a>{{end}} + <a href="/news/{{.Time}}">{{unixtoreadable $e.Time}} - {{$e.Title}}</a> + <br><p style="margin-left: 25px;">{{$e.Content}}</p> + </td> + </tr> + {{ end }} + </table> + </div> + {{ end }} + + <div class="popup-box" style="margin-top:50px;"> + <table style="text-align: left; margin: 25px;"> <th> - <tr>Current known instances</tr> + <tr><h4>Current known instances</h4></tr> </th> {{ range .InstanceIndex }} diff --git a/static/js/footerscript.js b/static/js/footerscript.js index a63f422..69e56e7 100644 --- a/static/js/footerscript.js +++ b/static/js/footerscript.js @@ -3,34 +3,34 @@ var imgArray = [].slice.call(imgs); imgArray.forEach(function(img, i){ img.addEventListener("click", function(e){ - var id = img.getAttribute("id") - var media = document.getElementById("media-" + id) - var sensitive = document.getElementById("sensitive-" + id) + var id = img.getAttribute("id"); + var media = document.getElementById("media-" + id); + var sensitive = document.getElementById("sensitive-" + id); if(img.getAttribute("enlarge") == "0") { - var attachment = img.getAttribute("attachment") + var attachment = img.getAttribute("attachment"); img.setAttribute("enlarge", "1"); img.setAttribute("style", "float: left; margin-right: 10px; cursor: pointer;"); - img.src = attachment + img.src = attachment; } else { - var preview = img.getAttribute("preview") + 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: pointer;"); - img.src = preview + img.src = preview; } else { img.setAttribute("style", "float: left; margin-right: 10px; max-width: 125px; max-height: 125px; cursor: pointer;"); - img.src = preview + img.src = preview; } } }); -}) +}); function viewLink(board, actor) { @@ -38,7 +38,7 @@ function viewLink(board, actor) { var postsArray = [].slice.call(posts); postsArray.forEach(function(p, i){ - var id = p.getAttribute("post") - p.href = "/" + board + "/" + shortURL(actor, id) - }) + var id = p.getAttribute("post"); + p.href = "/" + board + "/" + shortURL(actor, id); + }); } diff --git a/static/js/posts.js b/static/js/posts.js index 79fb7c4..d91fadd 100644 --- a/static/js/posts.js +++ b/static/js/posts.js @@ -1,33 +1,44 @@ +function startNewPost(){ + var el = document.getElementById("newpostbtn"); + el.style="display:none;"; + el.setAttribute("state", "1"); + document.getElementById("newpost").style = "display: block;"; +} + +function stopNewPost(){ + var el = document.getElementById("newpostbtn"); + el.style="display:block;"; + el.setAttribute("state", "0"); + document.getElementById("newpost").style = "display: hidden;"; +} + function newpost() { - var el = document.getElementById("newpostbtn") - var state = el.getAttribute("state") - if(state = "0") + var state = document.getElementById("newpostbtn").getAttribute("state"); + if(state === "0") { - el.style="display:none;" - el.setAttribute("state", "1") - document.getElementById("newpost").style = "display: block;"; + startNewPost(); + sessionStorage.setItem("newpostState", true); } else { - el.style="display:block;" - el.setAttribute("state", "0") - document.getElementById("newpost").style = "display: hidden;"; + stopNewPost(); + sessionStorage.setItem("newpostState", false); } } function getMIMEType(type) { - re = /\/.+/g - return type.replace(re, "") + re = /\/.+/g; + return type.replace(re, ""); } function shortURL(actorName, url) { re = /.+\//g; - temp = re.exec(url) + temp = re.exec(url); - var output + var output; if(stripTransferProtocol(temp[0]) == stripTransferProtocol(actorName) + "/") { @@ -55,18 +66,18 @@ function shortURL(actorName, url) u = re.exec(short); - str = short.replace(/\/+/g, " ") + str = short.replace(/\/+/g, " "); - str = str.replace(u, " ").trim() + str = str.replace(u, " ").trim(); re = /(\w|[!@#$%^&*<>])+$/; - v = re.exec(str) + v = re.exec(str); output = "f" + v[0] + "-" + u } - return output + return output; } function shortImg(url) @@ -92,22 +103,22 @@ function convertSize(size) var convert = size / 1024.0; if(convert > 1024) { - convert = convert / 1024.0 - convert = convert.toFixed(2) + " MB" + convert = convert / 1024.0; + convert = convert.toFixed(2) + " MB"; } else { - convert = convert.toFixed(2) + " KB" + convert = convert.toFixed(2) + " KB"; } - return convert + return convert; } function getBoardId(url) { - var re = /\/([^/\n]+)(.+)?/gm + var re = /\/([^/\n]+)(.+)?/gm; var matches = re.exec(url); - return matches[1] + return matches[1]; } function convertContent(actorName, content, opid) @@ -118,24 +129,24 @@ function convertContent(actorName, content, opid) if(match) { match.forEach(function(quote, i){ - var link = quote.replace('>>', '') - var isOP = "" + var link = quote.replace('>>', ''); + var isOP = ""; if(link == opid) { isOP = " (OP)"; } - var q = link + var q = link; if(document.getElementById(link + "-content") != null) { q = document.getElementById(link + "-content").innerText; - q = q.replaceAll('>', '/\>') - q = q.replaceAll('"', '') - q = q.replaceAll("'", "") + q = q.replaceAll('>', '/\>'); + q = q.replaceAll('"', ''); + q = q.replaceAll("'", ""); } newContent = newContent.replace(quote, '<a class="reply" title="' + q + '" href="'+ (actorName) + "/" + shortURL(actorName, opid) + '#' + shortURL(actorName, link) + '";">>>' + shortURL(actorName, link) + isOP + '</a>'); - }) + }); } re = /^(\s+)?>.+/gm; @@ -146,10 +157,10 @@ function convertContent(actorName, content, opid) match.forEach(function(quote, i) { newContent = newContent.replace(quote, '<span class="quote">' + quote + '</span>'); - }) + }); } - return newContent.replaceAll('/\>', '>') + return newContent.replaceAll('/\>', '>'); } function convertContentNoLink(actorName, content, opid) @@ -160,36 +171,40 @@ function convertContentNoLink(actorName, content, opid) if(match) { match.forEach(function(quote, i){ - var link = quote.replace('>>', '') - var isOP = "" + var link = quote.replace('>>', ''); + var isOP = ""; if(link == opid) { isOP = " (OP)"; } - var q = link + var q = link; if(document.getElementById(link + "-content") != null) { q = document.getElementById(link + "-content").innerText; } newContent = newContent.replace(quote, '>>' + shortURL(actorName, link) + isOP); - }) + }); } - newContent = newContent.replaceAll("'", "") - return newContent.replaceAll('"', '') + newContent = newContent.replaceAll("'", ""); + return newContent.replaceAll('"', ''); } function closeReply() { document.getElementById("reply-box").style.display = "none"; - document.getElementById("reply-comment").value = ""; + document.getElementById("reply-comment").value = ""; + + sessionStorage.setItem("element-closed-reply", true); } function closeReport() { document.getElementById("report-box").style.display = "none"; document.getElementById("report-comment").value = ""; + + sessionStorage.setItem("element-closed-report", true); } @@ -211,10 +226,11 @@ function next(actorName, totalPage, page) function quote(actorName, opid, id) { + sessionStorage.setItem("element-closed-reply", false); 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 inReplyTo = document.getElementById("inReplyTo-box"); var w = window.innerWidth / 2 - 200; if(id == "reply") { @@ -223,8 +239,11 @@ function quote(actorName, opid, id) var h = document.getElementById(id + "-content").offsetTop - 348; } - - box.setAttribute("style", "display: block; position: absolute; width: 400px; height: 600px; z-index: 9; top: " + h + "px; left: " + w + "px; padding: 5px;"); + const boxStyle = "display: block; position: absolute; width: 400px; height: 600px; z-index: 9; top: " + h + "px; left: " + w + "px; padding: 5px;"; + box.setAttribute("style", boxStyle); + sessionStorage.setItem("element-reply-style", boxStyle); + sessionStorage.setItem("reply-top", h); + sessionStorage.setItem("reply-left", w); if (inReplyTo.value != opid) @@ -232,9 +251,12 @@ function quote(actorName, opid, id) header.innerText = "Replying to Thread No. " + shortURL(actorName, opid); inReplyTo.value = opid; + sessionStorage.setItem("element-reply-actor", actorName); + sessionStorage.setItem("element-reply-id", inReplyTo.value); if(id != "reply") comment.value += ">>" + id + "\n"; + sessionStorage.setItem("element-reply-comment", comment.value); dragElement(header); @@ -242,66 +264,161 @@ function quote(actorName, opid, id) function report(actorName, id) { + sessionStorage.setItem("element-closed-report", false); var box = document.getElementById("report-box"); var header = document.getElementById("report-header"); var comment = document.getElementById("report-comment"); - var inReplyTo = document.getElementById("report-inReplyTo-box"); + var inReplyTo = document.getElementById("report-inReplyTo-box"); var w = window.innerWidth / 2 - 200; var h = document.getElementById(id + "-content").offsetTop - 348; - box.setAttribute("style", "display: block; position: absolute; width: 400px; height: 480px; z-index: 9; top: " + h + "px; left: " + w + "px; padding: 5px;"); + const boxStyle = "display: block; position: absolute; width: 400px; height: 480px; z-index: 9; top: " + h + "px; left: " + w + "px; padding: 5px;"; + box.setAttribute("style", boxStyle); + sessionStorage.setItem("element-report-style", boxStyle); + sessionStorage.setItem("report-top", h); + sessionStorage.setItem("report-left", w); header.innerText = "Report Post No. " + shortURL(actorName, id); inReplyTo.value = id; + sessionStorage.setItem("element-report-actor", actorName); + sessionStorage.setItem("element-report-id", id); dragElement(header); } +var pos1, pos2, pos3, pos4; +var elmnt; + +function closeDragElement(e) { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + sessionStorage.setItem("eventhandler", false); +} + +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; + sessionStorage.setItem("pos1", pos1); + sessionStorage.setItem("pos2", pos2); + sessionStorage.setItem("pos3", pos3); + sessionStorage.setItem("pos4", pos4); + + // set the element's new position: + elmnt.parentElement.style.top = (elmnt.parentElement.offsetTop - pos2) + "px"; + elmnt.parentElement.style.left = (elmnt.parentElement.offsetLeft - pos1) + "px"; + if(elmnt.id.startsWith("report")){ + sessionStorage.setItem("report-top", elmnt.parentElement.style.top); + sessionStorage.setItem("report-left", elmnt.parentElement.style.left); + }else if(elmnt.id.startsWith("reply")){ + sessionStorage.setItem("reply-top", elmnt.parentElement.style.top); + sessionStorage.setItem("reply-left", elmnt.parentElement.style.left); + } +} + +function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + sessionStorage.setItem("pos3", pos3); + sessionStorage.setItem("pos4", pos4); + + elmnt = e.currentTarget; + + // call a function whenever the cursor moves: + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + sessionStorage.setItem("eventhandler", true); + +} + 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; - } +const stateLoadHandler = function(event){ + pos1 = parseInt(sessionStorage.getItem("pos1")); + pos2 = parseInt(sessionStorage.getItem("pos2")); + pos3 = parseInt(sessionStorage.getItem("pos3")); + pos4 = parseInt(sessionStorage.getItem("pos4")); - 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"; - } + if(sessionStorage.getItem("element-closed-report") === "false"){ + var box = document.getElementById("report-box"); + var header = document.getElementById("report-header"); + var comment = document.getElementById("report-comment"); + var inReplyTo = document.getElementById("report-inReplyTo-box"); - function closeDragElement() { - // stop moving when mouse button is released: - document.onmouseup = null; - document.onmousemove = null; + header.onmousedown = dragMouseDown; + inReplyTo.value = parseInt(sessionStorage.getItem("element-report-id")); + header.innerText = "Report Post No. " + shortURL(sessionStorage.getItem("element-report-actor"), sessionStorage.getItem("element-report-id")); + comment.value = sessionStorage.getItem("element-report-comment"); + + box.setAttribute("style", sessionStorage.getItem("element-report-style")); + + box.style.top = sessionStorage.getItem("report-top"); + box.style.left = sessionStorage.getItem("report-left"); + + if(sessionStorage.getItem("eventhandler") === "true"){ + elmnt = header; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + }else{ + document.onmouseup = null; + document.onmousemove = null; + } } -} + if(sessionStorage.getItem("element-closed-reply") === "false"){ + var box = document.getElementById("reply-box"); + var header = document.getElementById("reply-header"); + var comment = document.getElementById("reply-comment"); + var inReplyTo = document.getElementById("inReplyTo-box"); + + header.onmousedown = dragMouseDown; + inReplyTo.value = parseInt(sessionStorage.getItem("element-reply-id")); + header.innerText = "Replying to Thread No. " + shortURL(sessionStorage.getItem("element-reply-actor"), sessionStorage.getItem("element-reply-id")); + comment.value = sessionStorage.getItem("element-reply-comment"); + + pos1 = parseInt(sessionStorage.getItem("pos1")); + pos2 = parseInt(sessionStorage.getItem("pos2")); + pos3 = parseInt(sessionStorage.getItem("pos3")); + pos4 = parseInt(sessionStorage.getItem("pos4")); + + box.setAttribute("style", sessionStorage.getItem("element-reply-style")); + + box.style.top = sessionStorage.getItem("reply-top"); + box.style.left = sessionStorage.getItem("reply-left"); + + if(sessionStorage.getItem("eventhandler") === "true"){ + elmnt = header; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + }else{ + document.onmouseup = null; + document.onmousemove = null; + } + } +}; + +document.addEventListener("DOMContentLoaded", stateLoadHandler, false); function stripTransferProtocol(value){ - var re = /(https:\/\/|http:\/\/)?(www.)?/ - return value.replace(re, "") + var re = /(https:\/\/|http:\/\/)?(www.)?/; + return value.replace(re, ""); } function isOnion(value){ - var re = /\.onion/ + var re = /\.onion/; if(value.match(re) != null) - return true - return false + return true; + return false; } + diff --git a/static/js/timer.js b/static/js/timer.js new file mode 100644 index 0000000..8f6516c --- /dev/null +++ b/static/js/timer.js @@ -0,0 +1,38 @@ +var timerCount; +var timerToggle = false; +var timer; +const contentLoadHandler = function(event){ + timerToggle = !!document.getElementById("autoreload-checkbox").checked; + if(timerToggle){ + timerCount = 5; + document.getElementById("autoreload-countdown").innerHTML = "5"; + document.getElementById("autoreload-countdown").style.visibility = "visible"; + timer = setInterval(timerFunction, 1000); + document.removeEventListener("DOMContentLoaded", contentLoadHandler, false); + } +}; + +document.addEventListener("DOMContentLoaded", contentLoadHandler, false); + +function timerFunction(){ + timerCount--; + document.getElementById("autoreload-countdown").innerHTML = timerCount; + if(timerCount <= 0){ + document.getElementById("autoreload-countdown").innerHTML = "Refreshing..."; + clearInterval(timer); + location.reload(); + } +} + +function autoTimer(){ + timerToggle = !timerToggle; + if(timerToggle === true){ + timerCount = 5; + document.getElementById("autoreload-countdown").innerHTML = "5"; + document.getElementById("autoreload-countdown").style.visibility = "visible"; + timer = setInterval(timerFunction, 1000); + }else{ + clearInterval(timer); + document.getElementById("autoreload-countdown").style.visibility = "hidden"; + } +}
\ No newline at end of file diff --git a/static/main.html b/static/main.html index a295a36..3cb8555 100644 --- a/static/main.html +++ b/static/main.html @@ -7,7 +7,7 @@ <meta name="keywords" content="Federated Imageboard based on Activtypub"> <meta property="og:locale" content="en_US" /> <meta property="og:type" content="website" /> - <link rel="icon" type="image/png" href="/static/favicon.png"> + <link rel="icon" type="image/png" href="/static/favicon.png"> <style> a, a:link, a:visited, a:hover, a:active { text-decoration: none @@ -40,6 +40,14 @@ background-color: #f9f9e0; {{ end }} } + + .box { + {{ if .Board.Restricted }} + background-color: #eff5ff; + {{ else }} + background-color: #f9f9e0; + {{ end }} + } .quote { color: #789922; @@ -76,8 +84,17 @@ </head> <body> <ul style="display: inline; padding:0;"> - {{range .Boards}} - <li style="display: inline;"><a href="{{.Location}}">{{.Name}}</a></li> + {{ $l := len .Boards }} + {{range $i, $e := .Boards}} + {{ if eq (sub $l 1) 0 }} + <li style="display: inline;">[ <a href="{{.Location}}">{{$e.Name}} </a>]</li> + {{ else if eq $i 0 }} + <li style="display: inline;">[<a href="{{.Location}}">{{$e.Name}} </a>/</li> + {{ else if eq $i (sub $l 1) }} + <li style="display: inline;"><a href="{{.Location}}">{{$e.Name}}</a>]</li> + {{ else }} + <li style="display: inline;"><a href="{{.Location}}">{{$e.Name}} </a>/</li> + {{ end }} {{end}} </ul> {{ if .Board.ModCred }} diff --git a/static/manage.html b/static/manage.html index 4fb417f..e161f18 100644 --- a/static/manage.html +++ b/static/manage.html @@ -50,7 +50,7 @@ <ul style="display: inline-block; padding: 0; margin: 0; list-style-type: none;"> {{ $domain := .Domain }} {{ range .Reported }} - <li><a id="rpost" post="{{ .ID }}" href=""></a> - <b>{{ .Count }}</b> <a href="/delete?id={{ .ID }}&board={{ $board.Name }}&manage=t">[Remove Post]</a> <a href="/deleteattach?id={{ .ID }}&board={{ $board.Name }}&manage=t">[Remove Attachment]</a> <a href="/report?id={{ .ID }}&close=1&board={{ $board.Name }}">[Close]</a></li> + <li><a id="rpost" post="{{ .ID }}" href=""></a> - <b>{{ .Count }}</b><span> "{{ .Reason }}" </span> <a href="/delete?id={{ .ID }}&board={{ $board.Name }}&manage=t">[Remove Post]</a> <a href="/deleteattach?id={{ .ID }}&board={{ $board.Name }}&manage=t">[Remove Attachment]</a> <a href="/report?id={{ .ID }}&close=1&board={{ $board.Name }}">[Close]</a></li> {{ end }} </ul> </div> diff --git a/static/nadmin.html b/static/nadmin.html index 984eb76..88b92c9 100644 --- a/static/nadmin.html +++ b/static/nadmin.html @@ -17,13 +17,24 @@ <option value="False">False</option> </select> </form> - <ul style="display: inline-block; padding: 0;"> <li style="display: inline-block;"><a href="#following">Subscribed</a></li> <!-- <li style="display: inline-block;"><a href="javascript:show('followers')">Followers</a></li> --> <li style="display: inline-block;"><a href="#reported">Reported</a></li> </ul> -</div> +</div> + +<div class="popup-box" style="margin-bottom: 25px; padding: 12px;"> + <h3>Post News</h3> + <form id="news" action="/{{ .Key }}/postnews" method="post" enctype="application/x-www-form-urlencoded"> + <label>Title:</label><br> + <input type="text" name="title" placeholder="New Board" required><input type="submit" value="Post"><br> + <label>Content:</label><br> + <textarea name="summary" rows="8" cols="50"></textarea><br> + </form> +</div> + + <div id="following" class="popup-box" style="margin-bottom: 25px; padding: 12px;"> <h4 style="margin: 0; margin-bottom: 5px;">Subscribed</h4> diff --git a/static/news.html b/static/news.html new file mode 100644 index 0000000..4bd43e1 --- /dev/null +++ b/static/news.html @@ -0,0 +1,32 @@ +{{ define "header" }} +<title>{{ .Title }}</title> +<meta name="description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> + +<meta property="og:locale" content="en_US" /> +<meta property="og:type" content="website" /> +<meta property="og:url" content="{{ .Board.Domain }}"> +<meta property="og:site_name" content="{{ .Board.Actor.PreferredUsername }}" /> + +<meta property="og:title" content="{{ .Title }}"> +<meta property="og:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> + +<meta name="twitter:title" content="{{ .Title }}"> +<meta name="twitter:description" content="{{ .PreferredUsername }} is a federated image board based on activitypub. The current version of the code running the server is still a work in progress, expect a bumpy ride for the time being. Get the server code here: https://github.com/FChannel0."> +<meta name="twitter:card" content="summary_large_image"> + +{{ end }} + +{{ define "top" }}{{ end }} +{{ define "content" }} +<div style="text-align: left; max-width: 800px; margin: 0 auto;"> + + {{ range .NewsItems }} + <p><h1>{{unixtoreadable .Time}} - {{.Title}}</h1><br>{{.Content}}</p> + {{ end }} + +</div> +{{ end }} +{{ define "bottom" }}{{ end }} + +{{ define "script" }} +{{ end }} diff --git a/static/npost.html b/static/npost.html index 740018b..3df4090 100644 --- a/static/npost.html +++ b/static/npost.html @@ -17,6 +17,7 @@ {{ end }} <script src="/static/js/posts.js"></script> +<script src="/static/js/timer.js"></script> {{ end }} {{ define "content" }} @@ -39,6 +40,7 @@ <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> + <li style="display: inline"><input id="autoreload-checkbox" type="checkbox" onclick="autoTimer()"> Auto refresh <span id="autoreload-countdown" style="visibility: hidden;">0</span></li> </ul> {{ $replies := (index .Posts 0).Replies }} <span style="float: right;">{{ $replies.TotalItems }} / {{ $replies.TotalImgs }}</span> diff --git a/static/top.html b/static/top.html index 26c2fcc..fa4cb9e 100644 --- a/static/top.html +++ b/static/top.html @@ -9,7 +9,7 @@ <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" style="margin-left: 180px;"> + <form onsubmit="sessionStorage.setItem('element-closed-reply', true)" id="new-post" action="/post" method="post" enctype="multipart/form-data" style="margin-left: 180px;"> <label for="name">Name:</label><br> <input type="text" id="name" name="name" placeholder="Anonymous" maxlength="100"><br> <label for="options">Options:</label><br> diff --git a/verification.go b/verification.go index c649a4e..555e9ee 100644 --- a/verification.go +++ b/verification.go @@ -1,24 +1,30 @@ package main -import "fmt" -import "database/sql" -import _ "github.com/lib/pq" -import "net/smtp" -import "time" -import "os/exec" -import "os" -import "math/rand" -import "crypto" -import "crypto/rsa" -import "crypto/x509" -import "crypto/sha256" -import "encoding/pem" -import "encoding/base64" -import crand "crypto/rand" -import "io/ioutil" -import "strings" -import "net/http" -import "regexp" +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "database/sql" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "math/rand" + "net/smtp" + "os" + "os/exec" + "time" + + _ "github.com/lib/pq" + + crand "crypto/rand" + "io/ioutil" + "net/http" + "regexp" + "strings" +) + type Verify struct { Type string @@ -527,6 +533,52 @@ func CreatePem(db *sql.DB, actor Actor) { } else { StorePemToDB(db, actor) } + + fmt.Println(`Created PEM keypair for the "` + actor.Name +`" board. Please keep in mind that +the PEM key is crucial in identifying yourself as the legitimate owner of the board, +so DO NOT LOSE IT!!! If you lose it, YOU WILL LOSE ACCESS TO YOUR BOARD!`); +} + +func CreatePublicKeyFromPrivate(db *sql.DB, actor *Actor, publicKeyPem string) error{ + publicFilename := GetActorPemFileFromDB(db, publicKeyPem); + privateFilename := strings.ReplaceAll(publicFilename, "public.pem", "private.pem") + _, err := os.Stat(privateFilename) + if err == nil { + //Not a lost cause + priv, err := ioutil.ReadFile(privateFilename) + + block, _ := pem.Decode([]byte(priv)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return errors.New("failed to decode PEM block containing public key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + CheckError(err, "failed to parse private key") + + publicKeyDer, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + CheckError(err, "failed to marshal public key from private key") + pubKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + + publicFileWriter, err := os.Create(publicFilename) + CheckError(err, "error creating public pem file for " + actor.Name) + + err = pem.Encode(publicFileWriter, &pubKeyBlock) + CheckError(err, "error encoding public pem") + }else{ + fmt.Println(`\nUnable to locate private key from public key generation. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return errors.New("unable to locate private key") + } + return nil } func StorePemToDB(db *sql.DB, actor Actor) { @@ -555,7 +607,7 @@ func StorePemToDB(db *sql.DB, actor Actor) { CheckError(err, "error creating publicKeyPem for actor ") } -func ActivitySign(db *sql.DB, actor Actor, signature string) string { +func ActivitySign(db *sql.DB, actor Actor, signature string) (string, error) { query := `select file from publicKeyPem where id=$1 ` rows, err := db.Query(query, actor.PublicKey.Id) @@ -581,10 +633,17 @@ func ActivitySign(db *sql.DB, actor Actor, signature string) string { hashed.Write([]byte(signature)) cipher, _ := rsa.SignPKCS1v15(rng, pub, crypto.SHA256, hashed.Sum(nil)) - return base64.StdEncoding.EncodeToString(cipher) + return base64.StdEncoding.EncodeToString(cipher), nil + }else{ + fmt.Println(`\n Unable to locate private key. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return "", errors.New("unable to locate private key") } - - return "" } func ActivityVerify(actor Actor, signature string, verify string) error { |