~aw/fishbb

3d95d602bf89bec3fdc3bdd242df8adc4e5d70be — alex wennerberg a month ago 9f89665
a bunch of stuff, but main thing is gemtext parsing
M TODO => TODO +7 -0
@@ 1,6 1,13 @@
Setup fishbb.org (cloud provider)

RESEARCH:
- look into email: IMAP uname/pw?

FEATURES:
consider htmx
user profile should be /u/{profile} not id
4chan style replies

minor/bugs:
rate limit registration
better error handling on login

M db.go => db.go +10 -9
@@ 10,14 10,6 @@ import (
	_ "github.com/mattn/go-sqlite3"
)

var stmtGetForumID, stmtUpdateMe, stmtSearchPosts,
	stmtEditPost, stmtGetPost, stmtGetPostSlug, stmtGetForum,
	stmtGetForumBySlug, stmtCreateUser, stmtGetForums, stmtUpdateForum,
	stmtGetUser, stmtGetUsers, stmtGetPostAuthorID, stmtDeletePost,
	stmtThreadPin, stmtThreadLock, stmtActivateUser,
	stmtCreatePost, stmtGetThread, stmtGetPosts, stmtGetThreadCount,
	stmtQueryPosts, stmtDeleteUser, stmtUpdateUserRole, stmtUpdateBanStatus, stmtUpdateConfig, stmtGetConfig,
	stmtGetThreads, stmtCreateThread, stmtCreateForum *sql.Stmt
var db *sql.DB

