Basic (experimental) plugin support with two plugin types (exec and middleware)

This commit is contained in:
Jan-Lukas Else 2022-08-09 17:25:22 +02:00 committed by Jan-Lukas Else
parent d813e9579c
commit 2158b156c5
36 changed files with 678 additions and 81 deletions

View File

@ -12,6 +12,7 @@ ADD leaflet/ /app/leaflet/
ADD hlsjs/ /app/hlsjs/ ADD hlsjs/ /app/hlsjs/
ADD dbmigrations/ /app/dbmigrations/ ADD dbmigrations/ /app/dbmigrations/
ADD strings/ /app/strings/ ADD strings/ /app/strings/
ADD plugins/ /app/plugins/
FROM buildbase as build FROM buildbase as build

View File

@ -291,7 +291,7 @@ func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, error) {
} }
func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) { func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) {
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog)) rows, err := db.Query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -307,17 +307,17 @@ func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) {
} }
func (db *database) apAddFollower(blog, follower, inbox string) error { func (db *database) apAddFollower(blog, follower, inbox string) error {
_, err := db.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox)) _, err := db.Exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
return err return err
} }
func (db *database) apRemoveFollower(blog, follower string) error { func (db *database) apRemoveFollower(blog, follower string) error {
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower)) _, err := db.Exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
return err return err
} }
func (db *database) apRemoveInbox(inbox string) error { func (db *database) apRemoveInbox(inbox string) error {
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox)) _, err := db.Exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
return err return err
} }

3
app.go
View File

