Compare commits
3 commits
2eb8a2e113
...
f6b6c8a7e6
| Author | SHA1 | Date | |
|---|---|---|---|
| f6b6c8a7e6 | |||
| 0bd065c2d1 | |||
| 78ae49c2c7 |
8 changed files with 340 additions and 19 deletions
|
|
@ -3,8 +3,8 @@ FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install git if needed for go mod
|
# Install git, gcc, musl-dev, and sqlite-dev for go mod and CGO
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git gcc musl-dev sqlite-dev
|
||||||
|
|
||||||
# Copy go mod and sum files
|
# Copy go mod and sum files
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
|
@ -15,6 +15,9 @@ RUN go mod download
|
||||||
COPY main.go .
|
COPY main.go .
|
||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
|
|
||||||
|
# Enable CGO for go-sqlite3
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
|
||||||
RUN go build -o main .
|
RUN go build -o main .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -3,6 +3,7 @@ module gemini_site
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.travisshears.com/travisshears/gemlog-cli v1.0.1
|
git.travisshears.com/travisshears/gemlog-cli v1.1.0
|
||||||
github.com/kulak/gemini v1.2.2
|
github.com/kulak/gemini v1.2.2
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -1,9 +1,11 @@
|
||||||
git.travisshears.com/travisshears/gemlog-cli v1.0.1 h1:0AcFwrnukwcWHuePEROG3WDSGbcE8WoFvJTQxrTaszc=
|
git.travisshears.com/travisshears/gemlog-cli v1.1.0 h1:iFMIeYyzPvoUw2sQqGg8PTejCcKxgmhjy5HqpVo3Ag8=
|
||||||
git.travisshears.com/travisshears/gemlog-cli v1.0.1/go.mod h1:N6l94N174EhDOIHU0/RlJ0PWrxB0BMa0W6LcpgAtvCE=
|
git.travisshears.com/travisshears/gemlog-cli v1.1.0/go.mod h1:N6l94N174EhDOIHU0/RlJ0PWrxB0BMa0W6LcpgAtvCE=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/kulak/gemini v1.2.2 h1:wPFOAFFdOf9ZaHcpMwTq1xYUWxmyV3h0uQl0OXCGa+A=
|
github.com/kulak/gemini v1.2.2 h1:wPFOAFFdOf9ZaHcpMwTq1xYUWxmyV3h0uQl0OXCGa+A=
|
||||||
github.com/kulak/gemini v1.2.2/go.mod h1:8yiD7yhLkUGvOpdvgd/0nKQD2I0ChIAKD3yHuT13R5k=
|
github.com/kulak/gemini v1.2.2/go.mod h1:8yiD7yhLkUGvOpdvgd/0nKQD2I0ChIAKD3yHuT13R5k=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|
|
||||||
255
internal/guestbook/guestbook.go
Normal file
255
internal/guestbook/guestbook.go
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
package guestbook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
|
gemini "github.com/kulak/gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
const adminUserId = "6a1171c755cbde90373bee2ac08c6aa4607ecf8ed3f50c55cf76c6a0e27dfa5c"
|
||||||
|
|
||||||
|
type GuestBook struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGuestBook(db *sql.DB) *GuestBook {
|
||||||
|
err := initDB(db)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error migrating sqlite database", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &GuestBook{
|
||||||
|
db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS guestbook (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
approved BOOLEAN NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) HandleRequest(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
path := req.URL.Path
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case path == "/guestbook" || path == "/guestbook/":
|
||||||
|
book.serveIndex(w, req)
|
||||||
|
case path == "/guestbook/new":
|
||||||
|
book.serveNewEntry(w, req)
|
||||||
|
case path == "/guestbook/admin":
|
||||||
|
book.serveAdmin(w, req)
|
||||||
|
case strings.HasPrefix(path, "/guestbook/approve"):
|
||||||
|
book.serveApprove(w, req)
|
||||||
|
case strings.HasPrefix(path, "/guestbook/delete"):
|
||||||
|
book.serveDelete(w, req)
|
||||||
|
default:
|
||||||
|
w.WriteStatusMsg(gemini.StatusNotFound, "Page not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserIDFromCert(cert *x509.Certificate) (string, error) {
|
||||||
|
pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hash := sha256.Sum256(pubKeyBytes)
|
||||||
|
return hex.EncodeToString(hash[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) serveDelete(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
if req.Certificate() == nil {
|
||||||
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId, err := UserIDFromCert(req.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating user id from cert", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userId != adminUserId {
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entryId := req.URL.Query().Get("id")
|
||||||
|
slog.Info("Deleting", "userId", userId, "entryId", entryId)
|
||||||
|
_, err = book.db.Exec("DELETE FROM guestbook WHERE id = ?", entryId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error deleting guestbook entry", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error deleting guestbook entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
book.serveAdmin(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) serveApprove(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
if req.Certificate() == nil {
|
||||||
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId, err := UserIDFromCert(req.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating user id from cert", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userId != adminUserId {
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entryId := req.URL.Query().Get("id")
|
||||||
|
slog.Info("Approving", "userId", userId, "entryId", entryId)
|
||||||
|
_, err = book.db.Exec("UPDATE guestbook SET approved = true WHERE id = ?", entryId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error approving guestbook entry", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error approving guestbook entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
book.serveAdmin(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) serveNewEntry(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
if req.Certificate() == nil {
|
||||||
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.URL.RawQuery == "" {
|
||||||
|
w.WriteStatusMsg(gemini.StatusPlainInput, "Your guestbook entry:")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||||
|
name := req.Certificate().Subject.CommonName
|
||||||
|
userId, err := UserIDFromCert(req.Certificate())
|
||||||
|
slog.Info("User ID created", "user_id", userId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating user id from cert", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txt := req.URL.RawQuery
|
||||||
|
_, err = book.db.Exec("INSERT INTO guestbook(name, user_id, approved, message) VALUES(?, ?, ?, ?)", name, userId, false, txt)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error writing to database", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString("# Success!\n")
|
||||||
|
content.WriteString("Your entry was added to the guestbook\n\n")
|
||||||
|
content.WriteString(fmt.Sprintf("username: %s\nentry:\n> %s\n\n", name, txt))
|
||||||
|
content.WriteString("For now since the entry has not been approved it will appear in the list with xxxxx in place of chars.\n")
|
||||||
|
content.WriteString("\n\n=> /guestbook Back to guestbook")
|
||||||
|
w.WriteBody([]byte(content.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) serveIndex(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||||
|
var content strings.Builder
|
||||||
|
page, err := os.ReadFile("./pages/guestbook.gmi")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Problem reading guestbook.gmi", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.Write(page)
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
rows, err := book.db.Query("SELECT id, name, approved, created_at, message FROM guestbook ORDER BY created_at DESC")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error querying database", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var name string
|
||||||
|
var approved bool
|
||||||
|
var createdAt time.Time
|
||||||
|
var message string
|
||||||
|
rows.Scan(&id, &name, &approved, &createdAt, &message)
|
||||||
|
if !approved {
|
||||||
|
// Overwrite all non-space characters in message with "x"
|
||||||
|
masked := []rune(message)
|
||||||
|
for i, c := range masked {
|
||||||
|
if c != ' ' {
|
||||||
|
masked[i] = 'x'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message = string(masked)
|
||||||
|
}
|
||||||
|
content.WriteString(fmt.Sprintf("%s :: %s\n> %s\n", createdAt.Format("2006-01-02"), name, message))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
content.WriteString("=> / Back to home\n")
|
||||||
|
w.WriteBody([]byte(content.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (book *GuestBook) serveAdmin(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
if req.Certificate() == nil {
|
||||||
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication Required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||||
|
|
||||||
|
userId, err := UserIDFromCert(req.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error creating user id from cert", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error generating user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userId != adminUserId {
|
||||||
|
w.WriteStatusMsg(gemini.StatusBadRequest, "UserId != adminUserId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
rows, err := book.db.Query("SELECT id, user_id, name, approved, created_at, message FROM guestbook ORDER BY created_at DESC")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error querying database", "error", err)
|
||||||
|
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Error writing to database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var userId string
|
||||||
|
var name string
|
||||||
|
var approved bool
|
||||||
|
var createdAt time.Time
|
||||||
|
var message string
|
||||||
|
rows.Scan(&id, &userId, &name, &approved, &createdAt, &message)
|
||||||
|
content.WriteString(fmt.Sprintf("%s :: %s\n> %s\n", createdAt.Format("2006-01-02"), name, message))
|
||||||
|
if !approved {
|
||||||
|
content.WriteString(fmt.Sprintf("=> /guestbook/approve?id=%d Approve\n", id))
|
||||||
|
}
|
||||||
|
content.WriteString(fmt.Sprintf("=> /guestbook/delete?id=%d Delete\n", id))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
content.WriteString("=> /guestbook Back to guesbook\n")
|
||||||
|
content.WriteString("=> / Back to home\n")
|
||||||
|
w.WriteBody([]byte(content.String()))
|
||||||
|
}
|
||||||
17
internal/guestbook/handler.go
Normal file
17
internal/guestbook/handler.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package guestbook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
gemini "github.com/kulak/gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler interface for microblog functionality
|
||||||
|
type Handler interface {
|
||||||
|
HandleRequest(w gemini.ResponseWriter, req *gemini.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new microblog handler
|
||||||
|
func NewHandler(db *sql.DB) Handler {
|
||||||
|
return NewGuestBook(db)
|
||||||
|
}
|
||||||
35
main.go
35
main.go
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -10,7 +11,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
gemlog "gemini_site/internal/gemlog"
|
gemlog "gemini_site/internal/gemlog"
|
||||||
|
"gemini_site/internal/guestbook"
|
||||||
microblog "gemini_site/internal/microblog"
|
microblog "gemini_site/internal/microblog"
|
||||||
"gemini_site/internal/pocketbase"
|
"gemini_site/internal/pocketbase"
|
||||||
|
|
||||||
|
|
@ -18,8 +22,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type MainHandler struct {
|
type MainHandler struct {
|
||||||
blog microblog.Handler
|
blog microblog.Handler
|
||||||
gemlog gemlog.Handler
|
gemlog gemlog.Handler
|
||||||
|
guestbook guestbook.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
|
@ -28,14 +33,17 @@ func (h MainHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
"user", strings.Join(userName(req), " "))
|
"user", strings.Join(userName(req), " "))
|
||||||
|
|
||||||
// Check if this is a blog request
|
// Check if this is a blog request
|
||||||
if strings.HasPrefix(req.URL.Path, "/microblog") {
|
switch {
|
||||||
|
case strings.HasPrefix(req.URL.Path, "/microblog"):
|
||||||
h.blog.HandleBlogRequest(w, req)
|
h.blog.HandleBlogRequest(w, req)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(req.URL.Path, "/gemlog") {
|
case strings.HasPrefix(req.URL.Path, "/gemlog"):
|
||||||
h.gemlog.HandleRequest(w, req)
|
h.gemlog.HandleRequest(w, req)
|
||||||
return
|
return
|
||||||
|
case strings.HasPrefix(req.URL.Path, "/guestbook"):
|
||||||
|
h.guestbook.HandleRequest(w, req)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch req.URL.Path {
|
switch req.URL.Path {
|
||||||
|
|
@ -102,6 +110,16 @@ func main() {
|
||||||
Level: slog.LevelInfo,
|
Level: slog.LevelInfo,
|
||||||
}))
|
}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
// Setup DB
|
||||||
|
sqlitePath := os.Getenv("SQLITE_PATH")
|
||||||
|
if sqlitePath == "" {
|
||||||
|
panic("SQLITE_PATH environment variable must be set and non-empty")
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite3", sqlitePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
slog.Info("Starting gemini server")
|
slog.Info("Starting gemini server")
|
||||||
var host, cert, key string
|
var host, cert, key string
|
||||||
|
|
@ -112,11 +130,12 @@ func main() {
|
||||||
|
|
||||||
pbClient := pocketbase.NewPocketBaseClient()
|
pbClient := pocketbase.NewPocketBaseClient()
|
||||||
handler := MainHandler{
|
handler := MainHandler{
|
||||||
blog: microblog.NewHandler(pbClient),
|
blog: microblog.NewHandler(pbClient),
|
||||||
gemlog: gemlog.NewHandler(),
|
gemlog: gemlog.NewHandler(),
|
||||||
|
guestbook: guestbook.NewGuestBook(db),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
|
err = gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("server failed to start", "error", err)
|
slog.Error("server failed to start", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
13
pages/guestbook.gmi
Normal file
13
pages/guestbook.gmi
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Guestbook
|
||||||
|
|
||||||
|
A place to leave messages.
|
||||||
|
|
||||||
|
Hey gemspace! Thanks for visiting my capsule. Why not leave a message?
|
||||||
|
|
||||||
|
=> /guestbook/new Sign the guestbook
|
||||||
|
=> /guestbook/admin Admin
|
||||||
|
|
||||||
|
|
||||||
|
Messages like "xxxxx xxxx" mean they are awaiting my approval. Once approved, the actual text will be rendered.
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
|
@ -11,7 +11,11 @@
|
||||||
|
|
||||||
Hello, World!
|
Hello, World!
|
||||||
|
|
||||||
Welcome to my Gemini capsule. This capsule is in very early stages of development. Mostly a proof of concept at this point. I plan to add more features like a gemlog soon. Looking forward to writing and interacting with folks in gemspace.
|
Welcome to my Gemini capsule. Est 2025.09.
|
||||||
|
|
||||||
|
This capsule is my playground for experimenting with text based content and interacting with folks in gemspace.
|
||||||
|
|
||||||
|
Want to get in touch? Shoot me an email at t [at] travisshears.com
|
||||||
|
|
||||||
My personal website:
|
My personal website:
|
||||||
=> https://travisshears.com
|
=> https://travisshears.com
|
||||||
|
|
@ -21,17 +25,24 @@ So far I've joined the following communities here in gemspace:
|
||||||
## Capsule Features
|
## Capsule Features
|
||||||
|
|
||||||
=> /gemlog Gemlog - Gemini exclusive blog
|
=> /gemlog Gemlog - Gemini exclusive blog
|
||||||
|
=> /guestbook Guestbook - Open guestbook for visitors to sign
|
||||||
=> /microblog Microblog - Aggregation of all my microblog posts
|
=> /microblog Microblog - Aggregation of all my microblog posts
|
||||||
|
|
||||||
## Site updates
|
## Site updates
|
||||||
|
|
||||||
== 06.10.2025 ==
|
== 2025.10.07 ==
|
||||||
|
|
||||||
Added gemlog!
|
Added Guestbook!
|
||||||
|
|
||||||
=> /gemlog
|
=> /guestbook Sign it now
|
||||||
|
|
||||||
== 29.09.2025 ==
|
== 2025.10.06 ==
|
||||||
|
|
||||||
|
Added Gemlog!
|
||||||
|
|
||||||
|
=> /gemlog Check it out
|
||||||
|
|
||||||
|
== 2025.09.29 ==
|
||||||
|
|
||||||
Added microblog feature rendering my nostr post feed. Nice to have a dynamic source of content in this capsule to keep it fresh between updates.
|
Added microblog feature rendering my nostr post feed. Nice to have a dynamic source of content in this capsule to keep it fresh between updates.
|
||||||
|
|
||||||
|
|
@ -39,7 +50,7 @@ Ideas for next features:
|
||||||
* Guestbook backed by sqlite. Would be fun to playout sqlite and golang. Could also be intresting to do a telegram notification.
|
* Guestbook backed by sqlite. Would be fun to playout sqlite and golang. Could also be intresting to do a telegram notification.
|
||||||
* Code block rendering from my git hosting
|
* Code block rendering from my git hosting
|
||||||
|
|
||||||
== 26.09.2025 ==
|
== 2025.09.26 ==
|
||||||
|
|
||||||
Initial deployment
|
Initial deployment
|
||||||
Took quite some tinkering to get everything working, but it's finally up and running! If you are reading this then I got the following architecture working:
|
Took quite some tinkering to get everything working, but it's finally up and running! If you are reading this then I got the following architecture working:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue