mirror of
https://github.com/Kugelschieber/marvinblum.git
synced 2026-01-18 06:40:27 +00:00
Added blog page, article page and added latest articles to start page.
This commit is contained in:
189
blog/blog.go
Normal file
189
blog/blog.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package blog
|
||||
|
||||
import (
|
||||
"github.com/Kugelschieber/marvinblum.de/tpl"
|
||||
emvi "github.com/emvi/api-go"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
blogCacheTime = time.Hour
|
||||
maxLatestArticles = 3
|
||||
)
|
||||
|
||||
var (
|
||||
blog Blog
|
||||
)
|
||||
|
||||
type Blog struct {
|
||||
client *emvi.Client
|
||||
articles map[string]emvi.Article // id -> article
|
||||
articlesYear map[int][]emvi.Article // year -> articles
|
||||
nextUpdate time.Time
|
||||
}
|
||||
|
||||
func InitBlog() {
|
||||
logbuch.Info("Initializing blog")
|
||||
blog.client = emvi.NewClient(os.Getenv("MB_EMVI_CLIENT_ID"),
|
||||
os.Getenv("MB_EMVI_CLIENT_SECRET"),
|
||||
os.Getenv("MB_EMVI_ORGA"),
|
||||
nil)
|
||||
blog.nextUpdate = time.Now().Add(blogCacheTime)
|
||||
blog.loadArticles()
|
||||
}
|
||||
|
||||
func (blog *Blog) loadArticles() {
|
||||
logbuch.Info("Refreshing blog articles...")
|
||||
articles, offset, count := make(map[string]emvi.Article), 0, 1
|
||||
var err error
|
||||
|
||||
for count > 0 {
|
||||
var results []emvi.Article
|
||||
results, _, err = blog.client.FindArticles("", &emvi.ArticleFilter{
|
||||
BaseSearch: emvi.BaseSearch{Offset: offset},
|
||||
Tags: "blog",
|
||||
SortPublished: emvi.SortDescending,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logbuch.Error("Error loading blog articles", logbuch.Fields{"err": err})
|
||||
break
|
||||
}
|
||||
|
||||
offset += len(results)
|
||||
count = len(results)
|
||||
|
||||
for _, article := range results {
|
||||
articles[article.Id] = article
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
for k, v := range articles {
|
||||
v.LatestArticleContent = blog.loadArticle(v)
|
||||
articles[k] = v
|
||||
}
|
||||
|
||||
blog.setArticles(articles)
|
||||
}
|
||||
|
||||
blog.nextUpdate = time.Now().Add(blogCacheTime)
|
||||
logbuch.Info("Done", logbuch.Fields{"count": len(articles)})
|
||||
}
|
||||
|
||||
func (blog *Blog) loadArticle(article emvi.Article) *emvi.ArticleContent {
|
||||
_, content, _, err := blog.client.GetArticle(article.Id, article.LatestArticleContent.LanguageId, 0)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Error("Error loading article", logbuch.Fields{"err": err, "id": article.Id})
|
||||
return nil
|
||||
}
|
||||
|
||||
logbuch.Debug("Article loaded", logbuch.Fields{"id": article.Id})
|
||||
return content
|
||||
}
|
||||
|
||||
func (blog *Blog) setArticles(articles map[string]emvi.Article) {
|
||||
blog.articles = articles
|
||||
blog.articlesYear = make(map[int][]emvi.Article)
|
||||
|
||||
for _, article := range articles {
|
||||
if blog.articlesYear[article.Published.Year()] == nil {
|
||||
blog.articlesYear[article.Published.Year()] = make([]emvi.Article, 0)
|
||||
}
|
||||
|
||||
blog.articlesYear[article.Published.Year()] = append(blog.articlesYear[article.Published.Year()], article)
|
||||
}
|
||||
}
|
||||
|
||||
func (blog *Blog) getArticle(id string) emvi.Article {
|
||||
blog.refreshIfRequired()
|
||||
return blog.articles[id]
|
||||
}
|
||||
|
||||
func (blog *Blog) getArticles() map[int][]emvi.Article {
|
||||
blog.refreshIfRequired()
|
||||
return blog.articlesYear
|
||||
}
|
||||
|
||||
func (blog *Blog) getLatestArticles() []emvi.Article {
|
||||
blog.refreshIfRequired()
|
||||
articles := make([]emvi.Article, 0, 3)
|
||||
i := 0
|
||||
|
||||
for _, v := range blog.articles {
|
||||
articles = append(articles, v)
|
||||
i++
|
||||
|
||||
if i > maxLatestArticles {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return articles
|
||||
}
|
||||
|
||||
func (blog *Blog) refreshIfRequired() {
|
||||
if blog.nextUpdate.Before(time.Now()) {
|
||||
blog.loadArticles()
|
||||
}
|
||||
}
|
||||
|
||||
func GetLatestArticles() []emvi.Article {
|
||||
return blog.getLatestArticles()
|
||||
}
|
||||
|
||||
func ServeBlogPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
Articles map[int][]emvi.Article
|
||||
}{
|
||||
blog.getArticles(),
|
||||
}
|
||||
|
||||
if err := tpl.Get().ExecuteTemplate(w, "blog.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServeBlogArticle() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
slug := strings.Split(vars["slug"], "-")
|
||||
|
||||
if len(slug) == 0 {
|
||||
http.Redirect(w, r, "/notfound", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
article := blog.getArticle(slug[len(slug)-1])
|
||||
|
||||
if len(article.Id) == 0 {
|
||||
http.Redirect(w, r, "/notfound", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Title string
|
||||
Content template.HTML
|
||||
Published string
|
||||
}{
|
||||
article.LatestArticleContent.Title,
|
||||
template.HTML(article.LatestArticleContent.Content),
|
||||
article.Published.Format("2. January 2006"),
|
||||
}
|
||||
|
||||
if err := tpl.Get().ExecuteTemplate(w, "article.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog article template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
dev.sh
3
dev.sh
@@ -7,5 +7,8 @@ export MB_LOGLEVEL=debug
|
||||
export MB_ALLOWED_ORIGINS=*
|
||||
export MB_HOST=localhost:8080
|
||||
export MB_HOT_RELOAD=true
|
||||
export MB_EMVI_CLIENT_ID=3fBBn144yvSF9R3dPC8l
|
||||
export MB_EMVI_CLIENT_SECRET=dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
|
||||
export MB_EMVI_ORGA=marvin
|
||||
|
||||
go run main.go
|
||||
|
||||
@@ -30,6 +30,10 @@ services:
|
||||
restart: always
|
||||
depends_on:
|
||||
- traefik
|
||||
environment:
|
||||
- MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l
|
||||
- MB_EMVI_CLIENT_SECRET: dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
|
||||
- MB_EMVI_ORGA: marvin
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.marvinblum.entrypoints=web"
|
||||
|
||||
12
go.mod
12
go.mod
@@ -3,9 +3,11 @@ module github.com/Kugelschieber/marvinblum.de
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.10.13 // indirect
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 // indirect
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/rs/cors v1.7.0 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/caddyserver/certmagic v0.10.13
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/rs/cors v1.7.0
|
||||
)
|
||||
|
||||
6
go.sum
6
go.sum
@@ -73,6 +73,8 @@ github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8 h1:DZJfRdvwlybLvHkyeN8HRU4QnBFqVZg21ki4to1AVPo=
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8/go.mod h1:g9RdDC3s5ebCknAHQQ5PjoM2vRFSyyGoOUX3QkDKU+o=
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 h1:+G10WRp72llJuaW89QezNK8QU9SsOmd5Ja7ocrrUaI0=
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -137,6 +139,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
|
||||
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -216,6 +220,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
|
||||
65
main.go
65
main.go
@@ -1,13 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/Kugelschieber/marvinblum.de/blog"
|
||||
"github.com/Kugelschieber/marvinblum.de/tpl"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/caddyserver/certmagic"
|
||||
emvi "github.com/emvi/api-go"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/cors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -16,17 +17,10 @@ import (
|
||||
const (
|
||||
staticDir = "static"
|
||||
staticDirPrefix = "/static/"
|
||||
templateDir = "template/*"
|
||||
logTimeFormat = "2006-01-02_15:04:05"
|
||||
envPrefix = "MB_"
|
||||
)
|
||||
|
||||
var (
|
||||
tpl *template.Template
|
||||
tplCache = make(map[string][]byte)
|
||||
hotReload bool
|
||||
)
|
||||
|
||||
func configureLog() {
|
||||
logbuch.SetFormatter(logbuch.NewFieldFormatter(logTimeFormat, "\t\t"))
|
||||
logbuch.Info("Configure logging...")
|
||||
@@ -50,42 +44,17 @@ func logEnvConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadTemplate() {
|
||||
logbuch.Debug("Loading templates")
|
||||
var err error
|
||||
tpl, err = template.ParseGlob(templateDir)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Fatal("Error loading template", logbuch.Fields{"err": err})
|
||||
}
|
||||
|
||||
hotReload = os.Getenv("MB_HOT_RELOAD") == "true"
|
||||
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload})
|
||||
}
|
||||
|
||||
func renderTemplate(name string) {
|
||||
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil {
|
||||
logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name})
|
||||
}
|
||||
|
||||
tplCache[name] = buffer.Bytes()
|
||||
}
|
||||
|
||||
func serveTemplate(name string) http.HandlerFunc {
|
||||
// render once so we have it in cache
|
||||
renderTemplate(name)
|
||||
|
||||
func serveAbout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if hotReload {
|
||||
loadTemplate()
|
||||
renderTemplate(name)
|
||||
data := struct {
|
||||
Articles []emvi.Article
|
||||
}{
|
||||
blog.GetLatestArticles(),
|
||||
}
|
||||
|
||||
if _, err := w.Write(tplCache[name]); err != nil {
|
||||
logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name})
|
||||
if err := tpl.Get().ExecuteTemplate(w, "about.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,10 +62,11 @@ func serveTemplate(name string) http.HandlerFunc {
|
||||
func setupRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir)))))
|
||||
router.Handle("/legal", serveTemplate("legal.html"))
|
||||
router.Handle("/blog", serveTemplate("blog.html"))
|
||||
router.Handle("/", serveTemplate("about.html"))
|
||||
router.NotFoundHandler = serveTemplate("notfound.html")
|
||||
router.Handle("/blog/{slug}", blog.ServeBlogArticle())
|
||||
router.Handle("/blog", blog.ServeBlogPage())
|
||||
router.Handle("/legal", tpl.ServeTemplate("legal.html"))
|
||||
router.Handle("/", serveAbout())
|
||||
router.NotFoundHandler = tpl.ServeTemplate("notfound.html")
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -136,7 +106,8 @@ func start(handler http.Handler) {
|
||||
func main() {
|
||||
configureLog()
|
||||
logEnvConfig()
|
||||
loadTemplate()
|
||||
tpl.LoadTemplate()
|
||||
blog.InitBlog()
|
||||
router := setupRouter()
|
||||
corsConfig := configureCors(router)
|
||||
start(corsConfig)
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
</section>
|
||||
<section>
|
||||
<h2>latest blog posts</h2>
|
||||
<p>
|
||||
TODO
|
||||
</p>
|
||||
{{range $article := .Articles}}
|
||||
<p>
|
||||
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<p>There are no blog posts yet...</p>
|
||||
{{end}}
|
||||
<p>
|
||||
<a href="/blog">View all</a>
|
||||
</p>
|
||||
|
||||
10
template/article.html
Normal file
10
template/article.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{{template "head.html"}}
|
||||
{{template "menu.html"}}
|
||||
|
||||
<section>
|
||||
<h1>{{.Title}}</h1>
|
||||
<small>Published on {{.Published}}</small>
|
||||
{{.Content}}
|
||||
</section>
|
||||
|
||||
{{template "end.html"}}
|
||||
@@ -2,7 +2,18 @@
|
||||
{{template "menu.html"}}
|
||||
|
||||
<section>
|
||||
<p>TODO Blog</p>
|
||||
<h1>Blog</h1>
|
||||
{{range $year, $articles := .Articles}}
|
||||
<h2>{{$year}}</h2>
|
||||
|
||||
{{range $article := $articles}}
|
||||
<p>
|
||||
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
|
||||
</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p>There are no blog posts yet...</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
{{template "end.html"}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>Page not found</h2>
|
||||
<p>
|
||||
Nothing to see here...<br />
|
||||
<a href="/">Go back Home</a>
|
||||
<a href="/">Return home</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
68
tpl/template.go
Normal file
68
tpl/template.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gosimple/slug"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
templateDir = "template/*"
|
||||
)
|
||||
|
||||
var (
|
||||
tpl *template.Template
|
||||
tplCache = make(map[string][]byte)
|
||||
hotReload bool
|
||||
)
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"slug": slug.Make,
|
||||
}
|
||||
|
||||
func LoadTemplate() {
|
||||
logbuch.Debug("Loading templates")
|
||||
var err error
|
||||
tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Fatal("Error loading template", logbuch.Fields{"err": err})
|
||||
}
|
||||
|
||||
hotReload = os.Getenv("MB_HOT_RELOAD") == "true"
|
||||
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload})
|
||||
}
|
||||
|
||||
func renderTemplate(name string) {
|
||||
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil {
|
||||
logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name})
|
||||
}
|
||||
|
||||
tplCache[name] = buffer.Bytes()
|
||||
}
|
||||
|
||||
func Get() *template.Template {
|
||||
return tpl
|
||||
}
|
||||
|
||||
func ServeTemplate(name string) http.HandlerFunc {
|
||||
// render once so we have it in cache
|
||||
renderTemplate(name)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if hotReload {
|
||||
LoadTemplate()
|
||||
renderTemplate(name)
|
||||
}
|
||||
|
||||
if _, err := w.Write(tplCache[name]); err != nil {
|
||||
logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user