@ -14,6 +14,7 @@ import (
rotatelogs "github.com/lestrrat-go/file-rotatelogs" rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"go.goblog.app/app/pkgs/minify" "go.goblog.app/app/pkgs/minify"
"go.goblog.app/app/pkgs/plugins"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
"tailscale.com/tsnet" "tailscale.com/tsnet"
@ -75,6 +76,8 @@ type goBlog struct {
mediaStorage mediaStorage mediaStorage mediaStorage
// Minify // Minify
min minify.Minifier min minify.Minifier
// Plugins
pluginHost *plugins.PluginHost
// Reactions // Reactions
reactionsInit sync.Once reactionsInit sync.Once
reactionsCache *ristretto.Cache reactionsCache *ristretto.Cache

View File

@ -154,7 +154,7 @@ func (db *database) getBlogStats(blog string) (data *blogStatsData, err error) {
Months: map[string][]blogStatsRow{}, Months: map[string][]blogStatsRow{},
} }
// Query and scan // Query and scan
rows, err := db.query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("blog", blog)) rows, err := db.Query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("blog", blog))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -28,7 +28,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
row, err := a.db.queryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id)) row, err := a.db.QueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
if err != nil { if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError) a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return return
@ -63,7 +63,7 @@ func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) {
name := defaultIfEmpty(cleanHTMLText(r.FormValue("name")), "Anonymous") name := defaultIfEmpty(cleanHTMLText(r.FormValue("name")), "Anonymous")
website := cleanHTMLText(r.FormValue("website")) website := cleanHTMLText(r.FormValue("website"))
// Insert // Insert
result, err := a.db.exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website)) result, err := a.db.Exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
if err != nil { if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError) a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return return
@ -116,7 +116,7 @@ func buildCommentsQuery(config *commentsRequestConfig) (query string, args []any
func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) { func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) {
comments := []*comment{} comments := []*comment{}
query, args := buildCommentsQuery(config) query, args := buildCommentsQuery(config)
rows, err := db.query(query, args...) rows, err := db.Query(query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -134,7 +134,7 @@ func (db *database) getComments(config *commentsRequestConfig) ([]*comment, erro
func (db *database) countComments(config *commentsRequestConfig) (count int, err error) { func (db *database) countComments(config *commentsRequestConfig) (count int, err error) {
query, params := buildCommentsQuery(config) query, params := buildCommentsQuery(config)
query = "select count(*) from (" + query + ")" query = "select count(*) from (" + query + ")"
row, err := db.queryRow(query, params...) row, err := db.QueryRow(query, params...)
if err != nil { if err != nil {
return return
} }
@ -143,6 +143,6 @@ func (db *database) countComments(config *commentsRequestConfig) (count int, err
} }
func (db *database) deleteComment(id int) error { func (db *database) deleteComment(id int) error {
_, err := db.exec("delete from comments where id = @id", sql.Named("id", id)) _, err := db.Exec("delete from comments where id = @id", sql.Named("id", id))
return err return err
} }

View File

@ -19,6 +19,7 @@ type config struct {
Blogs map[string]*configBlog `mapstructure:"blogs"` Blogs map[string]*configBlog `mapstructure:"blogs"`
User *configUser `mapstructure:"user"` User *configUser `mapstructure:"user"`
Hooks *configHooks `mapstructure:"hooks"` Hooks *configHooks `mapstructure:"hooks"`
Plugins []*configPlugin `mapstructure:"plugins"`
Micropub *configMicropub `mapstructure:"micropub"` Micropub *configMicropub `mapstructure:"micropub"`
PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"` PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"`
ActivityPub *configActivityPub `mapstructure:"activityPub"` ActivityPub *configActivityPub `mapstructure:"activityPub"`
@ -322,6 +323,13 @@ type configPprof struct {
Address string `mapstructure:"address"` Address string `mapstructure:"address"`
} }
type configPlugin struct {
Path string `mapstructure:"path"`
Type string `mapstructure:"type"`
Import string `mapstructure:"import"`
Config map[string]any `mapstructure:"config"`
}
func (a *goBlog) loadConfigFile(file string) error { func (a *goBlog) loadConfigFile(file string) error {
// Use viper to load the config file // Use viper to load the config file
v := viper.New() v := viper.New()

View File

@ -211,11 +211,11 @@ func (db *database) prepare(query string, args ...any) (*sql.Stmt, []any, error)
const dbNoCache = "nocache" const dbNoCache = "nocache"
func (db *database) exec(query string, args ...any) (sql.Result, error) { func (db *database) Exec(query string, args ...any) (sql.Result, error) {
return db.execContext(context.Background(), query, args...) return db.ExecContext(context.Background(), query, args...)
} }
func (db *database) execContext(c context.Context, query string, args ...any) (sql.Result, error) { func (db *database) ExecContext(c context.Context, query string, args ...any) (sql.Result, error) {
if db == nil || db.db == nil { if db == nil || db.db == nil {
return nil, errors.New("database not initialized") return nil, errors.New("database not initialized")
} }
@ -234,11 +234,11 @@ func (db *database) execContext(c context.Context, query string, args ...any) (s
return db.db.ExecContext(ctx, query, args...) return db.db.ExecContext(ctx, query, args...)
} }
func (db *database) query(query string, args ...any) (*sql.Rows, error) { func (db *database) Query(query string, args ...any) (*sql.Rows, error) {
return db.queryContext(context.Background(), query, args...) return db.QueryContext(context.Background(), query, args...)
} }
func (db *database) queryContext(c context.Context, query string, args ...any) (rows *sql.Rows, err error) { func (db *database) QueryContext(c context.Context, query string, args ...any) (rows *sql.Rows, err error) {
if db == nil || db.db == nil { if db == nil || db.db == nil {
return nil, errors.New("database not initialized") return nil, errors.New("database not initialized")
} }
@ -257,11 +257,11 @@ func (db *database) queryContext(c context.Context, query string, args ...any) (
return return
} }
func (db *database) queryRow(query string, args ...any) (*sql.Row, error) { func (db *database) QueryRow(query string, args ...any) (*sql.Row, error) {
return db.queryRowContext(context.Background(), query, args...) return db.QueryRowContext(context.Background(), query, args...)
} }
func (db *database) queryRowContext(c context.Context, query string, args ...any) (row *sql.Row, err error) { func (db *database) QueryRowContext(c context.Context, query string, args ...any) (row *sql.Row, err error) {
if db == nil || db.db == nil { if db == nil || db.db == nil {
return nil, errors.New("database not initialized") return nil, errors.New("database not initialized")
} }
@ -283,5 +283,5 @@ func (db *database) queryRowContext(c context.Context, query string, args ...any
// Other things // Other things
func (d *database) rebuildFTSIndex() { func (d *database) rebuildFTSIndex() {
_, _ = d.exec("insert into posts_fts(posts_fts) values ('rebuild')") _, _ = d.Exec("insert into posts_fts(posts_fts) values ('rebuild')")
} }

View File

@ -15,17 +15,17 @@ func Test_database(t *testing.T) {
t.Fatalf("Error: %v", err) t.Fatalf("Error: %v", err)
} }
_, err = db.exec("create table test(test text);") _, err = db.Exec("create table test(test text);")
if err != nil { if err != nil {
t.Fatalf("Error: %v", err) t.Fatalf("Error: %v", err)
} }
_, err = db.exec("insert into test (test) values ('Test')") _, err = db.Exec("insert into test (test) values ('Test')")
if err != nil { if err != nil {
t.Fatalf("Error: %v", err) t.Fatalf("Error: %v", err)
} }
row, err := db.queryRow("select count(test) from test") row, err := db.QueryRow("select count(test) from test")
if err != nil { if err != nil {
t.Fatalf("Error: %v", err) t.Fatalf("Error: %v", err)
} }
@ -38,7 +38,7 @@ func Test_database(t *testing.T) {
t.Error("Wrong result") t.Error("Wrong result")
} }
rows, err := db.query("select count(test), test from test") rows, err := db.Query("select count(test), test from test")
if err != nil { if err != nil {
t.Fatalf("Error: %v", err) t.Fatalf("Error: %v", err)
} }

1
go.mod
View File

@ -53,6 +53,7 @@ require (
// master // master
github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
github.com/traefik/yaegi v0.14.1
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2
github.com/yuin/goldmark v1.4.13 github.com/yuin/goldmark v1.4.13
// master // master

2
go.sum
View File

@ -481,6 +481,8 @@ github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec h1:o5aL1yX+/xzvK4
github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec/go.mod h1:795sjVRFo5wWyN6oOZp0RYienGGBJjpAlgOz2nCngA0= github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec/go.mod h1:795sjVRFo5wWyN6oOZp0RYienGGBJjpAlgOz2nCngA0=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
github.com/traefik/yaegi v0.14.1 h1:t0ssyzeZCWTFGd/JnVuDxH/slMQfYg+2CDD4dLW/rU0=
github.com/traefik/yaegi v0.14.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4= github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4=
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=

14
http.go
View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"time" "time"
@ -17,6 +18,7 @@ import (
"github.com/klauspost/compress/flate" "github.com/klauspost/compress/flate"
"go.goblog.app/app/pkgs/httpcompress" "go.goblog.app/app/pkgs/httpcompress"
"go.goblog.app/app/pkgs/maprouter" "go.goblog.app/app/pkgs/maprouter"
"go.goblog.app/app/pkgs/plugintypes"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -43,6 +45,16 @@ func (a *goBlog) startServer() (err error) {
if a.httpsConfigured(false) { if a.httpsConfigured(false) {
h = h.Append(a.securityHeaders) h = h.Append(a.securityHeaders)
} }
// Add plugin middlewares
middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, "middleware")
sort.Slice(middlewarePlugins, func(i, j int) bool {
// Sort with descending prio
return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio()
})
for _, plugin := range middlewarePlugins {
h = h.Append(plugin.Handler)
}
// Finally...
finalHandler := h.ThenFunc(func(w http.ResponseWriter, r *http.Request) { finalHandler := h.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
a.d.ServeHTTP(w, r) a.d.ServeHTTP(w, r)
}) })
@ -245,7 +257,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
} }
// Check if post or alias // Check if post or alias
path := r.URL.Path path := r.URL.Path
row, err := a.db.queryRow(` row, err := a.db.QueryRow(`
-- normal posts -- normal posts
select 'post', status, 200 from posts where path = @path select 'post', status, 200 from posts where path = @path
union all union all

View File

@ -182,7 +182,7 @@ func (db *database) indieAuthSaveAuthRequest(data *indieauth.AuthenticationReque
// Generate a code to identify the request // Generate a code to identify the request
code := uuid.NewString() code := uuid.NewString()
// Save the request // Save the request
_, err := db.exec( _, err := db.Exec(
"insert into indieauthauth (time, code, client, redirect, scope, challenge, challengemethod) values (?, ?, ?, ?, ?, ?, ?)", "insert into indieauthauth (time, code, client, redirect, scope, challenge, challengemethod) values (?, ?, ?, ?, ?, ?, ?)",
time.Now().UTC().Unix(), code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "), data.CodeChallenge, data.CodeChallengeMethod, time.Now().UTC().Unix(), code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "), data.CodeChallenge, data.CodeChallengeMethod,
) )
@ -194,7 +194,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen
// code valid for 10 minutes // code valid for 10 minutes
maxAge := time.Now().UTC().Add(-10 * time.Minute).Unix() maxAge := time.Now().UTC().Add(-10 * time.Minute).Unix()
// Query the database // Query the database
row, err := db.queryRow("select client, redirect, scope, challenge, challengemethod from indieauthauth where time >= ? and code = ?", maxAge, code) row, err := db.QueryRow("select client, redirect, scope, challenge, challengemethod from indieauthauth where time >= ? and code = ?", maxAge, code)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -210,7 +210,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen
data.Scopes = strings.Split(scope, " ") data.Scopes = strings.Split(scope, " ")
} }
// Delete the auth code and expired auth codes // Delete the auth code and expired auth codes
_, _ = db.exec("delete from indieauthauth where code = ? or time < ?", code, maxAge) _, _ = db.Exec("delete from indieauthauth where code = ? or time < ?", code, maxAge)
return data, nil return data, nil
} }
@ -251,7 +251,7 @@ func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Reque
func (db *database) indieAuthVerifyToken(token string) (data *indieauth.AuthenticationRequest, err error) { func (db *database) indieAuthVerifyToken(token string) (data *indieauth.AuthenticationRequest, err error) {
token = strings.ReplaceAll(token, "Bearer ", "") token = strings.ReplaceAll(token, "Bearer ", "")
data = &indieauth.AuthenticationRequest{Scopes: []string{}} data = &indieauth.AuthenticationRequest{Scopes: []string{}}
row, err := db.queryRow("select client, scope from indieauthtoken where token = @token", sql.Named("token", token)) row, err := db.QueryRow("select client, scope from indieauthtoken where token = @token", sql.Named("token", token))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -271,13 +271,13 @@ func (db *database) indieAuthVerifyToken(token string) (data *indieauth.Authenti
// Save a new token to the database // Save a new token to the database
func (db *database) indieAuthSaveToken(data *indieauth.AuthenticationRequest) (string, error) { func (db *database) indieAuthSaveToken(data *indieauth.AuthenticationRequest) (string, error) {
token := uuid.NewString() token := uuid.NewString()
_, err := db.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", time.Now().UTC().Unix(), token, data.ClientID, strings.Join(data.Scopes, " ")) _, err := db.Exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", time.Now().UTC().Unix(), token, data.ClientID, strings.Join(data.Scopes, " "))
return token, err return token, err
} }
// Revoke and delete the token from the database // Revoke and delete the token from the database
func (db *database) indieAuthRevokeToken(token string) { func (db *database) indieAuthRevokeToken(token string) {
if token != "" { if token != "" {
_, _ = db.exec("delete from indieauthtoken where token=?", token) _, _ = db.Exec("delete from indieauthtoken where token=?", token)
} }
} }

View File

@ -67,6 +67,12 @@ func main() {
return return
} }
// Initialize plugins
if err = app.initPlugins(); err != nil {
app.logErrAndQuit("Failed to init plugins:", err.Error())
return
}
// Healthcheck tool // Healthcheck tool
if len(os.Args) >= 2 && os.Args[1] == "healthcheck" { if len(os.Args) >= 2 && os.Args[1] == "healthcheck" {
// Connect to public address + "/ping" and exit with 0 when successful // Connect to public address + "/ping" and exit with 0 when successful

View File

@ -41,19 +41,19 @@ func (a *goBlog) sendNotification(text string) {
} }
func (db *database) saveNotification(n *notification) error { func (db *database) saveNotification(n *notification) error {
if _, err := db.exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil { if _, err := db.Exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
return err return err
} }
return nil return nil
} }
func (db *database) deleteNotification(id int) error { func (db *database) deleteNotification(id int) error {
_, err := db.exec("delete from notifications where id = @id", sql.Named("id", id)) _, err := db.Exec("delete from notifications where id = @id", sql.Named("id", id))
return err return err
} }
func (db *database) deleteAllNotifications() error { func (db *database) deleteAllNotifications() error {
_, err := db.exec("delete from notifications") _, err := db.Exec("delete from notifications")
return err return err
} }
@ -75,7 +75,7 @@ func buildNotificationsQuery(config *notificationsRequestConfig) (query string,
func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) { func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) {
notifications := []*notification{} notifications := []*notification{}
query, args := buildNotificationsQuery(config) query, args := buildNotificationsQuery(config)
rows, err := db.query(query, args...) rows, err := db.Query(query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,7 +93,7 @@ func (db *database) getNotifications(config *notificationsRequestConfig) ([]*not
func (db *database) countNotifications(config *notificationsRequestConfig) (count int, err error) { func (db *database) countNotifications(config *notificationsRequestConfig) (count int, err error) {
query, params := buildNotificationsQuery(config) query, params := buildNotificationsQuery(config)
query = "select count(*) from (" + query + ")" query = "select count(*) from (" + query + ")"
row, err := db.queryRow(query, params...) row, err := db.QueryRow(query, params...)
if err != nil { if err != nil {
return return
} }

View File

@ -14,7 +14,7 @@ func (db *database) cachePersistentlyContext(ctx context.Context, key string, da
if db == nil { if db == nil {
return errors.New("database is nil") return errors.New("database is nil")
} }
_, err := db.execContext(ctx, "insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", utcNowString())) _, err := db.ExecContext(ctx, "insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", utcNowString()))
return err return err
} }
@ -27,7 +27,7 @@ func (db *database) retrievePersistentCacheContext(c context.Context, key string
return nil, errors.New("database is nil") return nil, errors.New("database is nil")
} }
d, err, _ := db.pc.Do(key, func() (any, error) { d, err, _ := db.pc.Do(key, func() (any, error) {
if row, err := db.queryRowContext(c, "select data from persistent_cache where key = @key", sql.Named("key", key)); err != nil { if row, err := db.QueryRowContext(c, "select data from persistent_cache where key = @key", sql.Named("key", key)); err != nil {
return nil, err return nil, err
} else { } else {
err = row.Scan(&data) err = row.Scan(&data)
@ -51,6 +51,6 @@ func (db *database) clearPersistentCache(pattern string) error {
} }
func (db *database) clearPersistentCacheContext(c context.Context, pattern string) error { func (db *database) clearPersistentCacheContext(c context.Context, pattern string) error {
_, err := db.execContext(c, "delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern)) _, err := db.ExecContext(c, "delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern))
return err return err
} }

59
pkgs/plugins/plugin.go Normal file
View File

@ -0,0 +1,59 @@
package plugins
import (
"fmt"
"path/filepath"
"reflect"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
type plugin struct {
Config *PluginConfig
plugin reflect.Value
}
// PluginConfig is the configuration of the plugin.
type PluginConfig struct {
// Path is the storage path of the plugin.
Path string
// ImportPath is the module path i.e. "github.com/user/module".
ImportPath string
// PluginType is the type of plugin, this plugin is checked against that type.
// The available types are specified by the implementor of this package.
PluginType string
}
func (p *plugin) initPlugin(host *PluginHost) error {
const errText = "initPlugin: %w"
interpreter := interp.New(interp.Options{
GoPath: p.Config.Path,
})
if err := interpreter.Use(stdlib.Symbols); err != nil {
return fmt.Errorf(errText, err)
}
if err := interpreter.Use(host.Symbols); err != nil {
return fmt.Errorf(errText, err)
}
if _, err := interpreter.Eval(fmt.Sprintf(`import "%s"`, p.Config.ImportPath)); err != nil {
return fmt.Errorf(errText, err)
}
v, err := interpreter.Eval(filepath.Base(p.Config.ImportPath) + ".GetPlugin")
if err != nil {
return fmt.Errorf(errText, err)
}
result := v.Call([]reflect.Value{})
if len(result) > 1 {
return fmt.Errorf(errText+": function GetPlugin has more than one return value", ErrValidatingPlugin)
}
p.plugin = result[0]
return nil
}

80
pkgs/plugins/plugins.go Normal file
View File

@ -0,0 +1,80 @@
package plugins
import (
"fmt"
"reflect"
"github.com/traefik/yaegi/interp"
)
// NewPluginHost initializes a PluginHost.
func NewPluginHost(symbols interp.Exports) *PluginHost {
return &PluginHost{
Plugins: []*plugin{},
PluginTypes: map[string]reflect.Type{},
Symbols: symbols,
}
}
// AddPluginType adds a plugin type to the list.
// The interface for the pluginType parameter should be a nil of the plugin type interface:
//
// (*PluginInterface)(nil)
func (h *PluginHost) AddPluginType(name string, pluginType interface{}) {
h.PluginTypes[name] = reflect.TypeOf(pluginType).Elem()
}
// LoadPlugin loads a new plugin to the host.
func (h *PluginHost) LoadPlugin(config *PluginConfig) (any, error) {
p := &plugin{
Config: config,
}
err := p.initPlugin(h)
if err != nil {
return nil, err
}
err = h.validatePlugin(p)
if err != nil {
return nil, err
}
h.Plugins = append(h.Plugins, p)
return p.plugin.Interface(), nil
}
func (h *PluginHost) validatePlugin(p *plugin) error {
pType := reflect.TypeOf(p.plugin.Interface())
if _, ok := h.PluginTypes[p.Config.PluginType]; !ok {
return fmt.Errorf("validatePlugin: %v: %w", p.Config.PluginType, ErrInvalidType)
}
if !pType.Implements(h.PluginTypes[p.Config.PluginType]) {
return fmt.Errorf("validatePlugin:%v: %w %v", p, ErrValidatingPlugin, p.Config.PluginType)
}
return nil
}
// GetPlugins returns a list of all plugins.
func (h *PluginHost) GetPlugins() (list []any) {
for _, p := range h.Plugins {
list = append(list, p.plugin.Interface())
}
return
}
// GetPluginsForType returns all the plugins that are of type pluginType or empty if the pluginType doesn't exist.
func GetPluginsForType[T any](h *PluginHost, pluginType string) (list []T) {
if _, ok := h.PluginTypes[pluginType]; !ok {
return
}
for _, p := range h.Plugins {
if p.Config.PluginType != pluginType {
continue
}
if t, ok := p.plugin.Interface().(T); ok {
list = append(list, t)
}
}
return
}

25
pkgs/plugins/types.go Normal file
View File

@ -0,0 +1,25 @@
package plugins
import (
"errors"
"reflect"
"github.com/traefik/yaegi/interp"
)
// PluginHost manages the plugins.
type PluginHost struct {
// Plugins contains a list of the plugins.
Plugins []*plugin
// PluginTypes is a list of plugins types that plugins have to use at least one of.
PluginTypes map[string]reflect.Type
// Symbols is the map of symbols generated by yaegi extract.
Symbols interp.Exports
}
var (
// ErrInvalidType is returned when the plugin type specified by the plugin is invalid.
ErrInvalidType = errors.New("invalid plugin type")
// ErrValidatingPlugin is returned when the plugin fails to fully implement the interface of the plugin type.
ErrValidatingPlugin = errors.New("plugin does not implement type")
)

51
pkgs/plugintypes/types.go Normal file
View File

@ -0,0 +1,51 @@
package plugintypes
import (
"context"
"database/sql"
"net/http"
)
// Interface to GoBlog
// App is used to access GoBlog's app instance.
type App interface {
GetDatabase() Database
}
// Database is used to provide access to GoBlog's database.
type Database interface {
Exec(string, ...any) (sql.Result, error)
ExecContext(context.Context, string, ...any) (sql.Result, error)
Query(string, ...any) (*sql.Rows, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRow(string, ...any) (*sql.Row, error)
QueryRowContext(context.Context, string, ...any) (*sql.Row, error)
}
// Plugin types
// SetApp is used in all plugin types to allow
// GoBlog set it's app instance to be accessible by the plugin.
type SetApp interface {
SetApp(App)
}
// SetConfig is used in all plugin types to allow
// GoBlog set plugin configuration.
type SetConfig interface {
SetConfig(map[string]any)
}
type Exec interface {
SetApp
SetConfig
Exec()
}
type Middleware interface {
SetApp
SetConfig
Handler(http.Handler) http.Handler
Prio() int
}

View File

@ -0,0 +1,153 @@
// Code generated by 'yaegi extract go.goblog.app/app/pkgs/plugintypes'. DO NOT EDIT.
// MIT License
//
// Copyright (c) 2020 - 2022 Jan-Lukas Else
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package yaegiwrappers
import (
"context"
"database/sql"
"go.goblog.app/app/pkgs/plugintypes"
"net/http"
"reflect"
)
func init() {
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
// type definitions
"App": reflect.ValueOf((*plugintypes.App)(nil)),
"Database": reflect.ValueOf((*plugintypes.Database)(nil)),
"Exec": reflect.ValueOf((*plugintypes.Exec)(nil)),
"Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)),
"SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)),
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
// interface wrapper definitions
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
"_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)),
"_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)),
"_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(nil)),
"_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)),
"_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)),
}
}
// _go_goblog_app_app_pkgs_plugintypes_App is an interface wrapper for App type
type _go_goblog_app_app_pkgs_plugintypes_App struct {
IValue interface{}
WGetDatabase func() plugintypes.Database
}
func (W _go_goblog_app_app_pkgs_plugintypes_App) GetDatabase() plugintypes.Database {
return W.WGetDatabase()
}
// _go_goblog_app_app_pkgs_plugintypes_Database is an interface wrapper for Database type
type _go_goblog_app_app_pkgs_plugintypes_Database struct {
IValue interface{}
WExec func(a0 string, a1 ...any) (sql.Result, error)
WExecContext func(a0 context.Context, a1 string, a2 ...any) (sql.Result, error)
WQuery func(a0 string, a1 ...any) (*sql.Rows, error)
WQueryContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error)
WQueryRow func(a0 string, a1 ...any) (*sql.Row, error)
WQueryRowContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Exec(a0 string, a1 ...any) (sql.Result, error) {
return W.WExec(a0, a1...)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) ExecContext(a0 context.Context, a1 string, a2 ...any) (sql.Result, error) {
return W.WExecContext(a0, a1, a2...)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Query(a0 string, a1 ...any) (*sql.Rows, error) {
return W.WQuery(a0, a1...)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryContext(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error) {
return W.WQueryContext(a0, a1, a2...)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRow(a0 string, a1 ...any) (*sql.Row, error) {
return W.WQueryRow(a0, a1...)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRowContext(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error) {
return W.WQueryRowContext(a0, a1, a2...)
}
// _go_goblog_app_app_pkgs_plugintypes_Exec is an interface wrapper for Exec type
type _go_goblog_app_app_pkgs_plugintypes_Exec struct {
IValue interface{}
WExec func()
WSetApp func(a0 plugintypes.App)
WSetConfig func(a0 map[string]any)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) Exec() {
W.WExec()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetConfig(a0 map[string]any) {
W.WSetConfig(a0)
}
// _go_goblog_app_app_pkgs_plugintypes_Middleware is an interface wrapper for Middleware type
type _go_goblog_app_app_pkgs_plugintypes_Middleware struct {
IValue interface{}
WHandler func(a0 http.Handler) http.Handler
WPrio func() int
WSetApp func(a0 plugintypes.App)
WSetConfig func(a0 map[string]any)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Handler(a0 http.Handler) http.Handler {
return W.WHandler(a0)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int {
return W.WPrio()
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
}
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetConfig(a0 map[string]any) {
W.WSetConfig(a0)
}
// _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
IValue interface{}
WSetApp func(a0 plugintypes.App)
}
func (W _go_goblog_app_app_pkgs_plugintypes_SetApp) SetApp(a0 plugintypes.App) {
W.WSetApp(a0)
}
// _go_goblog_app_app_pkgs_plugintypes_SetConfig is an interface wrapper for SetConfig type
type _go_goblog_app_app_pkgs_plugintypes_SetConfig struct {
IValue interface{}
WSetConfig func(a0 map[string]any)
}
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) {
W.WSetConfig(a0)
}

View File

@ -0,0 +1,11 @@
package yaegiwrappers
import (
"reflect"
)
var (
Symbols = make(map[string]map[string]reflect.Value)
)
//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/plugintypes

52
plugins.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"go.goblog.app/app/pkgs/plugins"
"go.goblog.app/app/pkgs/plugintypes"
"go.goblog.app/app/pkgs/yaegiwrappers"
)
func (a *goBlog) initPlugins() error {
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
a.pluginHost.AddPluginType("exec", (*plugintypes.Exec)(nil))
a.pluginHost.AddPluginType("middleware", (*plugintypes.Middleware)(nil))
for _, pc := range a.cfg.Plugins {
if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
Path: pc.Path,
ImportPath: pc.Import,
PluginType: pc.Type,
}); err != nil {
return err
} else if pluginInterface != nil {
if setAppPlugin, ok := pluginInterface.(plugintypes.SetApp); ok {
setAppPlugin.SetApp(a)
}
if setConfigPlugin, ok := pluginInterface.(plugintypes.SetConfig); ok {
setConfigPlugin.SetConfig(pc.Config)
}
}
}
execs := getPluginsForType[plugintypes.Exec](a, "exec")
for _, p := range execs {
go p.Exec()
}
return nil
}
func getPluginsForType[T any](a *goBlog, pluginType string) (list []T) {
return plugins.GetPluginsForType[T](a.pluginHost, pluginType)
}
// Implement all needed interfaces for goblog
var _ plugintypes.App = &goBlog{}
func (a *goBlog) GetDatabase() plugintypes.Database {
return a.db
}
var _ plugintypes.Database = &database{}

View File

@ -0,0 +1,37 @@
package demoexec
import (
"fmt"
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() plugintypes.Exec {
return &plugin{}
}
type plugin struct {
app plugintypes.App
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
func (*plugin) SetConfig(_ map[string]any) {
// Ignore
}
func (p *plugin) Exec() {
fmt.Println("Hello World from the demo plugin!")
row, _ := p.app.GetDatabase().QueryRow("select count (*) from posts")
var count int
if err := row.Scan(&count); err != nil {
fmt.Println(fmt.Errorf("failed to count posts: %w", err))
return
}
fmt.Printf("Number of posts in database: %d", count)
fmt.Println()
}

View File

@ -0,0 +1,41 @@
package demomiddleware
import (
"fmt"
"net/http"
"go.goblog.app/app/pkgs/plugintypes"
)
func GetPlugin() plugintypes.Middleware {
return &plugin{}
}
type plugin struct {
app plugintypes.App
config map[string]any
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
func (p *plugin) SetConfig(config map[string]any) {
p.config = config
}
func (p *plugin) Prio() int {
if prioAny, ok := p.config["prio"]; ok {
if prio, ok := prioAny.(int); ok {
return prio
}
}
return 100
}
func (p *plugin) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Demo", fmt.Sprintf("This is from the demo middleware with prio %d", p.Prio()))
next.ServeHTTP(w, r)
})
}

55
plugins_test.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.goblog.app/app/pkgs/plugintypes"
)
func TestExecPlugin(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Plugins = []*configPlugin{
{
Path: "./plugins/demo",
Type: "exec",
Import: "demoexec",
},
}
err := app.initConfig(false)
require.NoError(t, err)
err = app.initPlugins()
require.NoError(t, err)
}
func TestMiddlewarePlugin(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
app.cfg.Plugins = []*configPlugin{
{
Path: "./plugins/demo",
Type: "middleware",
Import: "demomiddleware",
Config: map[string]any{
"prio": 99,
},
},
}
err := app.initConfig(false)
require.NoError(t, err)
err = app.initPlugins()
require.NoError(t, err)
middlewarePlugins := getPluginsForType[plugintypes.Middleware](app, "middleware")
if assert.Len(t, middlewarePlugins, 1) {
mdw := middlewarePlugins[0]
assert.Equal(t, 99, mdw.Prio())
}
}

View File

@ -203,7 +203,7 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
// Commit transaction // Commit transaction
sqlBuilder.WriteString("commit;") sqlBuilder.WriteString("commit;")
// Execute // Execute
if _, err := db.exec(sqlBuilder.String(), sqlArgs...); err != nil { if _, err := db.Exec(sqlBuilder.String(), sqlArgs...); err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed: posts.path") { if strings.Contains(err.Error(), "UNIQUE constraint failed: posts.path") {
return errors.New("post already exists at given path") return errors.New("post already exists at given path")
} }
@ -229,7 +229,7 @@ func (a *goBlog) deletePost(path string) error {
// Post exists, check if it's already marked as deleted // Post exists, check if it's already marked as deleted
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) { if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
// Post is already marked as deleted, delete it from database // Post is already marked as deleted, delete it from database
if _, err = a.db.exec( if _, err = a.db.Exec(
`begin; delete from posts where path = ?; insert or ignore into deleted (path) values (?); commit;`, `begin; delete from posts where path = ?; insert or ignore into deleted (path) values (?); commit;`,
dbNoCache, p.Path, p.Path, p.Path, dbNoCache, p.Path, p.Path, p.Path,
); err != nil { ); err != nil {
@ -250,7 +250,7 @@ func (a *goBlog) deletePost(path string) error {
} }
p.Parameters["deleted"] = []string{deletedTime} p.Parameters["deleted"] = []string{deletedTime}
// Mark post as deleted // Mark post as deleted
if _, err = a.db.exec( if _, err = a.db.Exec(
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; insert into post_parameters (path, parameter, value) values (?, 'deleted', ?); commit;`, `begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; insert into post_parameters (path, parameter, value) values (?, 'deleted', ?); commit;`,
dbNoCache, p.Status, p.Path, p.Path, p.Path, deletedTime, dbNoCache, p.Status, p.Path, p.Path, p.Path, deletedTime,
); err != nil { ); err != nil {
@ -283,7 +283,7 @@ func (a *goBlog) undeletePost(path string) error {
// Remove parameter // Remove parameter
p.Parameters["deleted"] = nil p.Parameters["deleted"] = nil
// Update database // Update database
if _, err = a.db.exec( if _, err = a.db.Exec(
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; commit;`, `begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; commit;`,
dbNoCache, p.Status, p.Path, p.Path, dbNoCache, p.Status, p.Path, p.Path,
); err != nil { ); err != nil {
@ -320,7 +320,7 @@ func (db *database) replacePostParam(path, param string, values []string) error
// Commit transaction // Commit transaction
sqlBuilder.WriteString("commit;") sqlBuilder.WriteString("commit;")
// Execute // Execute
_, err := db.exec(sqlBuilder.String(), sqlArgs...) _, err := db.Exec(sqlBuilder.String(), sqlArgs...)
bufferpool.Put(sqlBuilder) bufferpool.Put(sqlBuilder)
if err != nil { if err != nil {
return err return err
@ -514,7 +514,7 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
// Order // Order
queryBuilder.WriteString(" order by id") queryBuilder.WriteString(" order by id")
// Query // Query
rows, err := d.query(queryBuilder.String(), sqlArgs...) rows, err := d.Query(queryBuilder.String(), sqlArgs...)
if err != nil { if err != nil {
return err return err
} }
@ -542,7 +542,7 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) { func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) {
// Query posts // Query posts
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, priority") query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, priority")
rows, err := a.db.query(query, queryParams...) rows, err := a.db.Query(query, queryParams...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -595,7 +595,7 @@ func (a *goBlog) getPost(path string) (*post, error) {
func (d *database) countPosts(config *postsRequestConfig) (count int, err error) { func (d *database) countPosts(config *postsRequestConfig) (count int, err error) {
query, params := buildPostsQuery(config, "path") query, params := buildPostsQuery(config, "path")
row, err := d.queryRow("select count(distinct path) from ("+query+")", params...) row, err := d.QueryRow("select count(distinct path) from ("+query+")", params...)
if err != nil { if err != nil {
return return
} }
@ -606,7 +606,7 @@ func (d *database) countPosts(config *postsRequestConfig) (count int, err error)
func (a *goBlog) getRandomPostPath(blog string) (path string, err error) { func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
sections := lo.Keys(a.cfg.Blogs[blog].Sections) sections := lo.Keys(a.cfg.Blogs[blog].Sections)
query, params := buildPostsQuery(&postsRequestConfig{randomOrder: true, limit: 1, blog: blog, sections: sections}, "path") query, params := buildPostsQuery(&postsRequestConfig{randomOrder: true, limit: 1, blog: blog, sections: sections}, "path")
row, err := a.db.queryRow(query, params...) row, err := a.db.QueryRow(query, params...)
if err != nil { if err != nil {
return return
} }
@ -621,7 +621,7 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) { func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
// TODO: Query posts the normal way // TODO: Query posts the normal way
rows, err := d.query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished)) rows, err := d.Query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -663,7 +663,7 @@ func (db *database) usesOfMediaFile(names ...string) (counts []int, err error) {
nameValues.WriteByte(')') nameValues.WriteByte(')')
sqlArgs = append(sqlArgs, sql.Named(named, n)) sqlArgs = append(sqlArgs, sql.Named(named, n))
} }
rows, err := db.query(fmt.Sprintf(mediaUseSql, nameValues.String()), sqlArgs...) rows, err := db.Query(fmt.Sprintf(mediaUseSql, nameValues.String()), sqlArgs...)
bufferpool.Put(nameValues) bufferpool.Put(nameValues)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -402,7 +402,7 @@ func Test_postDeletesParams(t *testing.T) {
err = app.deletePost("/test/abc") err = app.deletePost("/test/abc")
require.NoError(t, err) require.NoError(t, err)
row, err := app.db.queryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test") row, err := app.db.QueryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
require.NoError(t, err) require.NoError(t, err)
var count int var count int
@ -415,7 +415,7 @@ func Test_postDeletesParams(t *testing.T) {
err = app.deletePost("/test/abc") err = app.deletePost("/test/abc")
require.NoError(t, err) require.NoError(t, err)
row, err = app.db.queryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test") row, err = app.db.QueryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
require.NoError(t, err) require.NoError(t, err)
err = row.Scan(&count) err = row.Scan(&count)

View File

@ -22,7 +22,7 @@ func (a *goBlog) enqueue(name string, content []byte, schedule time.Time) error
if len(content) == 0 { if len(content) == 0 {
return errors.New("empty content") return errors.New("empty content")
} }
_, err := a.db.exec( _, err := a.db.Exec(
"insert into queue (name, content, schedule) values (@name, @content, @schedule)", "insert into queue (name, content, schedule) values (@name, @content, @schedule)",
sql.Named("name", name), sql.Named("name", name),
sql.Named("content", content), sql.Named("content", content),
@ -35,7 +35,7 @@ func (a *goBlog) enqueue(name string, content []byte, schedule time.Time) error
} }
func (a *goBlog) reschedule(qi *queueItem, dur time.Duration) error { func (a *goBlog) reschedule(qi *queueItem, dur time.Duration) error {
_, err := a.db.exec( _, err := a.db.Exec(
"update queue set schedule = @schedule, content = @content where id = @id", "update queue set schedule = @schedule, content = @content where id = @id",
sql.Named("schedule", qi.schedule.Add(dur).UTC().Format(time.RFC3339Nano)), sql.Named("schedule", qi.schedule.Add(dur).UTC().Format(time.RFC3339Nano)),
sql.Named("content", qi.content), sql.Named("content", qi.content),
@ -45,12 +45,12 @@ func (a *goBlog) reschedule(qi *queueItem, dur time.Duration) error {
} }
func (a *goBlog) dequeue(qi *queueItem) error { func (a *goBlog) dequeue(qi *queueItem) error {
_, err := a.db.exec("delete from queue where id = @id", sql.Named("id", qi.id)) _, err := a.db.Exec("delete from queue where id = @id", sql.Named("id", qi.id))
return err return err
} }
func (a *goBlog) peekQueue(ctx context.Context, name string) (*queueItem, error) { func (a *goBlog) peekQueue(ctx context.Context, name string) (*queueItem, error) {
row, err := a.db.queryRowContext( row, err := a.db.QueryRowContext(
ctx, ctx,
"select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1", "select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1",
sql.Named("name", name), sql.Named("name", name),

View File

@ -77,7 +77,7 @@ func (a *goBlog) saveReaction(reaction, path string) error {
defer a.reactionsSfg.Forget(path) defer a.reactionsSfg.Forget(path)
defer a.reactionsCache.Del(path) defer a.reactionsCache.Del(path)
// Insert reaction // Insert reaction
_, err := a.db.exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction) _, err := a.db.Exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction)
return err return err
} }
@ -125,7 +125,7 @@ func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) {
sqlBuf.WriteString(") and path not in (select path from post_parameters where parameter=? and value=?)") sqlBuf.WriteString(") and path not in (select path from post_parameters where parameter=? and value=?)")
sqlArgs = append(sqlArgs, reactionsPostParam, "false") sqlArgs = append(sqlArgs, reactionsPostParam, "false")
// Execute query // Execute query
rows, err := a.db.query(sqlBuf.String(), sqlArgs...) rows, err := a.db.Query(sqlBuf.String(), sqlArgs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -23,7 +23,7 @@ const (
func (a *goBlog) initSessions() { func (a *goBlog) initSessions() {
deleteExpiredSessions := func() { deleteExpiredSessions := func() {
if _, err := a.db.exec( if _, err := a.db.Exec(
"delete from sessions where expires < @now", "delete from sessions where expires < @now",
sql.Named("now", utcNowString()), sql.Named("now", utcNowString()),
); err != nil { ); err != nil {
@ -103,14 +103,14 @@ func (s *dbSessionStore) Delete(_ *http.Request, w http.ResponseWriter, session
for k := range session.Values { for k := range session.Values {
delete(session.Values, k) delete(session.Values, k)
} }
if _, err := s.db.exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil { if _, err := s.db.Exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil {
return err return err
} }
return nil return nil
} }
func (s *dbSessionStore) load(session *sessions.Session) (err error) { func (s *dbSessionStore) load(session *sessions.Session) (err error) {
row, err := s.db.queryRow( row, err := s.db.QueryRow(
"select data, created, modified, expires from sessions where id = @id and expires > @now", "select data, created, modified, expires from sessions where id = @id and expires > @now",
sql.Named("id", session.ID), sql.Named("id", session.ID),
sql.Named("now", utcNowString()), sql.Named("now", utcNowString()),
@ -142,7 +142,7 @@ func (s *dbSessionStore) insert(session *sessions.Session) (err error) {
session.ID = session.Name() + "-" + uuid.NewString() session.ID = session.Name() + "-" + uuid.NewString()
created, modified := utcNowString(), utcNowString() created, modified := utcNowString(), utcNowString()
expires := time.Now().UTC().Add(time.Second * time.Duration(session.Options.MaxAge)).Format(time.RFC3339) expires := time.Now().UTC().Add(time.Second * time.Duration(session.Options.MaxAge)).Format(time.RFC3339)
_, err = s.db.exec( _, err = s.db.Exec(
"insert or replace into sessions(id, data, created, modified, expires) values(@id, @data, @created, @modified, @expires)", "insert or replace into sessions(id, data, created, modified, expires) values(@id, @data, @created, @modified, @expires)",
sql.Named("id", session.ID), sql.Named("id", session.ID),
sql.Named("data", encoded.Bytes()), sql.Named("data", encoded.Bytes()),
@ -163,7 +163,7 @@ func (s *dbSessionStore) save(session *sessions.Session) (err error) {
if err = gob.NewEncoder(encoded).Encode(session.Values); err != nil { if err = gob.NewEncoder(encoded).Encode(session.Values); err != nil {
return err return err
} }
_, err = s.db.exec( _, err = s.db.Exec(
"update sessions set data = @data, modified = @modified where id = @id", "update sessions set data = @data, modified = @modified where id = @id",
sql.Named("data", encoded.Bytes()), sql.Named("data", encoded.Bytes()),
sql.Named("modified", utcNowString()), sql.Named("modified", utcNowString()),

View File

@ -18,7 +18,7 @@ const (
) )
func (a *goBlog) getSettingValue(name string) (string, error) { func (a *goBlog) getSettingValue(name string) (string, error) {
row, err := a.db.queryRow("select value from settings where name = @name", sql.Named("name", name)) row, err := a.db.QueryRow("select value from settings where name = @name", sql.Named("name", name))
if err != nil { if err != nil {
return "", return "",
err err
@ -45,7 +45,7 @@ func (a *goBlog) getBooleanSettingValue(name string, defaultValue bool) (bool, e
} }
func (a *goBlog) saveSettingValue(name, value string) error { func (a *goBlog) saveSettingValue(name, value string) error {
_, err := a.db.exec( _, err := a.db.Exec(
"insert into settings (name, value) values (@name, @value) on conflict (name) do update set value = @value2", "insert into settings (name, value) values (@name, @value) on conflict (name) do update set value = @value2",
sql.Named("name", name), sql.Named("name", name),
sql.Named("value", value), sql.Named("value", value),
@ -70,7 +70,7 @@ func (a *goBlog) loadSections() error {
} }
func (a *goBlog) getSections(blog string) (map[string]*configSection, error) { func (a *goBlog) getSections(blog string) (map[string]*configSection, error) {
rows, err := a.db.query("select name, title, description, pathtemplate, showfull from sections where blog = @blog", sql.Named("blog", blog)) rows, err := a.db.Query("select name, title, description, pathtemplate, showfull from sections where blog = @blog", sql.Named("blog", blog))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -99,7 +99,7 @@ func (a *goBlog) saveAllSections() error {
} }
func (a *goBlog) saveSection(blog string, section *configSection) error { func (a *goBlog) saveSection(blog string, section *configSection) error {
_, err := a.db.exec( _, err := a.db.Exec(
` `
insert into sections (blog, name, title, description, pathtemplate, showfull) values (@blog, @name, @title, @description, @pathtemplate, @showfull) insert into sections (blog, name, title, description, pathtemplate, showfull) values (@blog, @name, @title, @description, @pathtemplate, @showfull)
on conflict (blog, name) do update set title = @title2, description = @description2, pathtemplate = @pathtemplate2, showfull = @showfull2 on conflict (blog, name) do update set title = @title2, description = @description2, pathtemplate = @pathtemplate2, showfull = @showfull2
@ -119,6 +119,6 @@ func (a *goBlog) saveSection(blog string, section *configSection) error {
} }
func (a *goBlog) deleteSection(blog string, name string) error { func (a *goBlog) deleteSection(blog string, name string) error {
_, err := a.db.exec("delete from sections where blog = @blog and name = @name", sql.Named("blog", blog), sql.Named("name", name)) _, err := a.db.Exec("delete from sections where blog = @blog and name = @name", sql.Named("blog", blog), sql.Named("name", name))
return err return err
} }

View File

@ -17,7 +17,7 @@ func (db *database) shortenPath(p string) (string, error) {
return spi.(string), nil return spi.(string), nil
} }
// Insert in case it isn't shortened yet // Insert in case it isn't shortened yet
_, err := db.exec(` _, err := db.Exec(`
insert or rollback into shortpath (id, path) insert or rollback into shortpath (id, path)
values ( values (
-- next available id (reuse skipped ids due to bug) -- next available id (reuse skipped ids due to bug)
@ -31,7 +31,7 @@ func (db *database) shortenPath(p string) (string, error) {
} }
} }
// Query short path // Query short path
row, err := db.queryRow("select printf('/s/%x', id) from shortpath where path = @path", sql.Named("path", p)) row, err := db.QueryRow("select printf('/s/%x', id) from shortpath where path = @path", sql.Named("path", p))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -39,7 +39,7 @@ func Test_shortenPath(t *testing.T) {
assert.Equal(t, "/s/1", res4) assert.Equal(t, "/s/1", res4)
db.spc.Del("/a") db.spc.Del("/a")
_, _ = db.exec("delete from shortpath where id = 1") _, _ = db.Exec("delete from shortpath where id = 1")
res5, err := db.shortenPath("/c") res5, err := db.shortenPath("/c")
require.NoError(t, err) require.NoError(t, err)

View File

@ -213,7 +213,7 @@ select distinct '/x/x/' || day from alldates;
` `
func (a *goBlog) sitemapDatePaths(blog string) (paths []string, err error) { func (a *goBlog) sitemapDatePaths(blog string) (paths []string, err error) {
rows, err := a.db.query(sitemapDatePathsSql, sql.Named("blog", blog), sql.Named("status", statusPublished)) rows, err := a.db.Query(sitemapDatePathsSql, sql.Named("blog", blog), sql.Named("status", statusPublished))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -38,7 +38,7 @@ func (a *goBlog) serveTaxonomyValue(w http.ResponseWriter, r *http.Request) {
return return
} }
// Get value from DB // Get value from DB
row, err := a.db.queryRow( row, err := a.db.QueryRow(
"select value from post_parameters where parameter = @tax and urlize(value) = @taxValue limit 1", "select value from post_parameters where parameter = @tax and urlize(value) = @taxValue limit 1",
sql.Named("tax", tax.Name), sql.Named("taxValue", taxValueParam), sql.Named("tax", tax.Name), sql.Named("taxValue", taxValueParam),
) )

View File

@ -103,7 +103,7 @@ func (a *goBlog) extractMention(r *http.Request) (*mention, error) {
func (db *database) webmentionExists(m *mention) bool { func (db *database) webmentionExists(m *mention) bool {
result := 0 result := 0
row, err := db.queryRow( row, err := db.QueryRow(
` `
select exists( select exists(
select 1 select 1
@ -134,7 +134,7 @@ func (a *goBlog) createWebmention(source, target string) (err error) {
} }
func (db *database) insertWebmention(m *mention, status webmentionStatus) error { func (db *database) insertWebmention(m *mention, status webmentionStatus) error {
_, err := db.exec( _, err := db.Exec(
` `
insert into webmentions (source, target, url, created, status, title, content, author) insert into webmentions (source, target, url, created, status, title, content, author)
values (@source, lowerunescaped(@target), @url, @created, @status, @title, @content, @author) values (@source, lowerunescaped(@target), @url, @created, @status, @title, @content, @author)
@ -152,7 +152,7 @@ func (db *database) insertWebmention(m *mention, status webmentionStatus) error
} }
func (db *database) updateWebmention(m *mention, newStatus webmentionStatus) error { func (db *database) updateWebmention(m *mention, newStatus webmentionStatus) error {
_, err := db.exec(` _, err := db.Exec(`
update webmentions update webmentions
set set
source = @newsource, source = @newsource,
@ -182,12 +182,12 @@ func (db *database) updateWebmention(m *mention, newStatus webmentionStatus) err
} }
func (db *database) deleteWebmentionId(id int) error { func (db *database) deleteWebmentionId(id int) error {
_, err := db.exec("delete from webmentions where id = @id", sql.Named("id", id)) _, err := db.Exec("delete from webmentions where id = @id", sql.Named("id", id))
return err return err
} }
func (db *database) deleteWebmention(m *mention) error { func (db *database) deleteWebmention(m *mention) error {
_, err := db.exec( _, err := db.Exec(
"delete from webmentions where lowerunescaped(source) in (lowerunescaped(@source), lowerunescaped(@newsource)) and lowerunescaped(target) in (lowerunescaped(@target), lowerunescaped(@newtarget))", "delete from webmentions where lowerunescaped(source) in (lowerunescaped(@source), lowerunescaped(@newsource)) and lowerunescaped(target) in (lowerunescaped(@target), lowerunescaped(@newtarget))",
sql.Named("source", m.Source), sql.Named("source", m.Source),
sql.Named("newsource", defaultIfEmpty(m.NewSource, m.Source)), sql.Named("newsource", defaultIfEmpty(m.NewSource, m.Source)),
@ -198,7 +198,7 @@ func (db *database) deleteWebmention(m *mention) error {
} }
func (db *database) approveWebmentionId(id int) error { func (db *database) approveWebmentionId(id int) error {
_, err := db.exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id) _, err := db.Exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id)
return err return err
} }
@ -265,7 +265,7 @@ func buildWebmentionsQuery(config *webmentionsRequestConfig) (query string, args
func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) { func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) {
mentions := []*mention{} mentions := []*mention{}
query, args := buildWebmentionsQuery(config) query, args := buildWebmentionsQuery(config)
rows, err := db.query(query, args...) rows, err := db.Query(query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -310,7 +310,7 @@ func (db *database) getWebmentionsByAddress(address string) []*mention {
func (db *database) countWebmentions(config *webmentionsRequestConfig) (count int, err error) { func (db *database) countWebmentions(config *webmentionsRequestConfig) (count int, err error) {
query, params := buildWebmentionsQuery(config) query, params := buildWebmentionsQuery(config)
query = "select count(*) from (" + query + ")" query = "select count(*) from (" + query + ")"
row, err := db.queryRow(query, params...) row, err := db.QueryRow(query, params...)
if err != nil { if err != nil {
return return
} }