From 609780db791f71dcbc4b3ef7775ef24fc76ea30b Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Fri, 24 Mar 2023 21:25:20 +0100 Subject: [PATCH] Add AI-generated-summary plugin (aitldr), many new plugin hooks and update dependencies --- database.go | 2 +- docs/plugins.md | 31 ++- go.mod | 16 +- go.sum | 32 +-- hooks.go | 10 + markdown.go | 18 +- markdown_test.go | 3 +- pkgs/plugintypes/app.go | 22 +- pkgs/plugintypes/plugins.go | 26 +++ .../go_goblog_app-app-pkgs-plugintypes.go | 162 +++++++++++---- plugins.go | 69 +++++-- plugins/aitldr/src/aitldr/aitldr.go | 192 ++++++++++++++++++ postsFuncs.go | 2 +- ui.go | 10 +- 14 files changed, 492 insertions(+), 103 deletions(-) create mode 100644 plugins/aitldr/src/aitldr/aitldr.go diff --git a/database.go b/database.go index 0ecfbc1..d991d0d 100644 --- a/database.go +++ b/database.go @@ -70,7 +70,7 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) { ConnectHook: func(c *sqlite.SQLiteConn) error { // Register functions for n, f := range map[string]any{ - "mdtext": a.renderText, + "mdtext": a.renderTextSafe, "tolocal": toLocalSafe, "toutc": toUTCSafe, "wordcount": wordCount, diff --git a/docs/plugins.md b/docs/plugins.md index f3f8271..2655dfc 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -27,17 +27,9 @@ You need to specify the path to the plugin (remember to mount the path to your G ## Types of plugins -- `SetApp` (Access more GoBlog functionalities like the database) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetApp] -- `SetConfig` (Access the configuration provided for the plugin) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#SetConfig] +There are different plugin types for different functionalities a plugin wants to support. A plugin can implement multiple plugin types. You can find more information about the plugin types [in the Go documentation](https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes). -- `Exec` (Command that is executed in a Go routine when starting GoBlog) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#Exec] -- `Middleware` (HTTP middleware to intercept or modify HTTP requests) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/]plugintypes#Middleware -- `UI` (Modify rendered HTML) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI] -- `UI2` (Modify rendered HTML using a goquery document which improves performance and avoids multiple HTML parsing and rendering when using multiple plugins) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UI2] -- `UISummary` (like UI2 for only the post summary on indexes) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UISummary] -- `UIFooter` (like UI2 for only the post summary on indexes) - see [https://pkg.go.dev/go.goblog.app/app/pkgs/plugintypes#UIFooter] - -More types will be added later. Any plugin can implement multiple types, see the demo plugin as example. +You can take a look at the demo plugin, which implements many of the plugin types. ## Plugin implementation @@ -130,4 +122,23 @@ config: link: https://example.org/ # Link to the webring prev: https://example.com/ # Link to previous webring site next: https://example.net/ # Link to next webring site +``` + +### AI generated summary (Path `embedded:aitldr`, Import `aitldr`) + +A plugin that uses the ChatGPT API to generated a short one-sentence summary for the blog post (after creating or updating it). To prevent it from generating a summary for a post, add the following post parameter: + +```yaml +noaitldr: "true" +``` + +#### Config + +```yaml +config: + # Required + apikey: YOUR_OPEN_AI_API_KEY + # Optional: + default: # Name of the blog + title: "Custom title for the summary box:" ``` \ No newline at end of file diff --git a/go.mod b/go.mod index b168d57..5957781 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4 git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b github.com/PuerkitoBio/goquery v1.8.1 - github.com/alecthomas/chroma/v2 v2.5.0 + github.com/alecthomas/chroma/v2 v2.7.0 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b github.com/carlmjohnson/requests v0.23.2 @@ -20,8 +20,8 @@ require ( github.com/dmulholl/mp3lib v1.0.0 github.com/elnormous/contenttype v1.0.4 github.com/emersion/go-smtp v0.16.0 - github.com/go-ap/activitypub v0.0.0-20230317030458-892480c77bb6 - github.com/go-ap/client v0.0.0-20230317030549-9bf6268ae536 + github.com/go-ap/activitypub v0.0.0-20230323123728-77b329013634 + github.com/go-ap/client v0.0.0-20230323123805-a1114dc5ba4f github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-chi/chi/v5 v5.0.8 github.com/go-fed/httpsig v1.1.0 @@ -46,7 +46,7 @@ require ( github.com/paulmach/go.geojson v1.4.0 github.com/posener/wstest v1.2.0 github.com/pquerna/otp v1.4.0 - github.com/samber/lo v1.37.0 + github.com/samber/lo v1.38.1 github.com/schollz/sqlite3dump v1.3.1 github.com/snabb/sitemap v1.0.4 github.com/sourcegraph/conc v0.3.0 @@ -57,7 +57,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.15.0 + github.com/traefik/yaegi v0.15.1 github.com/vcraescu/go-paginator/v2 v2.0.0 github.com/xhit/go-simple-mail/v2 v2.13.0 github.com/yuin/goldmark v1.5.4 @@ -88,7 +88,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-gonic/gin v1.7.7 // indirect github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea // indirect - github.com/golang/glog v1.1.0 // indirect + github.com/golang/glog v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect @@ -100,7 +100,7 @@ require ( github.com/lestrrat-go/strftime v1.0.6 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcdole/goxpp v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -123,7 +123,7 @@ require ( github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/valyala/fastjson v1.6.4 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/image v0.6.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/sys v0.6.0 // indirect diff --git a/go.sum b/go.sum index 0fc8200..31bcf46 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= -github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk= -github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI= +github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= @@ -128,10 +128,10 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-ap/activitypub v0.0.0-20230317030458-892480c77bb6 h1:I6l80Wy1UK0KZvK0xLho5FkiHp84f77RjgYOPThQ2I0= -github.com/go-ap/activitypub v0.0.0-20230317030458-892480c77bb6/go.mod h1:1oVD0h0aPT3OEE1ZoSUoym/UGKzxe+e0y8K2AkQ1Hqs= -github.com/go-ap/client v0.0.0-20230317030549-9bf6268ae536 h1:+7v15882cEa9nRbjgk3D17qRcJgevDYvtcMUs5uvOkU= -github.com/go-ap/client v0.0.0-20230317030549-9bf6268ae536/go.mod h1:qgiGGIKang+sVcINaB3nN5uw4wZtaHHqp/qqtZ2HI2Y= +github.com/go-ap/activitypub v0.0.0-20230323123728-77b329013634 h1:zD/tSS22PgVrJTJatsefCvug/RjabVy6JmshKYzOQok= +github.com/go-ap/activitypub v0.0.0-20230323123728-77b329013634/go.mod h1:qw0WNf+PTG69Xu6mVqUluDuKl1VwVYdgntOZQFBZQ48= +github.com/go-ap/client v0.0.0-20230323123805-a1114dc5ba4f h1:ZOQfbSNAsQOLa/c3/mRCOMSSXjOnAyCMdiJ9myJiYBk= +github.com/go-ap/client v0.0.0-20230323123805-a1114dc5ba4f/go.mod h1:ChxiPiPaRRYpsEFAX3KGAeE9P9upancoJTRSaaudpJE= github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea h1:ywGtLGVjJjMrq4mu35Qmu+NtlhlTk/gTayE6Bb4tQZk= github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea/go.mod h1:SaTNjEEkp0q+w3pUS1ccyEL/lUrHteORlDq/e21mCc8= github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw= @@ -163,8 +163,8 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -310,8 +310,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -354,8 +354,8 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= -github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= -github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA= github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= @@ -412,8 +412,8 @@ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= -github.com/traefik/yaegi v0.15.0 h1:ScDDfQXTT75rKvcsMcP84rOxHsZ8b6NiQJyGocGDB7g= -github.com/traefik/yaegi v0.15.0/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= +github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4= +github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -465,8 +465,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/hooks.go b/hooks.go index d6e4063..e1f1c51 100644 --- a/hooks.go +++ b/hooks.go @@ -7,6 +7,7 @@ import ( "time" "go.goblog.app/app/pkgs/bufferpool" + "go.goblog.app/app/pkgs/plugintypes" ) func (a *goBlog) preStartHooks() { @@ -35,6 +36,9 @@ func (a *goBlog) postPostHooks(p *post) { for _, f := range a.pPostHooks { go f(p) } + for _, plugin := range a.getPlugins(pluginPostCreatedHookType) { + go plugin.(plugintypes.PostCreatedHook).PostCreated(p) + } } func (a *goBlog) postUpdateHooks(p *post) { @@ -52,6 +56,9 @@ func (a *goBlog) postUpdateHooks(p *post) { for _, f := range a.pUpdateHooks { go f(p) } + for _, plugin := range a.getPlugins(pluginPostUpdatedHookType) { + go plugin.(plugintypes.PostUpdatedHook).PostUpdated(p) + } } func (a *goBlog) postDeleteHooks(p *post) { @@ -68,6 +75,9 @@ func (a *goBlog) postDeleteHooks(p *post) { for _, f := range a.pDeleteHooks { go f(p) } + for _, plugin := range a.getPlugins(pluginPostDeletedHookType) { + go plugin.(plugintypes.PostDeletedHook).PostDeleted(p) + } } func (a *goBlog) postUndeleteHooks(p *post) { diff --git a/markdown.go b/markdown.go index 3cbcf0a..5ce6221 100644 --- a/markdown.go +++ b/markdown.go @@ -56,10 +56,7 @@ func (a *goBlog) initMarkdown() { goldmark.WithParser( // Override, no need for special Markdown parsers parser.NewParser( - parser.WithBlockParsers( - util.Prioritized(parser.NewParagraphParser(), 1000)), - parser.WithInlineParsers(), - parser.WithParagraphTransformers(), + parser.WithBlockParsers(util.Prioritized(parser.NewParagraphParser(), 1000)), ), ), goldmark.WithExtensions( @@ -78,9 +75,9 @@ func (a *goBlog) renderMarkdownToWriter(w io.Writer, source string, absoluteLink return err } -func (a *goBlog) renderText(s string) string { +func (a *goBlog) renderText(s string) (string, error) { if s == "" { - return "" + return "", nil } pr, pw := io.Pipe() go func() { @@ -89,9 +86,14 @@ func (a *goBlog) renderText(s string) string { text, err := htmlTextFromReader(pr) _ = pr.CloseWithError(err) if err != nil { - return "" + return "", nil } - return text + return text, nil +} + +func (a *goBlog) renderTextSafe(s string) string { + r, _ := a.renderText(s) + return r } func (a *goBlog) renderMdTitle(s string) string { diff --git a/markdown_test.go b/markdown_test.go index 3846c2b..91ae261 100644 --- a/markdown_test.go +++ b/markdown_test.go @@ -79,8 +79,9 @@ func Test_markdown(t *testing.T) { // Text - renderedText := app.renderText("**This** *is* [text](/)") + renderedText, err := app.renderText("**This** *is* [text](/)") assert.Equal(t, "This is text", renderedText) + assert.NoError(t, err) // Title diff --git a/pkgs/plugintypes/app.go b/pkgs/plugintypes/app.go index 4117fa0..1a785a8 100644 --- a/pkgs/plugintypes/app.go +++ b/pkgs/plugintypes/app.go @@ -13,6 +13,8 @@ type App interface { GetDatabase() Database // Get a post from the database or an error when there is no post for the given path GetPost(path string) (Post, error) + // Get a blog and a bool whether it exists + GetBlog(name string) (Blog, bool) // Purge the rendering cache PurgeCache() // Get the HTTP client used by GoBlog @@ -21,6 +23,10 @@ type App interface { CompileAsset(filename string, reader io.Reader) error // Get the asset path with the filename used when compiling the assert AssetPath(filename string) string + // Set parameter values for a post path + SetPostParameter(path string, parameter string, values []string) error + // Render markdown as text (without HTML) + RenderMarkdownAsText(markdown string) (text string, err error) } // Database is used to provide access to GoBlog's database. @@ -33,14 +39,18 @@ type Database interface { QueryRowContext(context.Context, string, ...any) (*sql.Row, error) } -// Post +// Post contains methods to access the post's data. type Post interface { // Get the post path GetPath() string + // Get the blog name + GetBlog() string // Get a string array map with all the post's parameters GetParameters() map[string][]string // Get a single parameter array (a parameter can have multiple values) GetParameter(parameter string) []string + // Get the first value of a post parameter + GetFirstParameterValue(parameter string) string // Get the post section name GetSection() string // Get the published date string @@ -49,9 +59,17 @@ type Post interface { GetUpdated() string // Get the post content (markdown) GetContent() string + // Get the post title + GetTitle() string } -// RenderContext +// Blog contains methods to access the blog's configuration. +type Blog interface { + // Get the language + GetLanguage() string +} + +// RenderContext gives some context of the current rendering. type RenderContext interface { // Get the path of the request GetPath() string diff --git a/pkgs/plugintypes/plugins.go b/pkgs/plugintypes/plugins.go index 0aaa73e..19122bd 100644 --- a/pkgs/plugintypes/plugins.go +++ b/pkgs/plugintypes/plugins.go @@ -52,9 +52,35 @@ type UISummary interface { RenderSummaryForPost(renderContext RenderContext, post Post, doc *goquery.Document) } +// UIPost plugins get called when rendering the h-entry for a post. But only on the HTML frontend, not ActivityPub or feeds. +type UIPost interface { + // The renderContext provides information such as the path of the request or the blog name. + // The post contains information about the post for which to render the summary. + // The document can be used to add or modify the default HTML. But it only contains the HTML for the post, not for the whole page. + RenderPost(renderContext RenderContext, post Post, doc *goquery.Document) +} + // UIFooter plugins get called when rendering the footer on each HTML page. type UIFooter interface { // The renderContext provides information such as the path of the request or the blog name. // The document can be used to add or modify the default HTML. RenderFooter(renderContext RenderContext, doc *goquery.Document) } + +// PostCreatedHook plugins get called after a post is created. +type PostCreatedHook interface { + // Handle the post. + PostCreated(post Post) +} + +// PostUpdatedHook plugins get called after a post is updated. +type PostUpdatedHook interface { + // Handle the post. + PostUpdated(post Post) +} + +// PostUpdatedHook plugins get called after a post is deleted. +type PostDeletedHook interface { + // Handle the post. + PostDeleted(post Post) +} diff --git a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go index 3cc5324..b620115 100644 --- a/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go +++ b/pkgs/yaegiwrappers/go_goblog_app-app-pkgs-plugintypes.go @@ -37,44 +37,57 @@ import ( 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)), - "Post": reflect.ValueOf((*plugintypes.Post)(nil)), - "RenderContext": reflect.ValueOf((*plugintypes.RenderContext)(nil)), - "SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)), - "SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)), - "UI": reflect.ValueOf((*plugintypes.UI)(nil)), - "UI2": reflect.ValueOf((*plugintypes.UI2)(nil)), - "UIFooter": reflect.ValueOf((*plugintypes.UIFooter)(nil)), - "UISummary": reflect.ValueOf((*plugintypes.UISummary)(nil)), + "App": reflect.ValueOf((*plugintypes.App)(nil)), + "Blog": reflect.ValueOf((*plugintypes.Blog)(nil)), + "Database": reflect.ValueOf((*plugintypes.Database)(nil)), + "Exec": reflect.ValueOf((*plugintypes.Exec)(nil)), + "Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)), + "Post": reflect.ValueOf((*plugintypes.Post)(nil)), + "PostCreatedHook": reflect.ValueOf((*plugintypes.PostCreatedHook)(nil)), + "PostDeletedHook": reflect.ValueOf((*plugintypes.PostDeletedHook)(nil)), + "PostUpdatedHook": reflect.ValueOf((*plugintypes.PostUpdatedHook)(nil)), + "RenderContext": reflect.ValueOf((*plugintypes.RenderContext)(nil)), + "SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)), + "SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)), + "UI": reflect.ValueOf((*plugintypes.UI)(nil)), + "UI2": reflect.ValueOf((*plugintypes.UI2)(nil)), + "UIFooter": reflect.ValueOf((*plugintypes.UIFooter)(nil)), + "UIPost": reflect.ValueOf((*plugintypes.UIPost)(nil)), + "UISummary": reflect.ValueOf((*plugintypes.UISummary)(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)), - "_Post": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Post)(nil)), - "_RenderContext": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_RenderContext)(nil)), - "_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)), - "_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)), - "_UI": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI)(nil)), - "_UI2": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI2)(nil)), - "_UIFooter": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UIFooter)(nil)), - "_UISummary": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UISummary)(nil)), + "_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)), + "_Blog": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Blog)(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)), + "_Post": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Post)(nil)), + "_PostCreatedHook": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_PostCreatedHook)(nil)), + "_PostDeletedHook": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_PostDeletedHook)(nil)), + "_PostUpdatedHook": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_PostUpdatedHook)(nil)), + "_RenderContext": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_RenderContext)(nil)), + "_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)), + "_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)), + "_UI": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI)(nil)), + "_UI2": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UI2)(nil)), + "_UIFooter": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UIFooter)(nil)), + "_UIPost": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UIPost)(nil)), + "_UISummary": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_UISummary)(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{} - WAssetPath func(filename string) string - WCompileAsset func(filename string, reader io.Reader) error - WGetDatabase func() plugintypes.Database - WGetHTTPClient func() *http.Client - WGetPost func(path string) (plugintypes.Post, error) - WPurgeCache func() + IValue interface{} + WAssetPath func(filename string) string + WCompileAsset func(filename string, reader io.Reader) error + WGetBlog func(name string) (plugintypes.Blog, bool) + WGetDatabase func() plugintypes.Database + WGetHTTPClient func() *http.Client + WGetPost func(path string) (plugintypes.Post, error) + WPurgeCache func() + WRenderMarkdownAsText func(markdown string) (text string, err error) + WSetPostParameter func(path string, parameter string, values []string) error } func (W _go_goblog_app_app_pkgs_plugintypes_App) AssetPath(filename string) string { @@ -83,6 +96,9 @@ func (W _go_goblog_app_app_pkgs_plugintypes_App) AssetPath(filename string) stri func (W _go_goblog_app_app_pkgs_plugintypes_App) CompileAsset(filename string, reader io.Reader) error { return W.WCompileAsset(filename, reader) } +func (W _go_goblog_app_app_pkgs_plugintypes_App) GetBlog(name string) (plugintypes.Blog, bool) { + return W.WGetBlog(name) +} func (W _go_goblog_app_app_pkgs_plugintypes_App) GetDatabase() plugintypes.Database { return W.WGetDatabase() } @@ -95,6 +111,22 @@ func (W _go_goblog_app_app_pkgs_plugintypes_App) GetPost(path string) (plugintyp func (W _go_goblog_app_app_pkgs_plugintypes_App) PurgeCache() { W.WPurgeCache() } +func (W _go_goblog_app_app_pkgs_plugintypes_App) RenderMarkdownAsText(markdown string) (text string, err error) { + return W.WRenderMarkdownAsText(markdown) +} +func (W _go_goblog_app_app_pkgs_plugintypes_App) SetPostParameter(path string, parameter string, values []string) error { + return W.WSetPostParameter(path, parameter, values) +} + +// _go_goblog_app_app_pkgs_plugintypes_Blog is an interface wrapper for Blog type +type _go_goblog_app_app_pkgs_plugintypes_Blog struct { + IValue interface{} + WGetLanguage func() string +} + +func (W _go_goblog_app_app_pkgs_plugintypes_Blog) GetLanguage() string { + return W.WGetLanguage() +} // _go_goblog_app_app_pkgs_plugintypes_Database is an interface wrapper for Database type type _go_goblog_app_app_pkgs_plugintypes_Database struct { @@ -152,19 +184,28 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int { // _go_goblog_app_app_pkgs_plugintypes_Post is an interface wrapper for Post type type _go_goblog_app_app_pkgs_plugintypes_Post struct { - IValue interface{} - WGetContent func() string - WGetParameter func(parameter string) []string - WGetParameters func() map[string][]string - WGetPath func() string - WGetPublished func() string - WGetSection func() string - WGetUpdated func() string + IValue interface{} + WGetBlog func() string + WGetContent func() string + WGetFirstParameterValue func(parameter string) string + WGetParameter func(parameter string) []string + WGetParameters func() map[string][]string + WGetPath func() string + WGetPublished func() string + WGetSection func() string + WGetTitle func() string + WGetUpdated func() string } +func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetBlog() string { + return W.WGetBlog() +} func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetContent() string { return W.WGetContent() } +func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetFirstParameterValue(parameter string) string { + return W.WGetFirstParameterValue(parameter) +} func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetParameter(parameter string) []string { return W.WGetParameter(parameter) } @@ -180,10 +221,43 @@ func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetPublished() string { func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetSection() string { return W.WGetSection() } +func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetTitle() string { + return W.WGetTitle() +} func (W _go_goblog_app_app_pkgs_plugintypes_Post) GetUpdated() string { return W.WGetUpdated() } +// _go_goblog_app_app_pkgs_plugintypes_PostCreatedHook is an interface wrapper for PostCreatedHook type +type _go_goblog_app_app_pkgs_plugintypes_PostCreatedHook struct { + IValue interface{} + WPostCreated func(post plugintypes.Post) +} + +func (W _go_goblog_app_app_pkgs_plugintypes_PostCreatedHook) PostCreated(post plugintypes.Post) { + W.WPostCreated(post) +} + +// _go_goblog_app_app_pkgs_plugintypes_PostDeletedHook is an interface wrapper for PostDeletedHook type +type _go_goblog_app_app_pkgs_plugintypes_PostDeletedHook struct { + IValue interface{} + WPostDeleted func(post plugintypes.Post) +} + +func (W _go_goblog_app_app_pkgs_plugintypes_PostDeletedHook) PostDeleted(post plugintypes.Post) { + W.WPostDeleted(post) +} + +// _go_goblog_app_app_pkgs_plugintypes_PostUpdatedHook is an interface wrapper for PostUpdatedHook type +type _go_goblog_app_app_pkgs_plugintypes_PostUpdatedHook struct { + IValue interface{} + WPostUpdated func(post plugintypes.Post) +} + +func (W _go_goblog_app_app_pkgs_plugintypes_PostUpdatedHook) PostUpdated(post plugintypes.Post) { + W.WPostUpdated(post) +} + // _go_goblog_app_app_pkgs_plugintypes_RenderContext is an interface wrapper for RenderContext type type _go_goblog_app_app_pkgs_plugintypes_RenderContext struct { IValue interface{} @@ -252,6 +326,16 @@ func (W _go_goblog_app_app_pkgs_plugintypes_UIFooter) RenderFooter(renderContext W.WRenderFooter(renderContext, doc) } +// _go_goblog_app_app_pkgs_plugintypes_UIPost is an interface wrapper for UIPost type +type _go_goblog_app_app_pkgs_plugintypes_UIPost struct { + IValue interface{} + WRenderPost func(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) +} + +func (W _go_goblog_app_app_pkgs_plugintypes_UIPost) RenderPost(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) { + W.WRenderPost(renderContext, post, doc) +} + // _go_goblog_app_app_pkgs_plugintypes_UISummary is an interface wrapper for UISummary type type _go_goblog_app_app_pkgs_plugintypes_UISummary struct { IValue interface{} diff --git a/plugins.go b/plugins.go index a050c04..4c6d27b 100644 --- a/plugins.go +++ b/plugins.go @@ -16,14 +16,18 @@ import ( var pluginsFS embed.FS const ( - pluginSetAppType = "setapp" - pluginSetConfigType = "setconfig" - pluginUiType = "ui" - pluginUi2Type = "ui2" - pluginExecType = "exec" - pluginMiddlewareType = "middleware" - pluginUiSummaryType = "uisummary" - pluginUiFooterType = "uifooter" + pluginSetAppType = "setapp" + pluginSetConfigType = "setconfig" + pluginUiType = "ui" + pluginUi2Type = "ui2" + pluginExecType = "exec" + pluginMiddlewareType = "middleware" + pluginUiSummaryType = "uisummary" + pluginUiPostType = "uiPost" + pluginUiFooterType = "uifooter" + pluginPostCreatedHookType = "postcreatedhook" + pluginPostUpdatedHookType = "postupdatedhook" + pluginPostDeletedHookType = "postdeletedhook" ) func (a *goBlog) initPlugins() error { @@ -33,14 +37,18 @@ func (a *goBlog) initPlugins() error { } a.pluginHost = plugins.NewPluginHost( map[string]reflect.Type{ - pluginSetAppType: reflect.TypeOf((*plugintypes.SetApp)(nil)).Elem(), - pluginSetConfigType: reflect.TypeOf((*plugintypes.SetConfig)(nil)).Elem(), - pluginUiType: reflect.TypeOf((*plugintypes.UI)(nil)).Elem(), - pluginUi2Type: reflect.TypeOf((*plugintypes.UI2)(nil)).Elem(), - pluginExecType: reflect.TypeOf((*plugintypes.Exec)(nil)).Elem(), - pluginMiddlewareType: reflect.TypeOf((*plugintypes.Middleware)(nil)).Elem(), - pluginUiSummaryType: reflect.TypeOf((*plugintypes.UISummary)(nil)).Elem(), - pluginUiFooterType: reflect.TypeOf((*plugintypes.UIFooter)(nil)).Elem(), + pluginSetAppType: reflect.TypeOf((*plugintypes.SetApp)(nil)).Elem(), + pluginSetConfigType: reflect.TypeOf((*plugintypes.SetConfig)(nil)).Elem(), + pluginUiType: reflect.TypeOf((*plugintypes.UI)(nil)).Elem(), + pluginUi2Type: reflect.TypeOf((*plugintypes.UI2)(nil)).Elem(), + pluginExecType: reflect.TypeOf((*plugintypes.Exec)(nil)).Elem(), + pluginMiddlewareType: reflect.TypeOf((*plugintypes.Middleware)(nil)).Elem(), + pluginUiSummaryType: reflect.TypeOf((*plugintypes.UISummary)(nil)).Elem(), + pluginUiPostType: reflect.TypeOf((*plugintypes.UIPost)(nil)).Elem(), + pluginUiFooterType: reflect.TypeOf((*plugintypes.UIFooter)(nil)).Elem(), + pluginPostCreatedHookType: reflect.TypeOf((*plugintypes.PostCreatedHook)(nil)).Elem(), + pluginPostUpdatedHookType: reflect.TypeOf((*plugintypes.PostUpdatedHook)(nil)).Elem(), + pluginPostDeletedHookType: reflect.TypeOf((*plugintypes.PostDeletedHook)(nil)).Elem(), }, yaegiwrappers.Symbols, subFS, @@ -86,6 +94,11 @@ func (a *goBlog) GetPost(path string) (plugintypes.Post, error) { return a.getPost(path) } +func (a *goBlog) GetBlog(name string) (plugintypes.Blog, bool) { + blog, ok := a.cfg.Blogs[name] + return blog, ok +} + func (a *goBlog) PurgeCache() { a.cache.purge() } @@ -102,6 +115,14 @@ func (a *goBlog) AssetPath(filename string) string { return a.assetFileName(filename) } +func (a *goBlog) SetPostParameter(path string, parameter string, values []string) error { + return a.db.replacePostParam(path, parameter, values) +} + +func (a *goBlog) RenderMarkdownAsText(markdown string) (text string, err error) { + return a.renderText(markdown) +} + func (p *post) GetPath() string { return p.Path } @@ -114,6 +135,10 @@ func (p *post) GetParameter(parameter string) []string { return p.Parameters[parameter] } +func (p *post) GetFirstParameterValue(parameter string) string { + return p.firstParameter(parameter) +} + func (p *post) GetSection() string { return p.Section } @@ -129,3 +154,15 @@ func (p *post) GetUpdated() string { func (p *post) GetContent() string { return p.Content } + +func (p *post) GetTitle() string { + return p.Title() +} + +func (p *post) GetBlog() string { + return p.Blog +} + +func (b *configBlog) GetLanguage() string { + return b.Lang +} diff --git a/plugins/aitldr/src/aitldr/aitldr.go b/plugins/aitldr/src/aitldr/aitldr.go new file mode 100644 index 0000000..d862a12 --- /dev/null +++ b/plugins/aitldr/src/aitldr/aitldr.go @@ -0,0 +1,192 @@ +package aitldr + +import ( + "context" + "log" + "net/http" + "strings" + "sync" + + "github.com/PuerkitoBio/goquery" + "github.com/carlmjohnson/requests" + "go.goblog.app/app/pkgs/bufferpool" + "go.goblog.app/app/pkgs/htmlbuilder" + "go.goblog.app/app/pkgs/plugintypes" +) + +type plugin struct { + app plugintypes.App + + config map[string]any + initCSS sync.Once +} + +func GetPlugin() ( + plugintypes.SetConfig, plugintypes.SetApp, + plugintypes.PostCreatedHook, plugintypes.PostUpdatedHook, + plugintypes.UIPost, plugintypes.UI2, +) { + p := &plugin{} + return p, p, p, p, p, p +} + +func (p *plugin) SetApp(app plugintypes.App) { + p.app = app +} + +func (p *plugin) SetConfig(config map[string]any) { + p.config = config +} + +func (p *plugin) PostCreated(post plugintypes.Post) { + p.summarize(post) +} + +func (p *plugin) PostUpdated(post plugintypes.Post) { + p.summarize(post) +} + +const postParam = "aitldr" + +func (p *plugin) RenderPost(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) { + tldr := post.GetFirstParameterValue(postParam) + if tldr == "" { + return + } + + title := "AI generated summary:" + if blogConfig, ok := p.config[renderContext.GetBlog()]; ok { + if blogConfigAsMap, ok := blogConfig.(map[string]any); ok { + if blogSpecificTitle, ok := blogConfigAsMap["title"]; ok { + if blogSpecificTitleAsString, ok := blogSpecificTitle.(string); ok { + title = blogSpecificTitleAsString + } + } + } + } + + buf := bufferpool.Get() + defer bufferpool.Put(buf) + hw := htmlbuilder.NewHtmlBuilder(buf) + hw.WriteElementOpen("div", "class", "p aitldr") + hw.WriteElementOpen("b") + hw.WriteEscaped(title) + hw.WriteElementClose("b") + hw.WriteEscaped(" ") + hw.WriteElementOpen("i") + hw.WriteEscaped(tldr) + hw.WriteElementsClose("i", "div") + + doc.Find(".e-content").BeforeHtml(buf.String()) +} + +const customCSS = ".aitldr { border: 1px dashed; padding: 1em; }" + +func (p *plugin) RenderWithDocument(_ plugintypes.RenderContext, doc *goquery.Document) { + if p.app == nil { + return + } + + // Init custom CSS for plugin + p.initCSS.Do(func() { + _ = p.app.CompileAsset("aitldr.css", strings.NewReader(customCSS)) + }) + + // Check if page has AI TLDR, then add the custom CSS + doc.Find(".aitldr").First().Each(func(_ int, _ *goquery.Selection) { + buf := bufferpool.Get() + defer bufferpool.Put(buf) + hb := htmlbuilder.NewHtmlBuilder(buf) + hb.WriteElementOpen("link", "rel", "stylesheet", "href", p.app.AssetPath("aitldr.css")) + doc.Find("head").AppendHtml(buf.String()) + }) +} + +type apiMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type apiResponse struct { + Choices []struct { + Message apiMessage `json:"message"` + } `json:"choices"` +} + +func (p *plugin) summarize(post plugintypes.Post) { + if post.GetFirstParameterValue("noaitldr") == "true" { + log.Println("aitldr: Skip summarizing", post.GetPath()) + return + } + + apikey := "" + if k, ok := p.config["apikey"]; ok { + if ks, ok := k.(string); ok { + apikey = ks + } + } + if apikey == "" { + log.Println("Config for aitldr plugin not correct! apikey missing!") + return + } + + var response apiResponse + + err := requests.URL("https://api.openai.com/v1/chat/completions"). + Method(http.MethodPost). + Header("Authorization", "Bearer "+apikey). + BodyJSON(map[string]any{ + "model": "gpt-3.5-turbo", + "temperature": 0, + "max_tokens": 200, + "messages": []apiMessage{ + { + Role: "user", + Content: p.createPrompt(post), + }, + }, + }). + ToJSON(&response). + Fetch(context.Background()) + + if err != nil { + log.Println("aitldr plugin:", err.Error()) + return + } + + if len(response.Choices) < 1 { + return + } + + summary := response.Choices[0].Message.Content + summary = strings.TrimSpace(summary) + + err = p.app.SetPostParameter(post.GetPath(), postParam, []string{summary}) + if err != nil { + log.Println("aitldr plugin:", err.Error()) + return + } + + p.app.PurgeCache() +} + +func (p *plugin) createPrompt(post plugintypes.Post) string { + lang := "en" + if blog, ok := p.app.GetBlog(post.GetBlog()); ok { + if blogLang := blog.GetLanguage(); lang != "" { + lang = blogLang + } + } + prompt := "Summarize the content of following text in one sentence in the language \"" + lang + "\". Answer with just the summary.\n\n\n" + if title, err := p.app.RenderMarkdownAsText(post.GetTitle()); err == nil && title != "" { + prompt += title + "\n\n" + } else if err != nil { + log.Println("aitldr plugin: Rendering markdown as text failed:", err.Error()) + } + if text, err := p.app.RenderMarkdownAsText(post.GetContent()); err == nil && text != "" { + prompt += text + "\n\n" + } else if err != nil { + log.Println("aitldr plugin: Rendering markdown as text failed:", err.Error()) + } + return prompt +} diff --git a/postsFuncs.go b/postsFuncs.go index 64a0def..ca7e5a8 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -120,7 +120,7 @@ func (a *goBlog) postSummary(p *post) (summary string) { splitted := strings.Split(p.Content, summaryDivider) hasDivider := len(splitted) > 1 markdown := splitted[0] - summary = a.renderText(markdown) + summary = a.renderTextSafe(markdown) if !hasDivider { summary = strings.Split(summary, "\n\n")[0] } diff --git a/ui.go b/ui.go index 73e554e..db3d515 100644 --- a/ui.go +++ b/ui.go @@ -4,12 +4,14 @@ import ( "fmt" "time" + "github.com/PuerkitoBio/goquery" "github.com/hacdias/indieauth/v3" "github.com/kaorimatz/go-opml" "github.com/mergestat/timediff" "github.com/samber/lo" "go.goblog.app/app/pkgs/contenttype" "go.goblog.app/app/pkgs/htmlbuilder" + "go.goblog.app/app/pkgs/plugintypes" ) func (a *goBlog) renderEditorPreview(hb *htmlbuilder.HtmlBuilder, bc *configBlog, p *post) { @@ -847,7 +849,13 @@ func (a *goBlog) renderPost(hb *htmlbuilder.HtmlBuilder, rd *renderData) { hb.WriteElementOpen("link", "rel", "shortlink", "href", su) } }, - func(hb *htmlbuilder.HtmlBuilder) { + func(origHb *htmlbuilder.HtmlBuilder) { + // Wrap plugins + hb, finish := a.wrapForPlugins(origHb, a.getPlugins(pluginUiPostType), func(plugin any, doc *goquery.Document) { + plugin.(plugintypes.UIPost).RenderPost(rd.prc, p, doc) + }) + defer finish() + // Render... hb.WriteElementOpen("main", "class", "h-entry") // URL (hidden just for microformats) hb.WriteElementOpen("data", "value", a.getFullAddress(p.Path), "class", "u-url hide")