func opendb() *sql.DB {


@@ 88,8 80,16 @@ func prepare(db *sql.DB, stmt string) *sql.Stmt {
	return s
}

var stmtGetForumID, stmtUpdateMe, stmtSearchPosts,
	stmtEditPost, stmtGetPost, stmtGetPostSlug, stmtGetForum,
	stmtGetForumBySlug, stmtCreateUser, stmtGetForums, stmtUpdateForum,
	stmtGetUser, stmtGetUsers, stmtGetPostAuthorID, stmtDeletePost,
	stmtThreadPin, stmtThreadLock, stmtActivateUser, stmtGetAllUsernames,
	stmtCreatePost, stmtGetThread, stmtGetPosts, stmtGetThreadCount,
	stmtQueryPosts, stmtDeleteUser, stmtUpdateUserRole, stmtUpdateBanStatus, stmtUpdateConfig, stmtGetConfig,
	stmtGetThreads, stmtCreateThread, stmtCreateForum *sql.Stmt

func prepareStatements(db *sql.DB) {
	// TODO maybe do this in code?
	stmtGetForums = prepare(db, `
		select forums.id, name, description, permissions,
		coalesce(threadid, 0), coalesce(latest.title, ''), coalesce(latest.id, 0), coalesce(latest.authorid, 0),


@@ 199,6 199,7 @@ func prepareStatements(db *sql.DB) {
	stmtQueryPosts = prepare(db, "select 1")
	stmtUpdateConfig = prepare(db, "insert into config(value) values(?)")
	stmtGetConfig = prepare(db, "select value from config order by id desc limit 1")
	stmtGetAllUsernames = prepare(db, "select username from users;")
	LoginInit(LoginInitArgs{
		Db: db,
	})

A gemtext.go => gemtext.go +15 -0
@@ 0,0 1,15 @@
package main

import (
	"html/template"
	"strings"

	"git.sr.ht/~aw/gmi2html"
)

func (p Post) Render() template.HTML {
	r := strings.NewReader(p.Content)
	g := gmi2html.NewReader(r)
	g.NestedBlocks = true
	return template.HTML(g.HTMLString())
}

M go.mod => go.mod +1 -1
@@ 8,13 8,13 @@ require (
)

require (
	git.sr.ht/~aw/gmi2html v0.1.3
	github.com/BurntSushi/toml v1.4.0
	github.com/alexedwards/argon2id v1.0.0
	github.com/go-chi/chi/v5 v5.1.0
	github.com/go-chi/httplog/v2 v2.0.11
	github.com/go-chi/httprate v0.9.0
	github.com/k3a/html2text v1.2.1
	github.com/yuin/goldmark v1.7.1
	golang.org/x/oauth2 v0.21.0
)


M go.sum => go.sum +2 -2
@@ 1,5 1,7 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
git.sr.ht/~aw/gmi2html v0.1.3 h1:Gj2vcmI/HqSs+9t05s0y/9n8o7swXSnJH4H+AQSu1Sc=
git.sr.ht/~aw/gmi2html v0.1.3/go.mod h1:WXvBJ0UXRsTcMcm/jmE1yJXz4bxUZDdFqv8HDcIueE0=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=


@@ 27,8 29,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=

M notifications.go => notifications.go +1 -1
@@ 4,7 4,7 @@ import "time"

type Notification struct {
	ID      int
	Message string // markdown
	Message string
	Created time.Time
}


M post.go => post.go +2 -0
@@ 15,6 15,8 @@ type Post struct {
	Edited  *time.Time
}

// Custom inline parser for user and post links

// TODO reconcile these somehow maybe
type PostSummary struct {
	ID          int

M schema.sql => schema.sql +1 -0
@@ 24,6 24,7 @@ create table posts (
  threadid integer,
  authorid integer,
  content text,
  in_reply_to integer, 
  created datetime default current_timestamp,
  edited datetime,
  foreign key (authorid) references users(id),

M user.go => user.go +18 -0
@@ 99,6 99,24 @@ func getUsers() ([]User, error) {
	return users, nil
}

// unused
func getAllUsernames() ([]string, error) {
	var usernames []string
	rows, err := stmtGetUsers.Query()
	if err != nil {
		return nil, fmt.Errorf("could not execute query: %w", err)
	}
	for rows.Next() {
		var username string
		err := rows.Scan(&username)
		if err != nil {
			return nil, fmt.Errorf("failed to scan rows: %w", err)
		}
		usernames = append(usernames, username)
	}
	return usernames, nil
}

func activateUser(id int) error {
	_, err := stmtActivateUser.Exec(id)
	return err

M util.go => util.go +0 -19
@@ 5,7 5,6 @@ import (
	"crypto/rand"
	"crypto/sha512"
	"fmt"
	"html/template"
	"image"
	"image/png"
	"io"


@@ 14,9 13,6 @@ import (
	"strconv"
	"time"

	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/extension"

	"strings"
)



@@ 185,18 181,3 @@ func timeago(t *time.Time) string {
		return fmt.Sprintf("%d %ss ago", amount, metric)
	}
}

// TODO investigate extensions
var gm = goldmark.New(
	goldmark.WithExtensions(extension.Linkify),
)

func markup(md string) template.HTML {
	var buf bytes.Buffer
	err := gm.Convert([]byte(md), &buf)
	if err != nil {
		// TODO error handling? Bluemonday as backup?
		return template.HTML("(Error rendering post)!")
	}
	return template.HTML(buf.String())
}

M views/edit-post.html => views/edit-post.html +1 -1
@@ 4,7 4,7 @@
		<div class="body-padded" >
			<label for="content">Content:</label><br>
			<textarea class="post-entry" required name="content" id="content" placeholder='I have something to say...'>{{.Post.Content}}</textarea><br>
			<a href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">markdown guide</a><br>
			<a href="https://admin.flounder.online/gemini_text_guide.gmi">gemtext guide</a><br>
				
			<input type="hidden" name="CSRF" value="{{.CSRFToken}}">
			<button type="submit">Save</button>

M views/new-post.html => views/new-post.html +1 -1
@@ 8,7 8,7 @@
        <div class="body-padded" >
			<label for="content">Content:</label><br>
            <textarea class="post-entry" required name="content" id="content" placeholder='I have something to say...'></textarea><br>
			<a href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">markdown guide</a><br>
			<a href="https://admin.flounder.online/gemini_text_guide.gmi">gemtext guide</a><br>
				
			<input type="hidden" name="CSRF" value="{{.CSRFToken}}">
            <button type="submit">Create</button>

M views/style.css => views/style.css +14 -0
@@ 67,6 67,20 @@ button:not(.link-button) {
  font-size: 16px;
}

.post-body p {
  margin-top: 0px;
  margin-bottom: 0.2rem;
}


blockquote {
  margin-left: .5em;
  font-style: italic;
  background: var(--bg-alt);
  border-left: .5em solid var(--bg-inverse);
  padding: .5em 10px;
}

.error {
  font-weight: bold;
}

M views/thread.html => views/thread.html +1 -1
@@ 32,7 32,7 @@
	  </div>
  </div>
  <div class="post-body">
  {{ markup .Content }}
  {{ .Render }}
  </div>
  <div class="flex-end">
	  <a href="/post/new?threadid={{$.Thread.ID}}&reply={{.ID}}"><button class="padded-button">Reply</button></a></div>

M web.go => web.go +0 -1
@@ 366,7 366,6 @@ func loadTemplates() *template.Template {
	views, err := template.New("main").Funcs(template.FuncMap{
		"timeago": timeago,
		"pageArr": pageArray,
		"markup":  markup,
		"inc": func(i int) int {
			return i + 1