From 2158b156c55b27d22b1c89b66bfe3d0d413d3d5b Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 9 Aug 2022 17:25:22 +0200 Subject: [PATCH] Basic (experimental) plugin support with two plugin types (exec and middleware) --- Dockerfile | 1 + activityPub.go | 8 +- app.go | 3 + blogstats.go | 2 +- comments.go | 10 +- config.go | 8 + database.go | 20 +-- database_test.go | 8 +- go.mod | 1 + go.sum | 2 + http.go | 14 +- indieAuthServer.go | 12 +- main.go | 6 + notifications.go | 10 +- persistentCache.go | 6 +- pkgs/plugins/plugin.go | 59 +++++++ pkgs/plugins/plugins.go | 80 +++++++++ pkgs/plugins/types.go | 25 +++ pkgs/plugintypes/types.go | 51 ++++++ .../go_goblog_app-app-pkgs-plugintypes.go | 153 ++++++++++++++++++ pkgs/yaegiwrappers/wrappers.go | 11 ++ plugins.go | 52 ++++++ plugins/demo/src/demoexec/demo.go | 37 +++++ plugins/demo/src/demomiddleware/demo.go | 41 +++++ plugins_test.go | 55 +++++++ postsDb.go | 22 +-- postsDb_test.go | 4 +- queue.go | 8 +- reactions.go | 4 +- sessions.go | 10 +- settingsDb.go | 10 +- shortPath.go | 4 +- shortPath_test.go | 2 +- sitemap.go | 2 +- taxonomies.go | 2 +- webmention.go | 16 +- 36 files changed, 678 insertions(+), 81 deletions(-) create mode 100644 pkgs/plugins/plugin.go create mode 100644 pkgs/plugins/plugins.go create mode 100644 pkgs/plugins/types.go create mode 100644 pkgs/plugintypes/types.go create mode 100644 pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go create mode 100644 pkgs/yaegiwrappers/wrappers.go create mode 100644 plugins.go create mode 100644 plugins/demo/src/demoexec/demo.go create mode 100644 plugins/demo/src/demomiddleware/demo.go create mode 100644 plugins_test.go diff --git a/Dockerfile b/Dockerfile index fe1ff86..d12cdfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ ADD leaflet/ /app/leaflet/ ADD hlsjs/ /app/hlsjs/ ADD dbmigrations/ /app/dbmigrations/ ADD strings/ /app/strings/ +ADD plugins/ /app/plugins/ FROM buildbase as build diff --git a/activityPub.go b/activityPub.go index 040a96a..388cc4c 100644 --- a/activityPub.go +++ b/activityPub.go @@ -291,7 +291,7 @@ func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, 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 { 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 { - _, 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 } 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 } 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 } diff --git a/app.go b/app.go index 67d6597..5726cc3 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ import ( rotatelogs "github.com/lestrrat-go/file-rotatelogs" "github.com/yuin/goldmark" "go.goblog.app/app/pkgs/minify" + "go.goblog.app/app/pkgs/plugins" "golang.org/x/crypto/acme/autocert" "golang.org/x/sync/singleflight" "tailscale.com/tsnet" @@ -75,6 +76,8 @@ type goBlog struct { mediaStorage mediaStorage // Minify min minify.Minifier + // Plugins + pluginHost *plugins.PluginHost // Reactions reactionsInit sync.Once reactionsCache *ristretto.Cache diff --git a/blogstats.go b/blogstats.go index 3fef416..1e3e1bc 100644 --- a/blogstats.go +++ b/blogstats.go @@ -154,7 +154,7 @@ func (db *database) getBlogStats(blog string) (data *blogStatsData, err error) { Months: map[string][]blogStatsRow{}, } // 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 { return nil, err } diff --git a/comments.go b/comments.go index d6e7e39..5c7d838 100644 --- a/comments.go +++ b/comments.go @@ -28,7 +28,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusBadRequest) 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 { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return @@ -63,7 +63,7 @@ func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) { name := defaultIfEmpty(cleanHTMLText(r.FormValue("name")), "Anonymous") website := cleanHTMLText(r.FormValue("website")) // 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 { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return @@ -116,7 +116,7 @@ func buildCommentsQuery(config *commentsRequestConfig) (query string, args []any func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) { comments := []*comment{} query, args := buildCommentsQuery(config) - rows, err := db.query(query, args...) + rows, err := db.Query(query, args...) if err != nil { 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) { query, params := buildCommentsQuery(config) query = "select count(*) from (" + query + ")" - row, err := db.queryRow(query, params...) + row, err := db.QueryRow(query, params...) if err != nil { return } @@ -143,6 +143,6 @@ func (db *database) countComments(config *commentsRequestConfig) (count int, err } 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 } diff --git a/config.go b/config.go index 31ca80d..4904e5d 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ type config struct { Blogs map[string]*configBlog `mapstructure:"blogs"` User *configUser `mapstructure:"user"` Hooks *configHooks `mapstructure:"hooks"` + Plugins []*configPlugin `mapstructure:"plugins"` Micropub *configMicropub `mapstructure:"micropub"` PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"` ActivityPub *configActivityPub `mapstructure:"activityPub"` @@ -322,6 +323,13 @@ type configPprof struct { 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 { // Use viper to load the config file v := viper.New() diff --git a/database.go b/database.go index 709c2ab..0ecfbc1 100644 --- a/database.go +++ b/database.go @@ -211,11 +211,11 @@ func (db *database) prepare(query string, args ...any) (*sql.Stmt, []any, error) const dbNoCache = "nocache" -func (db *database) exec(query string, args ...any) (sql.Result, error) { - return db.execContext(context.Background(), query, args...) +func (db *database) Exec(query string, args ...any) (sql.Result, error) { + 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 { 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...) } -func (db *database) query(query string, args ...any) (*sql.Rows, error) { - return db.queryContext(context.Background(), query, args...) +func (db *database) Query(query string, args ...any) (*sql.Rows, error) { + 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 { return nil, errors.New("database not initialized") } @@ -257,11 +257,11 @@ func (db *database) queryContext(c context.Context, query string, args ...any) ( return } -func (db *database) queryRow(query string, args ...any) (*sql.Row, error) { - return db.queryRowContext(context.Background(), query, args...) +func (db *database) QueryRow(query string, args ...any) (*sql.Row, error) { + 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 { return nil, errors.New("database not initialized") } @@ -283,5 +283,5 @@ func (db *database) queryRowContext(c context.Context, query string, args ...any // Other things func (d *database) rebuildFTSIndex() { - _, _ = d.exec("insert into posts_fts(posts_fts) values ('rebuild')") + _, _ = d.Exec("insert into posts_fts(posts_fts) values ('rebuild')") } diff --git a/database_test.go b/database_test.go index 126acc3..ba7a3d3 100644 --- a/database_test.go +++ b/database_test.go @@ -15,17 +15,17 @@ func Test_database(t *testing.T) { t.Fatalf("Error: %v", err) } - _, err = db.exec("create table test(test text);") + _, err = db.Exec("create table test(test text);") if err != nil { 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 { 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 { t.Fatalf("Error: %v", err) } @@ -38,7 +38,7 @@ func Test_database(t *testing.T) { 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 { t.Fatalf("Error: %v", err) } diff --git a/go.mod b/go.mod index cf42c6a..4ad011f 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( // master github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec 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/yuin/goldmark v1.4.13 // master diff --git a/go.sum b/go.sum index c000a80..0c86d20 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4= github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= diff --git a/http.go b/http.go index 4bbdaf2..02582d0 100644 --- a/http.go +++ b/http.go @@ -7,6 +7,7 @@ import ( "log" "net" "net/http" + "sort" "strconv" "time" @@ -17,6 +18,7 @@ import ( "github.com/klauspost/compress/flate" "go.goblog.app/app/pkgs/httpcompress" "go.goblog.app/app/pkgs/maprouter" + "go.goblog.app/app/pkgs/plugintypes" "golang.org/x/net/context" ) @@ -43,6 +45,16 @@ func (a *goBlog) startServer() (err error) { if a.httpsConfigured(false) { 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) { a.d.ServeHTTP(w, r) }) @@ -245,7 +257,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc { } // Check if post or alias path := r.URL.Path - row, err := a.db.queryRow(` + row, err := a.db.QueryRow(` -- normal posts select 'post', status, 200 from posts where path = @path union all diff --git a/indieAuthServer.go b/indieAuthServer.go index 0a7d6d4..b7dbccc 100644 --- a/indieAuthServer.go +++ b/indieAuthServer.go @@ -182,7 +182,7 @@ func (db *database) indieAuthSaveAuthRequest(data *indieauth.AuthenticationReque // Generate a code to identify the request code := uuid.NewString() // Save the request - _, err := db.exec( + _, err := db.Exec( "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, ) @@ -194,7 +194,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen // code valid for 10 minutes maxAge := time.Now().UTC().Add(-10 * time.Minute).Unix() // 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 { return nil, err } @@ -210,7 +210,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen data.Scopes = strings.Split(scope, " ") } // 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 } @@ -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) { token = strings.ReplaceAll(token, "Bearer ", "") 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 { return nil, err } @@ -271,13 +271,13 @@ func (db *database) indieAuthVerifyToken(token string) (data *indieauth.Authenti // Save a new token to the database func (db *database) indieAuthSaveToken(data *indieauth.AuthenticationRequest) (string, error) { 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 } // Revoke and delete the token from the database func (db *database) indieAuthRevokeToken(token string) { if token != "" { - _, _ = db.exec("delete from indieauthtoken where token=?", token) + _, _ = db.Exec("delete from indieauthtoken where token=?", token) } } diff --git a/main.go b/main.go index 1fbfd04..0d8383e 100644 --- a/main.go +++ b/main.go @@ -67,6 +67,12 @@ func main() { return } + // Initialize plugins + if err = app.initPlugins(); err != nil { + app.logErrAndQuit("Failed to init plugins:", err.Error()) + return + } + // Healthcheck tool if len(os.Args) >= 2 && os.Args[1] == "healthcheck" { // Connect to public address + "/ping" and exit with 0 when successful diff --git a/notifications.go b/notifications.go index 3961195..60c89a0 100644 --- a/notifications.go +++ b/notifications.go @@ -41,19 +41,19 @@ func (a *goBlog) sendNotification(text string) { } 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 nil } 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 } func (db *database) deleteAllNotifications() error { - _, err := db.exec("delete from notifications") + _, err := db.Exec("delete from notifications") return err } @@ -75,7 +75,7 @@ func buildNotificationsQuery(config *notificationsRequestConfig) (query string, func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) { notifications := []*notification{} query, args := buildNotificationsQuery(config) - rows, err := db.query(query, args...) + rows, err := db.Query(query, args...) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (db *database) getNotifications(config *notificationsRequestConfig) ([]*not func (db *database) countNotifications(config *notificationsRequestConfig) (count int, err error) { query, params := buildNotificationsQuery(config) query = "select count(*) from (" + query + ")" - row, err := db.queryRow(query, params...) + row, err := db.QueryRow(query, params...) if err != nil { return } diff --git a/persistentCache.go b/persistentCache.go index b2d8ff7..5b7e75a 100644 --- a/persistentCache.go +++ b/persistentCache.go @@ -14,7 +14,7 @@ func (db *database) cachePersistentlyContext(ctx context.Context, key string, da if db == 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 } @@ -27,7 +27,7 @@ func (db *database) retrievePersistentCacheContext(c context.Context, key string return nil, errors.New("database is nil") } 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 } else { 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 { - _, 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 } diff --git a/pkgs/plugins/plugin.go b/pkgs/plugins/plugin.go new file mode 100644 index 0000000..5e13993 --- /dev/null +++ b/pkgs/plugins/plugin.go @@ -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 +} diff --git a/pkgs/plugins/plugins.go b/pkgs/plugins/plugins.go new file mode 100644 index 0000000..89b7660 --- /dev/null +++ b/pkgs/plugins/plugins.go @@ -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 +} diff --git a/pkgs/plugins/types.go b/pkgs/plugins/types.go new file mode 100644 index 0000000..f1b310f --- /dev/null +++ b/pkgs/plugins/types.go @@ -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") +) diff --git a/pkgs/plugintypes/types.go b/pkgs/plugintypes/types.go new file mode 100644 index 0000000..0e51fea --- /dev/null +++ b/pkgs/plugintypes/types.go @@ -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 +} diff --git a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go new file mode 100644 index 0000000..07ab411 --- /dev/null +++ b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go @@ -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) +} diff --git a/pkgs/yaegiwrappers/wrappers.go b/pkgs/yaegiwrappers/wrappers.go new file mode 100644 index 0000000..b6d2c7d --- /dev/null +++ b/pkgs/yaegiwrappers/wrappers.go @@ -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 diff --git a/plugins.go b/plugins.go new file mode 100644 index 0000000..3ff5cb9 --- /dev/null +++ b/plugins.go @@ -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{} diff --git a/plugins/demo/src/demoexec/demo.go b/plugins/demo/src/demoexec/demo.go new file mode 100644 index 0000000..1dc0c6c --- /dev/null +++ b/plugins/demo/src/demoexec/demo.go @@ -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() +} diff --git a/plugins/demo/src/demomiddleware/demo.go b/plugins/demo/src/demomiddleware/demo.go new file mode 100644 index 0000000..4332fc0 --- /dev/null +++ b/plugins/demo/src/demomiddleware/demo.go @@ -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) + }) +} diff --git a/plugins_test.go b/plugins_test.go new file mode 100644 index 0000000..d945ad6 --- /dev/null +++ b/plugins_test.go @@ -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()) + } + +} diff --git a/postsDb.go b/postsDb.go index d1d6fc6..98a69f5 100644 --- a/postsDb.go +++ b/postsDb.go @@ -203,7 +203,7 @@ func (db *database) savePost(p *post, o *postCreationOptions) error { // Commit transaction sqlBuilder.WriteString("commit;") // 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") { 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 if strings.HasSuffix(string(p.Status), statusDeletedSuffix) { // 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;`, dbNoCache, p.Path, p.Path, p.Path, ); err != nil { @@ -250,7 +250,7 @@ func (a *goBlog) deletePost(path string) error { } p.Parameters["deleted"] = []string{deletedTime} // 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;`, dbNoCache, p.Status, p.Path, p.Path, p.Path, deletedTime, ); err != nil { @@ -283,7 +283,7 @@ func (a *goBlog) undeletePost(path string) error { // Remove parameter p.Parameters["deleted"] = nil // 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;`, dbNoCache, p.Status, p.Path, p.Path, ); err != nil { @@ -320,7 +320,7 @@ func (db *database) replacePostParam(path, param string, values []string) error // Commit transaction sqlBuilder.WriteString("commit;") // Execute - _, err := db.exec(sqlBuilder.String(), sqlArgs...) + _, err := db.Exec(sqlBuilder.String(), sqlArgs...) bufferpool.Put(sqlBuilder) if err != nil { return err @@ -514,7 +514,7 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err // Order queryBuilder.WriteString(" order by id") // Query - rows, err := d.query(queryBuilder.String(), sqlArgs...) + rows, err := d.Query(queryBuilder.String(), sqlArgs...) if err != nil { 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) { // Query posts 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 { 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) { 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 { 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) { sections := lo.Keys(a.cfg.Blogs[blog].Sections) 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 { 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) { // 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 { return nil, err } @@ -663,7 +663,7 @@ func (db *database) usesOfMediaFile(names ...string) (counts []int, err error) { nameValues.WriteByte(')') 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) if err != nil { return nil, err diff --git a/postsDb_test.go b/postsDb_test.go index 6a2476e..2b2695c 100644 --- a/postsDb_test.go +++ b/postsDb_test.go @@ -402,7 +402,7 @@ func Test_postDeletesParams(t *testing.T) { err = app.deletePost("/test/abc") 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) var count int @@ -415,7 +415,7 @@ func Test_postDeletesParams(t *testing.T) { err = app.deletePost("/test/abc") 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) err = row.Scan(&count) diff --git a/queue.go b/queue.go index 2d1e8a2..4c78b4e 100644 --- a/queue.go +++ b/queue.go @@ -22,7 +22,7 @@ func (a *goBlog) enqueue(name string, content []byte, schedule time.Time) error if len(content) == 0 { return errors.New("empty content") } - _, err := a.db.exec( + _, err := a.db.Exec( "insert into queue (name, content, schedule) values (@name, @content, @schedule)", sql.Named("name", name), 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 { - _, err := a.db.exec( + _, err := a.db.Exec( "update queue set schedule = @schedule, content = @content where id = @id", sql.Named("schedule", qi.schedule.Add(dur).UTC().Format(time.RFC3339Nano)), 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 { - _, 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 } func (a *goBlog) peekQueue(ctx context.Context, name string) (*queueItem, error) { - row, err := a.db.queryRowContext( + row, err := a.db.QueryRowContext( ctx, "select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1", sql.Named("name", name), diff --git a/reactions.go b/reactions.go index a04fee4..40c5575 100644 --- a/reactions.go +++ b/reactions.go @@ -77,7 +77,7 @@ func (a *goBlog) saveReaction(reaction, path string) error { defer a.reactionsSfg.Forget(path) defer a.reactionsCache.Del(path) // 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 } @@ -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=?)") sqlArgs = append(sqlArgs, reactionsPostParam, "false") // Execute query - rows, err := a.db.query(sqlBuf.String(), sqlArgs...) + rows, err := a.db.Query(sqlBuf.String(), sqlArgs...) if err != nil { return nil, err } diff --git a/sessions.go b/sessions.go index 2a89da0..6f6a4c7 100644 --- a/sessions.go +++ b/sessions.go @@ -23,7 +23,7 @@ const ( func (a *goBlog) initSessions() { deleteExpiredSessions := func() { - if _, err := a.db.exec( + if _, err := a.db.Exec( "delete from sessions where expires < @now", sql.Named("now", utcNowString()), ); err != nil { @@ -103,14 +103,14 @@ func (s *dbSessionStore) Delete(_ *http.Request, w http.ResponseWriter, session for k := range session.Values { 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 nil } 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", sql.Named("id", session.ID), sql.Named("now", utcNowString()), @@ -142,7 +142,7 @@ func (s *dbSessionStore) insert(session *sessions.Session) (err error) { session.ID = session.Name() + "-" + uuid.NewString() created, modified := utcNowString(), utcNowString() 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)", sql.Named("id", session.ID), 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 { return err } - _, err = s.db.exec( + _, err = s.db.Exec( "update sessions set data = @data, modified = @modified where id = @id", sql.Named("data", encoded.Bytes()), sql.Named("modified", utcNowString()), diff --git a/settingsDb.go b/settingsDb.go index 0a35822..1e6c55b 100644 --- a/settingsDb.go +++ b/settingsDb.go @@ -18,7 +18,7 @@ const ( ) 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 { return "", err @@ -45,7 +45,7 @@ func (a *goBlog) getBooleanSettingValue(name string, defaultValue bool) (bool, e } 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", sql.Named("name", name), sql.Named("value", value), @@ -70,7 +70,7 @@ func (a *goBlog) loadSections() 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 { return nil, err } @@ -99,7 +99,7 @@ func (a *goBlog) saveAllSections() 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) 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 { - _, 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 } diff --git a/shortPath.go b/shortPath.go index 0949a9e..3d27cc3 100644 --- a/shortPath.go +++ b/shortPath.go @@ -17,7 +17,7 @@ func (db *database) shortenPath(p string) (string, error) { return spi.(string), nil } // Insert in case it isn't shortened yet - _, err := db.exec(` + _, err := db.Exec(` insert or rollback into shortpath (id, path) values ( -- next available id (reuse skipped ids due to bug) @@ -31,7 +31,7 @@ func (db *database) shortenPath(p string) (string, error) { } } // 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 { return nil, err } diff --git a/shortPath_test.go b/shortPath_test.go index 73829ba..4445236 100644 --- a/shortPath_test.go +++ b/shortPath_test.go @@ -39,7 +39,7 @@ func Test_shortenPath(t *testing.T) { assert.Equal(t, "/s/1", res4) 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") require.NoError(t, err) diff --git a/sitemap.go b/sitemap.go index 283027f..e333138 100644 --- a/sitemap.go +++ b/sitemap.go @@ -213,7 +213,7 @@ select distinct '/x/x/' || day from alldates; ` 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 { return nil, err } diff --git a/taxonomies.go b/taxonomies.go index 5204b86..c53c425 100644 --- a/taxonomies.go +++ b/taxonomies.go @@ -38,7 +38,7 @@ func (a *goBlog) serveTaxonomyValue(w http.ResponseWriter, r *http.Request) { return } // 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", sql.Named("tax", tax.Name), sql.Named("taxValue", taxValueParam), ) diff --git a/webmention.go b/webmention.go index 324dc8b..e16a5f4 100644 --- a/webmention.go +++ b/webmention.go @@ -103,7 +103,7 @@ func (a *goBlog) extractMention(r *http.Request) (*mention, error) { func (db *database) webmentionExists(m *mention) bool { result := 0 - row, err := db.queryRow( + row, err := db.QueryRow( ` select exists( select 1 @@ -134,7 +134,7 @@ func (a *goBlog) createWebmention(source, target string) (err 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) 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 { - _, err := db.exec(` + _, err := db.Exec(` update webmentions set source = @newsource, @@ -182,12 +182,12 @@ func (db *database) updateWebmention(m *mention, newStatus webmentionStatus) err } 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 } 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))", sql.Named("source", 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 { - _, err := db.exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id) + _, err := db.Exec("update webmentions set status = ? where id = ?", webmentionStatusApproved, id) return err } @@ -265,7 +265,7 @@ func buildWebmentionsQuery(config *webmentionsRequestConfig) (query string, args func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) { mentions := []*mention{} query, args := buildWebmentionsQuery(config) - rows, err := db.query(query, args...) + rows, err := db.Query(query, args...) if err != nil { return nil, err } @@ -310,7 +310,7 @@ func (db *database) getWebmentionsByAddress(address string) []*mention { func (db *database) countWebmentions(config *webmentionsRequestConfig) (count int, err error) { query, params := buildWebmentionsQuery(config) query = "select count(*) from (" + query + ")" - row, err := db.queryRow(query, params...) + row, err := db.QueryRow(query, params...) if err != nil { return }