diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d1a7fc2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + open-pull-requests-limit: 50 + schedule: + interval: "monthly" diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index bd71237..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "static/js/mathjax"] - path = static/js/mathjax - url = https://github.com/mathjax/MathJax.git diff --git a/account.go b/account.go index f8828d0..0a1cfb7 100644 --- a/account.go +++ b/account.go @@ -311,9 +311,11 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { OauthSlack bool OauthWriteAs bool OauthGitlab bool + GitlabDisplayName string OauthGeneric bool OauthGenericDisplayName string - GitlabDisplayName string + OauthGitea bool + GiteaDisplayName string }{ pageForReq(app, r), r.FormValue("to"), @@ -326,6 +328,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { app.Config().GenericOauth.ClientID != "", config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), + app.Config().GiteaOauth.ClientID != "", + config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } if earlyError != "" { @@ -1060,6 +1064,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != "" enableOauthGitLab := app.Config().GitlabOauth.ClientID != "" enableOauthGeneric := app.Config().GenericOauth.ClientID != "" + enableOauthGitea := app.Config().GiteaOauth.ClientID != "" oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID) if err != nil { @@ -1078,10 +1083,12 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect enableOauthGeneric = false + case "gitea": + enableOauthGitea = false } } - displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || len(oauthAccounts) > 0 + displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0 obj := struct { *UserPage @@ -1097,6 +1104,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err GitLabDisplayName string OauthGeneric bool OauthGenericDisplayName string + OauthGitea bool + GiteaDisplayName string }{ UserPage: NewUserPage(app, r, u, "Account Settings", flashes), Email: fullUser.EmailClear(app.keys), @@ -1111,6 +1120,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), OauthGeneric: enableOauthGeneric, OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), + OauthGitea: enableOauthGitea, + GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } showUserPage(w, "settings", obj) diff --git a/activitypub.go b/activitypub.go index db42726..d34e70c 100644 --- a/activitypub.go +++ b/activitypub.go @@ -397,7 +397,9 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request go func() { if to == nil { - log.Error("No to! %v", err) + if debugging { + log.Error("No `to` value!") + } return } @@ -697,6 +699,10 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { // I don't believe we'd ever have too many mentions in a single post that this // could become a burden. remoteUser, err := getRemoteUser(app, tag.HRef) + if err != nil { + log.Error("Unable to find remote user %s. Skipping: %v", tag.HRef, err) + continue + } err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity) if err != nil { log.Error("Couldn't post! %v", err) diff --git a/app.go b/app.go index 3c9e9d9..af0a56f 100644 --- a/app.go +++ b/app.go @@ -426,6 +426,11 @@ func Serve(app *App, r *mux.Router) { os.Exit(0) }() + // Start gopher server + if app.cfg.Server.GopherPort > 0 && !app.cfg.App.Private { + go initGopher(app) + } + // Start web application server var bindAddress = app.cfg.Server.Bind if bindAddress == "" { @@ -761,7 +766,7 @@ func connectToDatabase(app *App) { var db *sql.DB var err error if app.cfg.Database.Type == driverMySQL { - db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String()))) + db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&tls=%t", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String()), app.cfg.Database.TLS)) db.SetMaxOpenConns(50) } else if app.cfg.Database.Type == driverSQLite { if !SQLiteEnabled { diff --git a/collections.go b/collections.go index 9688ad9..edde677 100644 --- a/collections.go +++ b/collections.go @@ -47,6 +47,7 @@ type ( Language string `schema:"lang" json:"lang,omitempty"` StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"` Script string `datastore:"script" schema:"script" json:"script,omitempty"` + Signature string `datastore:"post_signature" schema:"signature" json:"-"` Public bool `datastore:"public" json:"public"` Visibility collVisibility `datastore:"private" json:"-"` Format string `datastore:"format" json:"format,omitempty"` @@ -91,6 +92,7 @@ type ( Description *string `schema:"description" json:"description"` StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"` Script *sql.NullString `schema:"script" json:"script"` + Signature *sql.NullString `schema:"signature" json:"signature"` Visibility *int `schema:"visibility" json:"public"` Format *sql.NullString `schema:"format" json:"format"` } diff --git a/config.ini.example b/config.ini.example index 7ac944e..8b74ddc 100644 --- a/config.ini.example +++ b/config.ini.example @@ -9,6 +9,7 @@ password = changeme database = writefreely host = db port = 3306 +tls = false [app] site_name = WriteFreely Example Blog! diff --git a/config/config.go b/config/config.go index 7d6e840..9ff13f8 100644 --- a/config/config.go +++ b/config/config.go @@ -45,6 +45,8 @@ type ( HashSeed string `ini:"hash_seed"` + GopherPort int `ini:"gopher_port"` + Dev bool `ini:"-"` } @@ -57,6 +59,7 @@ type ( Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` + TLS bool `ini:"tls"` } WriteAsOauthCfg struct { @@ -98,6 +101,14 @@ type ( AuthEndpoint string `ini:"auth_endpoint"` AllowDisconnect bool `ini:"allow_disconnect"` } + GiteaOauthCfg struct { + ClientID string `ini:"client_id"` + ClientSecret string `ini:"client_secret"` + Host string `ini:"host"` + DisplayName string `ini:"display_name"` + CallbackProxy string `ini:"callback_proxy"` + CallbackProxyAPI string `ini:"callback_proxy_api"` + } // AppCfg holds values that affect how the application functions AppCfg struct { @@ -155,6 +166,7 @@ type ( WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` GenericOauth GenericOauthCfg `ini:"oauth.generic"` + GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` } ) diff --git a/database.go b/database.go index 1e02010..27f7e84 100644 --- a/database.go +++ b/database.go @@ -14,6 +14,8 @@ import ( "context" "database/sql" "fmt" + "github.com/writeas/web-core/silobridge" + wf_db "github.com/writeas/writefreely/db" "net/http" "strings" "time" @@ -792,10 +794,10 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll c := &Collection{} // FIXME: change Collection to reflect database values. Add helper functions to get actual values - var styleSheet, script, format zero.String - row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value) + var styleSheet, script, signature, format zero.String + row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, post_signature, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value) - err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &format, &c.OwnerID, &c.Visibility, &c.Views) + err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &signature, &format, &c.OwnerID, &c.Visibility, &c.Views) switch { case err == sql.ErrNoRows: return nil, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} @@ -807,6 +809,7 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll } c.StyleSheet = styleSheet.String c.Script = script.String + c.Signature = signature.String c.Format = format.String c.Public = c.IsPublic() @@ -850,7 +853,8 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro SetStringPtr(c.Title, "title"). SetStringPtr(c.Description, "description"). SetNullString(c.StyleSheet, "style_sheet"). - SetNullString(c.Script, "script") + SetNullString(c.Script, "script"). + SetNullString(c.Signature, "post_signature") if c.Format != nil { cf := &CollectionFormat{Format: c.Format.String} @@ -1151,6 +1155,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu break } p.extractData() + p.augmentContent(c) p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) @@ -1215,6 +1220,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin break } p.extractData() + p.augmentContent(c) p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) @@ -1591,6 +1597,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[ break } p.extractData() + p.augmentContent(&coll.Collection) pp := p.processPost() pp.Collection = coll @@ -1641,6 +1648,40 @@ func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Col return c, nil } +func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error) { + rows, err := db.Query(`SELECT c.id, alias, title, description, privacy, view_count + FROM collections c + LEFT JOIN users u ON u.id = c.owner_id + WHERE c.privacy = 1 AND u.status = 0 + ORDER BY id ASC`) + if err != nil { + log.Error("Failed selecting public collections: %v", err) + return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."} + } + defer rows.Close() + + colls := []Collection{} + for rows.Next() { + c := Collection{} + err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) + if err != nil { + log.Error("Failed scanning row: %v", err) + break + } + c.hostName = hostName + c.URL = c.CanonicalURL() + c.Public = c.IsPublic() + + colls = append(colls, c) + } + err = rows.Err() + if err != nil { + log.Error("Error after Next() on rows: %v", err) + } + + return &colls, nil +} + func (db *datastore) GetMeStats(u *User) userMeStats { s := userMeStats{} @@ -2024,7 +2065,7 @@ func (db *datastore) RemoveCollectionRedirect(t *sql.Tx, alias string) error { func (db *datastore) GetCollectionRedirect(alias string) (new string) { row := db.QueryRow("SELECT new_alias FROM collectionredirects WHERE prev_alias = ?", alias) err := row.Scan(&new) - if err != nil && err != sql.ErrNoRows { + if err != nil && err != sql.ErrNoRows && !db.isIgnorableError(err) { log.Error("Failed selecting from collectionredirects: %v", err) } return @@ -2655,6 +2696,17 @@ func handleFailedPostInsert(err error) error { func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) { handle = strings.TrimLeft(handle, "@") actorIRI := "" + parts := strings.Split(handle, "@") + if len(parts) != 2 { + return "", fmt.Errorf("invalid handle format") + } + domain := parts[1] + + // Check non-AP instances + if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" { + return siloProfileURL, nil + } + remoteUser, err := getRemoteUserFromHandle(app, handle) if err != nil { // can't find using handle in the table but the table may already have this user without diff --git a/go.mod b/go.mod index b31cb62..efc35af 100644 --- a/go.mod +++ b/go.mod @@ -5,55 +5,57 @@ require ( github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/clbanning/mxj v1.8.4 // indirect github.com/dustin/go-humanize v1.0.0 - github.com/fatih/color v1.7.0 - github.com/go-sql-driver/mysql v1.4.1 + github.com/fatih/color v1.9.0 + github.com/go-sql-driver/mysql v1.5.0 github.com/go-test/deep v1.0.1 // indirect github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect - github.com/gorilla/feeds v1.1.0 + github.com/gorilla/feeds v1.1.1 github.com/gorilla/mux v1.7.4 - github.com/gorilla/schema v1.0.2 + github.com/gorilla/schema v1.1.0 github.com/gorilla/sessions v1.2.0 - github.com/guregu/null v3.4.0+incompatible - github.com/hashicorp/go-multierror v1.0.0 + github.com/guregu/null v3.5.0+incompatible + github.com/hashicorp/go-multierror v1.1.0 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/lunixbochs/vtclean v1.0.0 // indirect - github.com/manifoldco/promptui v0.3.2 + github.com/manifoldco/promptui v0.7.0 github.com/mattn/go-colorable v0.1.0 // indirect - github.com/mattn/go-sqlite3 v1.10.0 - github.com/microcosm-cc/bluemonday v1.0.2 + github.com/mattn/go-sqlite3 v1.14.0 + github.com/microcosm-cc/bluemonday v1.0.3 github.com/mitchellh/go-wordwrap v1.0.0 github.com/nicksnyder/go-i18n v1.10.0 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.1 // indirect + github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - github.com/stretchr/testify v1.3.0 - github.com/urfave/cli/v2 v2.1.1 + github.com/stretchr/testify v1.6.1 + github.com/urfave/cli/v2 v2.2.0 github.com/writeas/activity v0.1.2 github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-webfinger v1.1.0 github.com/writeas/httpsig v1.0.0 github.com/writeas/impart v1.1.1 - github.com/writeas/import v0.2.0 + github.com/writeas/import v0.2.1 github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 github.com/writeas/nerds v1.0.0 - github.com/writeas/saturday v1.7.1 + github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 github.com/writeas/slug v1.2.0 - github.com/writeas/web-core v1.2.0 + github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c github.com/writefreely/go-nodeinfo v1.2.0 - golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect + golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect google.golang.org/appengine v1.4.0 // indirect gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect - gopkg.in/ini.v1 v1.55.0 + gopkg.in/ini.v1 v1.57.0 src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect ) diff --git a/go.sum b/go.sum index c0bb24e..a7cefab 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,21 @@ code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 h1:AFSJaASPGYNbkUa5c8ZybrcW9pP3Cy7+z5dnpcc/qG8= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ= +github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= +github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -33,6 +39,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-fed/httpsig v0.1.0 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY= @@ -41,6 +49,8 @@ github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4L github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -55,22 +65,32 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/feeds v1.1.0 h1:pcgLJhbdYgaUESnj3AmXPcB7cS3vy63+jC/TI14AGXk= github.com/gorilla/feeds v1.1.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= +github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= +github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM= github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= +github.com/guregu/null v3.5.0+incompatible h1:fSdvRTQtmBA4B4YDZXhLtxTIJZYuUxBFTTHS4B9djG4= +github.com/guregu/null v3.5.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw= github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= @@ -92,16 +112,27 @@ github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+L github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/manifoldco/promptui v0.3.2 h1:rir7oByTERac6jhpHUPErHuopoRDvO3jxS+FdadEns8= github.com/manifoldco/promptui v0.3.2/go.mod h1:8JU+igZ+eeiiRku4T5BjtKh2ms8sziGpSYl1gN8Bazw= +github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= +github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/microcosm-cc/bluemonday v1.0.3 h1:EjVH7OqbU219kdm8acbveoclh2zZFqPJTJw6VUlTLAQ= +github.com/microcosm-cc/bluemonday v1.0.3/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/nicksnyder/go-i18n v1.10.0 h1:5AzlPKvXBH4qBzmZ09Ua9Gipyruv6uApMcrNZdo96+Q= @@ -114,6 +145,10 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44 h1:q5sit1FpzEt59aM2Fd2lSBKF+nxcY1o0StRCiJa/pWo= +github.com/prologic/go-gopher v0.0.0-20191226035442-664dbdb49f44/go.mod h1:a97DSBRiRljeRVd5CRZL5bYCIeeGjSEngGf+QMR2evA= +github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469 h1:rAbv2gekFbUcjhUkruwo0vMJ0JqhUgg9tz7t+bxHbN4= +github.com/prologic/go-gopher v0.0.0-20200721020712-3e11dcff0469/go.mod h1:c61IFFAJw8ADWu54tti30Tj5VrBstVoTprmET35UEkY= 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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -124,14 +159,20 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PX github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY= github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0= github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0= @@ -152,6 +193,8 @@ github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o= github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/import v0.2.0 h1:Ov23JW9Rnjxk06rki1Spar45bNX647HhwhAZj3flJiY= github.com/writeas/import v0.2.0/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM= +github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg= +github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ= github.com/writeas/nerds v1.0.0 h1:ZzRcCN+Sr3MWID7o/x1cr1ZbLvdpej9Y1/Ho+JKlqxo= @@ -161,10 +204,14 @@ github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD2 github.com/writeas/saturday v1.6.0/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/saturday v1.7.1 h1:lYo1EH6CYyrFObQoA9RNWHVlpZA5iYL5Opxo7PYAnZE= github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= +github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 h1:PozPZ29CQ/xt6ym/+FvIz+KvKEObSSc5ye+95zbTjVU= +github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0= github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI= +github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c h1:/aPb8WKtC+Ga/xUEcME0iX3VKBeeJ02kXCaROaZ21SE= +github.com/writeas/web-core v1.2.1-0.20200813161734-68a680d1b03c/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI= github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= @@ -173,19 +220,30 @@ golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 h1:rJm0LuqUjoDhSk2zO9ISMSToQxGz7Os2jRiOL8AWu4c= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -200,8 +258,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU= diff --git a/gopher.go b/gopher.go new file mode 100644 index 0000000..30391f1 --- /dev/null +++ b/gopher.go @@ -0,0 +1,146 @@ +/* + * Copyright © 2020 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package writefreely + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/prologic/go-gopher" + "github.com/writeas/web-core/log" +) + +func initGopher(apper Apper) { + handler := NewWFHandler(apper) + + gopher.HandleFunc("/", handler.Gopher(handleGopher)) + log.Info("Serving on gopher://localhost:%d", apper.App().Config().Server.GopherPort) + gopher.ListenAndServe(fmt.Sprintf(":%d", apper.App().Config().Server.GopherPort), nil) +} + +func handleGopher(app *App, w gopher.ResponseWriter, r *gopher.Request) error { + parts := strings.Split(r.Selector, "/") + if app.cfg.App.SingleUser { + if parts[1] != "" { + return handleGopherCollectionPost(app, w, r) + } + return handleGopherCollection(app, w, r) + } + + // Show all public collections (a gopher Reader view, essentially) + if len(parts) == 3 { + return handleGopherCollection(app, w, r) + } + + w.WriteInfo(fmt.Sprintf("Welcome to %s", app.cfg.App.SiteName)) + + colls, err := app.db.GetPublicCollections(app.cfg.App.Host) + if err != nil { + return err + } + + for _, c := range *colls { + w.WriteItem(&gopher.Item{ + Type: gopher.DIRECTORY, + Description: c.DisplayTitle(), + Selector: "/" + c.Alias + "/", + }) + } + return w.End() +} + +func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request) error { + var collAlias, slug string + var c *Collection + var err error + var baseSel = "/" + + parts := strings.Split(r.Selector, "/") + if app.cfg.App.SingleUser { + // sanity check + slug = parts[1] + if slug != "" { + return handleGopherCollectionPost(app, w, r) + } + + c, err = app.db.GetCollectionByID(1) + if err != nil { + return err + } + } else { + collAlias = parts[1] + slug = parts[2] + if slug != "" { + return handleGopherCollectionPost(app, w, r) + } + + c, err = app.db.GetCollection(collAlias) + if err != nil { + return err + } + baseSel = "/" + c.Alias + "/" + } + c.hostName = app.cfg.App.Host + + posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false) + if err != nil { + return err + } + + for _, p := range *posts { + w.WriteItem(&gopher.Item{ + Type: gopher.FILE, + Description: p.CreatedDate() + " - " + p.DisplayTitle(), + Selector: baseSel + p.Slug.String, + }) + } + return w.End() +} + +func handleGopherCollectionPost(app *App, w gopher.ResponseWriter, r *gopher.Request) error { + var collAlias, slug string + var c *Collection + var err error + + parts := strings.Split(r.Selector, "/") + if app.cfg.App.SingleUser { + slug = parts[1] + c, err = app.db.GetCollectionByID(1) + if err != nil { + return err + } + } else { + collAlias = parts[1] + slug = parts[2] + c, err = app.db.GetCollection(collAlias) + if err != nil { + return err + } + } + c.hostName = app.cfg.App.Host + + p, err := app.db.GetPost(slug, c.ID) + if err != nil { + return err + } + + b := bytes.Buffer{} + if p.Title.String != "" { + b.WriteString(p.Title.String + "\n") + } + b.WriteString(p.DisplayDate + "\n\n") + b.WriteString(p.Content) + io.Copy(w, &b) + + return w.End() +} diff --git a/handle.go b/handle.go index 4915420..1b5470f 100644 --- a/handle.go +++ b/handle.go @@ -21,6 +21,7 @@ import ( "time" "github.com/gorilla/sessions" + "github.com/prologic/go-gopher" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" @@ -64,6 +65,7 @@ func UserLevelReader(cfg *config.Config) UserLevel { type ( handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error + gopherFunc func(app *App, w gopher.ResponseWriter, r *gopher.Request) error userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) @@ -898,6 +900,24 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc { } } +func (h *Handler) Gopher(f gopherFunc) gopher.HandlerFunc { + return func(w gopher.ResponseWriter, r *gopher.Request) { + defer func() { + if e := recover(); e != nil { + log.Error("%s: %s", e, debug.Stack()) + w.WriteError("An internal error occurred") + } + log.Info("gopher: %s", r.Selector) + }() + + err := f(h.app.App(), w, r) + if err != nil { + log.Error("failed: %s", err) + w.WriteError("the page failed for some reason (see logs)") + } + } +} + func sendRedirect(w http.ResponseWriter, code int, location string) int { w.Header().Set("Location", location) w.WriteHeader(code) diff --git a/less/core.less b/less/core.less index c1cfad8..b502c5d 100644 --- a/less/core.less +++ b/less/core.less @@ -81,7 +81,7 @@ body { font-size: 1.5em; } h2 { - font-size: 1.17em; + font-size: 1.4em; } } diff --git a/less/pad.less b/less/pad.less index a132b30..91c002d 100644 --- a/less/pad.less +++ b/less/pad.less @@ -60,7 +60,7 @@ &:hover { background: @lightNavHoverBG; } - &:hover > ul { + &:hover > ul, &.open > ul { display: block; } &.selected { @@ -361,6 +361,24 @@ body#pad { z-index: 10; } +body#pad .alert { + position: fixed; + bottom: 0.25em; + left: 2em; + right: 2em; + font-size: 1.1em; + + edited-elsewhere { + &.hidden { + display: none; + } + + a { + font-weight: bold; + } + } +} + @media all and (max-height: 500px) { body#pad { textarea { @@ -425,6 +443,10 @@ body#pad { padding-left: 10%; padding-right: 10%; } + .alert { + left: 10%; + right: 10%; + } } } @media all and (min-width: 60em) { @@ -433,6 +455,10 @@ body#pad { padding-left: 15%; padding-right: 15%; } + .alert { + left: 15%; + right: 15%; + } } } @media all and (min-width: 70em) { @@ -441,6 +467,10 @@ body#pad { padding-left: 20%; padding-right: 20%; } + .alert { + left: 20%; + right: 20%; + } } } @media all and (min-width: 85em) { @@ -449,6 +479,10 @@ body#pad { padding-left: 25%; padding-right: 25%; } + .alert { + left: 25%; + right: 25%; + } } } @media all and (min-width: 105em) { @@ -457,6 +491,10 @@ body#pad { padding-left: 30%; padding-right: 30%; } + .alert { + left: 30%; + right: 30%; + } } } @media (pointer: coarse) { diff --git a/migrations/drivers.go b/migrations/drivers.go index 59fe16f..1399411 100644 --- a/migrations/drivers.go +++ b/migrations/drivers.go @@ -78,3 +78,10 @@ func (db *datastore) engine() string { } return " ENGINE = InnoDB" } + +func (db *datastore) after(colName string) string { + if db.driverName == driverSQLite { + return "" + } + return " AFTER " + colName +} diff --git a/migrations/migrations.go b/migrations/migrations.go index a0b3f25..88897fd 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -64,6 +64,8 @@ var migrations = []Migration{ New("support ActivityPub mentions", supportActivityPubMentions), // V5 -> V6 New("support oauth attach", oauthAttach), // V6 -> V7 New("support oauth via invite", oauthInvites), // V7 -> V8 (v0.12.0) + New("optimize drafts retrieval", optimizeDrafts), // V8 -> V9 + New("support post signatures", supportPostSignatures), // V9 -> V10 } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v10.go b/migrations/v10.go new file mode 100644 index 0000000..9c84a01 --- /dev/null +++ b/migrations/v10.go @@ -0,0 +1,33 @@ +/* + * Copyright © 2020 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package migrations + +func supportPostSignatures(db *datastore) error { + t, err := db.Begin() + if err != nil { + t.Rollback() + return err + } + + _, err = t.Exec(`ALTER TABLE collections ADD COLUMN post_signature ` + db.typeText() + db.collateMultiByte() + ` NULL` + db.after("script")) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/migrations/v9.go b/migrations/v9.go new file mode 100644 index 0000000..c6b832e --- /dev/null +++ b/migrations/v9.go @@ -0,0 +1,37 @@ +/* + * Copyright © 2020 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package migrations + +func optimizeDrafts(db *datastore) error { + t, err := db.Begin() + if err != nil { + t.Rollback() + return err + } + + if db.driverName == driverSQLite { + _, err = t.Exec(`CREATE INDEX key_owner_post_id ON posts (owner_id, id)`) + } else { + _, err = t.Exec(`ALTER TABLE posts ADD INDEX(owner_id, id)`) + } + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/oauth.go b/oauth.go index bb0de3e..fe9fe74 100644 --- a/oauth.go +++ b/oauth.go @@ -262,6 +262,33 @@ func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) { } } +func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) { + if app.Config().GiteaOauth.ClientID != "" { + callbackLocation := app.Config().App.Host + "/oauth/callback/gitea" + + var callbackProxy *callbackProxyClient = nil + if app.Config().GiteaOauth.CallbackProxy != "" { + callbackProxy = &callbackProxyClient{ + server: app.Config().GiteaOauth.CallbackProxyAPI, + callbackLocation: app.Config().App.Host + "/oauth/callback/gitea", + httpClient: config.DefaultHTTPClient(), + } + callbackLocation = app.Config().GiteaOauth.CallbackProxy + } + + oauthClient := giteaOauthClient{ + ClientID: app.Config().GiteaOauth.ClientID, + ClientSecret: app.Config().GiteaOauth.ClientSecret, + ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token", + InspectLocation: app.Config().GiteaOauth.Host + "/api/v1/user", + AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize", + HttpClient: config.DefaultHTTPClient(), + CallbackLocation: callbackLocation, + } + configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) + } +} + func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) { handler := &oauthHandler{ Config: app.Config(), diff --git a/oauth_gitea.go b/oauth_gitea.go new file mode 100644 index 0000000..e6e1000 --- /dev/null +++ b/oauth_gitea.go @@ -0,0 +1,114 @@ +package writefreely + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" +) + +type giteaOauthClient struct { + ClientID string + ClientSecret string + AuthLocation string + ExchangeLocation string + InspectLocation string + CallbackLocation string + HttpClient HttpClient +} + +var _ oauthClient = giteaOauthClient{} + +const ( + giteaDisplayName = "Gitea" +) + +func (c giteaOauthClient) GetProvider() string { + return "gitea" +} + +func (c giteaOauthClient) GetClientID() string { + return c.ClientID +} + +func (c giteaOauthClient) GetCallbackLocation() string { + return c.CallbackLocation +} + +func (c giteaOauthClient) buildLoginURL(state string) (string, error) { + u, err := url.Parse(c.AuthLocation) + if err != nil { + return "", err + } + q := u.Query() + q.Set("client_id", c.ClientID) + q.Set("redirect_uri", c.CallbackLocation) + q.Set("response_type", "code") + q.Set("state", state) + // q.Set("scope", "read_user") + u.RawQuery = q.Encode() + return u.String(), nil +} + +func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { + form := url.Values{} + form.Add("grant_type", "authorization_code") + form.Add("redirect_uri", c.CallbackLocation) + // form.Add("scope", "read_user") + form.Add("code", code) + req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(c.ClientID, c.ClientSecret) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to exchange code for access token") + } + + var tokenResponse TokenResponse + if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { + return nil, err + } + if tokenResponse.Error != "" { + return nil, errors.New(tokenResponse.Error) + } + return &tokenResponse, nil +} + +func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { + req, err := http.NewRequest("GET", c.InspectLocation, nil) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to inspect access token") + } + + var inspectResponse InspectResponse + if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil { + return nil, err + } + if inspectResponse.Error != "" { + return nil, errors.New(inspectResponse.Error) + } + return &inspectResponse, nil +} diff --git a/pages/login.tmpl b/pages/login.tmpl index 304df3e..9a65da2 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -17,7 +17,7 @@ input{margin-bottom:0.5em;} {{range .Flashes}}
MathJax is a JavaScript library that allows page"," authors to include mathematics within their web pages."," As a reader, you don't need to do anything to make that happen.
","Browsers: MathJax works with all modern browsers including"," Edge, Firefox, Chrome, Safari, Opera, and most mobile browsers.
","Math Menu: MathJax adds a contextual menu to equations."," Right-click or CTRL-click on any mathematics to access the menu.
",'Show Math As: These options allow you to view the formula's"," source markup (as MathML or in its original format).
","Copy to Clipboard: These options copy the formula's source markup,"," as MathML or in its original format, to the clipboard"," (in browsers that support that).
","Math Settings: These give you control over features of MathJax,"," such the size of the mathematics, and the mechanism used"," to display equations.
","Accessibility: MathJax can work with screen"," readers to make mathematics accessible to the visually impaired."," Turn on the explorer to enable generation of speech strings"," and the ability to investigate expressions interactively.
","Language: This menu lets you select the language used by MathJax"," for its menus and warning messages. (Not yet implemented in version 3.)
","Math Zoom: If you are having difficulty reading an"," equation, MathJax can enlarge it to help you see it better, or"," you can scall all the math on the page to make it larger."," Turn these features on in the Math Settings menu.
","Preferences: MathJax uses your browser's localStorage database"," to save the preferences set via this menu locally in your browser. These"," are not used to track you, and are not transferred or used remotely by"," MathJax in any way.
"].join("\n")},'www.mathjax.org'),this.mathmlCode=new i.SelectableInfo("MathJax MathML Expression",function(){if(!t.menu.mathItem)return"";var Q=t.toMML(t.menu.mathItem);return""+t.formatSource(Q)+""},""),this.originalText=new i.SelectableInfo("MathJax Original Source",function(){if(!t.menu.mathItem)return"";var Q=t.menu.mathItem.math;return'
'+t.formatSource(Q)+""},""),this.annotationText=new i.SelectableInfo("MathJax Annotation Text",function(){if(!t.menu.mathItem)return"";var Q=t.menu.annotation;return'
'+t.formatSource(Q)+""},""),this.zoomBox=new ContextMenu.Info("MathJax Zoomed Expression",function(){if(!t.menu.mathItem)return"";var Q=t.menu.mathItem.typesetRoot.cloneNode(!0);return Q.style.margin="0",'
"+this.generateContent()+""),t.write('