aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsageman_ <sageman@anomine.net>2021-07-04 13:45:13 +0200
committersageman_ <sageman@anomine.net>2021-07-04 13:45:13 +0200
commitbc868ba51864f2f1569f942cc44b01d631d0d337 (patch)
treee95d6a3d54349d416cf7f271bb2319f3934943e1
parentd287a2d0bb19f6a7eef682cd411e0e7cc31c1e3d (diff)
parent2d06abf5049e67e05c068461908c492f43ff4026 (diff)
Merge branch 'development' of https://github.com/FChannel0/FChannel-Server into faq-and-rules
# Conflicts: # client.go # static/faq.html
-rw-r--r--client.go102
-rw-r--r--database.go136
-rw-r--r--databaseschema.psql9
-rw-r--r--main.go99
-rw-r--r--static/admin.html2
-rw-r--r--static/anews.html46
-rw-r--r--static/bottom.html8
-rw-r--r--static/faq.html4
-rw-r--r--static/index.html55
-rw-r--r--static/js/footerscript.js24
-rw-r--r--static/js/posts.js273
-rw-r--r--static/js/timer.js38
-rw-r--r--static/main.html23
-rw-r--r--static/manage.html2
-rw-r--r--static/nadmin.html15
-rw-r--r--static/news.html32
-rw-r--r--static/npost.html2
-rw-r--r--static/top.html2
-rw-r--r--verification.go105
19 files changed, 810 insertions, 167 deletions
diff --git a/client.go b/client.go
index 6563ae3..9cfb364 100644
--- a/client.go
+++ b/client.go
@@ -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;
+
diff --git a/main.go b/main.go
index 2874396..755a0ec 100644
--- a/main.go
+++ b/main.go
@@ -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&trade;.</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 {