Merge branch 'develop' into wysiwyg
commit
61daca2b0d
|
@ -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"
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "static/js/mathjax"]
|
||||
path = static/js/mathjax
|
||||
url = https://github.com/mathjax/MathJax.git
|
1
Makefile
1
Makefile
|
@ -86,6 +86,7 @@ release : clean ui assets
|
|||
cp -r templates $(BUILDPATH)
|
||||
cp -r pages $(BUILDPATH)
|
||||
cp -r static $(BUILDPATH)
|
||||
scripts/invalidate-css.sh $(BUILDPATH)
|
||||
mkdir $(BUILDPATH)/keys
|
||||
$(MAKE) build-linux
|
||||
mv build/$(BINARY_NAME)-linux-amd64 $(BUILDPATH)/$(BINARY_NAME)
|
||||
|
|
69
README.md
69
README.md
|
@ -7,81 +7,76 @@
|
|||
<a href="https://github.com/writeas/writefreely/releases/">
|
||||
<img src="https://img.shields.io/github/release/writeas/writefreely.svg" alt="Latest release" />
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/writeas/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writeas/writefreely" alt="Go Report Card" />
|
||||
</a>
|
||||
<a href="https://travis-ci.org/writeas/writefreely">
|
||||
<img src="https://travis-ci.org/writeas/writefreely.svg" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/writeas/writefreely/releases/latest">
|
||||
<img src="https://img.shields.io/github/downloads/writeas/writefreely/total.svg" />
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/writeas/writefreely">
|
||||
<img src="https://goreportcard.com/badge/github.com/writeas/writefreely" alt="Go Report Card" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/writeas/writefreely/">
|
||||
<img src="https://img.shields.io/docker/pulls/writeas/writefreely.svg" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath.
|
||||
WriteFreely is free and open source software for building **a writing space** on the web — whether a publication, internal blog, or writing community in the fediverse.
|
||||
|
||||
It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and light enough to run on a Raspberry Pi.
|
||||

|
||||
|
||||
[Try the editor](https://write.as/new)
|
||||
[Try the writing experience](https://write.as/new)
|
||||
|
||||
[Find an instance](https://writefreely.org/instances)
|
||||
|
||||
## Features
|
||||
|
||||
* Start a blog for yourself, or host a community of writers
|
||||
* Form larger federated networks, and interact over modern protocols like ActivityPub
|
||||
* Write on a fast, dead-simple, and distraction-free editor
|
||||
* [Format text](https://howto.write.as/getting-started) with Markdown
|
||||
* [Organize posts](https://howto.write.as/organization) with hashtags
|
||||
* Create [static pages](https://howto.write.as/creating-a-static-page)
|
||||
* Publish drafts and let others proofread them by sharing a private link
|
||||
* Create multiple lightweight blogs under a single account
|
||||
* Export all data in plain text files
|
||||
* Read a stream of other posts in your writing community
|
||||
* Build more advanced apps and extensions with the [well-documented API](https://developers.write.as/docs/api/)
|
||||
* Designed around user privacy and consent
|
||||
### Made for writing
|
||||
|
||||
## Hosting
|
||||
Built on a plain, auto-saving editor, WriteFreely gives you a distraction-free writing environment. Once published, your words are front and center, and easy to read.
|
||||
|
||||
We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as Pro](https://write.as/pro) for individuals, and [Write.as for Teams](https://write.as/for/teams) for businesses. Besides saving you time and effort, both services directly fund WriteFreely development and ensure the long-term sustainability of our open source work.
|
||||
### A connected community
|
||||
|
||||
### [](https://write.as/pro)
|
||||
Start writing together, publicly or privately. Connect with other communities, whether running WriteFreely, [Plume](https://joinplu.me/), or other ActivityPub-powered software. And bring members on board from your existing platforms, thanks to our OAuth 2.0 support.
|
||||
|
||||
Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pro).
|
||||
### Intuitive organization
|
||||
|
||||
### [](https://write.as/for/teams)
|
||||
Categorize articles [with hashtags](https://writefreely.org/docs/latest/writer/hashtags), and create static pages from normal posts by [_pinning_ them](https://writefreely.org/docs/latest/writer/static) to your blog. Create draft posts and publish to multiple blogs from one account.
|
||||
|
||||
[Write.as for Teams](https://write.as/for/teams) gives your organization, business, or [open source project](https://write.as/for/open-source) a clutter-free space to share updates or proposals and build your collective knowledge. We take care of hosting, upgrades, backups, and maintenance so your team can focus on writing.
|
||||
### International
|
||||
|
||||
Blog elements are localized in 20+ languages, and WriteFreely includes first-class support for non-Latin and right-to-left (RTL) script languages.
|
||||
|
||||
### Private by default
|
||||
|
||||
WriteFreely collects minimal data, and never publicizes more than a writer consents to. Writers can seamlessly create multiple blogs from a single account for different pen names or purposes without publicly revealing their association.
|
||||
|
||||
<h2><a href="https://write.as/writefreely"><img src="https://writefreely.org/img/writeas-readme.png" height="32px" alt="Write.as" /></a></h2>
|
||||
|
||||
The quickest way to deploy WriteFreely is with [Write.as](https://write.as/writefreely), a hosted service from the team behind WriteFreely. You'll get fully-managed installation, backup, upgrades, and maintenance — and directly fund our free software work ❤️
|
||||
|
||||
[**Learn more on Write.as**](https://write.as/writefreely).
|
||||
|
||||
## Quick start
|
||||
|
||||
WriteFreely has minimal requirements to get up and running — you only need to be able to run an executable.
|
||||
WriteFreely deploys as a static binary on any platform and architecture that Go supports. Just use our built-in SQLite support, or add a MySQL database, and you'll be up and running!
|
||||
|
||||
> **Note** this is currently alpha software. We're quickly moving out of this v0.x stage, but while we're in it, there are no guarantees that this is ready for production use.
|
||||
For common platforms, start with our [pre-built binaries](https://github.com/writeas/writefreely/releases/) and head over to our [installation guide](https://writefreely.org/start) to get started.
|
||||
|
||||
To get started, head over to our [Getting Started guide](https://writefreely.org/start). For production use, jump to the [Running in Production](https://writefreely.org/start#production) section.
|
||||
### Packages
|
||||
|
||||
## Packages
|
||||
|
||||
WriteFreely is available in these package repositories:
|
||||
You can also find WriteFreely in these package repositories, thanks to our wonderful community!
|
||||
|
||||
* [Arch User Repository](https://aur.archlinux.org/packages/writefreely/)
|
||||
|
||||
## Documentation
|
||||
|
||||
Read our full [documentation on WriteFreely.org](https://writefreely.org/docs). Help us improve by contributing to the [writefreely/documentation](https://github.com/writefreely/documentation) repo.
|
||||
Read our full [documentation on WriteFreely.org](https://writefreely.org/docs) —️ and help us improve by contributing to the [writefreely/documentation](https://github.com/writefreely/documentation) repo.
|
||||
|
||||
## Development
|
||||
|
||||
Ready to hack on your site? Get started with our [developer guide](https://writefreely.org/docs/latest/developer/setup).
|
||||
|
||||
## Docker
|
||||
|
||||
Read about using Docker in the [documentation](https://writefreely.org/docs/latest/admin/docker).
|
||||
Start hacking on WriteFreely with our [developer setup guide](https://writefreely.org/docs/latest/developer/setup). For Docker support, see our [Docker guide](https://writefreely.org/docs/latest/admin/docker).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -91,4 +86,4 @@ Before contributing anything, please read our [Contributing Guide](https://githu
|
|||
|
||||
## License
|
||||
|
||||
Licensed under the AGPL.
|
||||
Copyright © 2018-2020 [A Bunch Tell LLC](https://abunchtell.com) and contributing authors. Licensed under the [AGPL](https://github.com/writeas/writefreely/blob/develop/LICENSE).
|
||||
|
|
124
account.go
124
account.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/writeas/web-core/auth"
|
||||
"github.com/writeas/web-core/data"
|
||||
"github.com/writeas/web-core/log"
|
||||
|
||||
"github.com/writeas/writefreely/author"
|
||||
"github.com/writeas/writefreely/config"
|
||||
"github.com/writeas/writefreely/page"
|
||||
|
@ -48,6 +49,7 @@ type (
|
|||
Separator template.HTML
|
||||
IsAdmin bool
|
||||
CanInvite bool
|
||||
CollAlias string
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -70,7 +72,7 @@ func canUserInvite(cfg *config.Config, isAdmin bool) bool {
|
|||
}
|
||||
|
||||
func (up *UserPage) SetMessaging(u *User) {
|
||||
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
|
||||
// up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -85,6 +87,11 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||
if app.cfg.App.DisablePasswordAuth {
|
||||
err := ErrDisabledPasswordAuth
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqJSON := IsJSON(r)
|
||||
|
||||
// Get params
|
||||
|
@ -167,11 +174,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr
|
|||
|
||||
// Log invite if needed
|
||||
if signup.InviteCode != "" {
|
||||
cu, err := app.db.GetUserForAuth(signup.Alias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = app.db.CreateInvitedUser(signup.InviteCode, cu.ID)
|
||||
err = app.db.CreateInvitedUser(signup.InviteCode, u.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -302,20 +305,18 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := &struct {
|
||||
page.StaticPage
|
||||
*OAuthButtons
|
||||
To string
|
||||
Message template.HTML
|
||||
Flashes []template.HTML
|
||||
LoginUsername string
|
||||
OauthSlack bool
|
||||
OauthWriteAs bool
|
||||
}{
|
||||
pageForReq(app, r),
|
||||
r.FormValue("to"),
|
||||
template.HTML(""),
|
||||
[]template.HTML{},
|
||||
getTempInfo(app, "login-user", r, w),
|
||||
app.Config().SlackOauth.ClientID != "",
|
||||
app.Config().WriteAsOauth.ClientID != "",
|
||||
StaticPage: pageForReq(app, r),
|
||||
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||
To: r.FormValue("to"),
|
||||
Message: template.HTML(""),
|
||||
Flashes: []template.HTML{},
|
||||
LoginUsername: getTempInfo(app, "login-user", r, w),
|
||||
}
|
||||
|
||||
if earlyError != "" {
|
||||
|
@ -390,6 +391,11 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
var err error
|
||||
var signin userCredentials
|
||||
|
||||
if app.cfg.App.DisablePasswordAuth {
|
||||
err := ErrDisabledPasswordAuth
|
||||
return err
|
||||
}
|
||||
|
||||
// Log in with one-time token if one is given
|
||||
if oneTimeToken != "" {
|
||||
log.Info("Login: Logging user in via token.")
|
||||
|
@ -488,6 +494,9 @@ func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."}
|
||||
}
|
||||
}
|
||||
if len(u.HashedPass) == 0 {
|
||||
return impart.HTTPError{http.StatusUnauthorized, "This user never set a password. Perhaps try logging in via OAuth?"}
|
||||
}
|
||||
if !auth.Authenticated(u.HashedPass, []byte(signin.Pass)) {
|
||||
return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
|
||||
}
|
||||
|
@ -832,6 +841,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
|||
Collection: c,
|
||||
Silenced: silenced,
|
||||
}
|
||||
obj.UserPage.CollAlias = c.Alias
|
||||
|
||||
showUserPage(w, "collection", obj)
|
||||
return nil
|
||||
|
@ -1011,6 +1021,7 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
|||
TopPosts: topPosts,
|
||||
Silenced: silenced,
|
||||
}
|
||||
obj.UserPage.CollAlias = c.Alias
|
||||
if app.cfg.App.Federation {
|
||||
folls, err := app.db.GetAPFollowers(c)
|
||||
if err != nil {
|
||||
|
@ -1038,18 +1049,68 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
|
||||
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||
|
||||
enableOauthSlack := app.Config().SlackOauth.ClientID != ""
|
||||
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 {
|
||||
log.Error("Unable to get oauth accounts for settings: %s", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
|
||||
}
|
||||
for idx, oauthAccount := range oauthAccounts {
|
||||
switch oauthAccount.Provider {
|
||||
case "slack":
|
||||
enableOauthSlack = false
|
||||
case "write.as":
|
||||
enableOauthWriteAs = false
|
||||
case "gitlab":
|
||||
enableOauthGitLab = false
|
||||
case "generic":
|
||||
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 || enableOauthGitea || len(oauthAccounts) > 0
|
||||
|
||||
obj := struct {
|
||||
*UserPage
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
OauthSection bool
|
||||
OauthAccounts []oauthAccountInfo
|
||||
OauthSlack bool
|
||||
OauthWriteAs bool
|
||||
OauthGitLab bool
|
||||
GitLabDisplayName string
|
||||
OauthGeneric bool
|
||||
OauthGenericDisplayName string
|
||||
OauthGitea bool
|
||||
GiteaDisplayName string
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
OauthSection: displayOauthSection,
|
||||
OauthAccounts: oauthAccounts,
|
||||
OauthSlack: enableOauthSlack,
|
||||
OauthWriteAs: enableOauthWriteAs,
|
||||
OauthGitLab: enableOauthGitLab,
|
||||
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)
|
||||
|
@ -1094,6 +1155,19 @@ func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) s
|
|||
return s
|
||||
}
|
||||
|
||||
func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
provider := r.FormValue("provider")
|
||||
clientID := r.FormValue("client_id")
|
||||
remoteUserID := r.FormValue("remote_user_id")
|
||||
|
||||
err := app.db.RemoveOauth(r.Context(), u.ID, provider, clientID, remoteUserID)
|
||||
if err != nil {
|
||||
return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
|
||||
}
|
||||
|
||||
return impart.HTTPError{Status: http.StatusFound, Message: "/me/settings"}
|
||||
}
|
||||
|
||||
func prepareUserEmail(input string, emailKey []byte) zero.String {
|
||||
email := zero.NewString("", input != "")
|
||||
if len(input) > 0 {
|
||||
|
|
|
@ -160,6 +160,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
|
|||
pp.Collection = res
|
||||
o := pp.ActivityObject(app)
|
||||
a := activitystreams.NewCreateActivity(o)
|
||||
a.Context = nil
|
||||
ocp.OrderedItems = append(ocp.OrderedItems, *a)
|
||||
}
|
||||
|
||||
|
@ -396,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
|
||||
}
|
||||
|
||||
|
@ -491,7 +494,7 @@ func makeActivityPost(hostName string, p *activitystreams.Person, url string, m
|
|||
|
||||
r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
||||
r.Header.Add("Content-Type", "application/activity+json")
|
||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
||||
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
|
@ -541,7 +544,7 @@ func resolveIRI(hostName, url string) ([]byte, error) {
|
|||
|
||||
r, _ := http.NewRequest("GET", url, nil)
|
||||
r.Header.Add("Accept", "application/activity+json")
|
||||
r.Header.Set("User-Agent", "Go ("+serverSoftware+"/"+softwareVer+"; +"+hostName+")")
|
||||
r.Header.Set("User-Agent", ServerUserAgent(hostName))
|
||||
|
||||
if debugging {
|
||||
dump, err := httputil.DumpRequestOut(r, true)
|
||||
|
@ -696,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)
|
||||
|
@ -708,7 +715,8 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
|
|||
|
||||
func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
||||
u := RemoteUser{ActorID: actorID}
|
||||
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &u.Handle)
|
||||
var handle sql.NullString
|
||||
err := app.db.QueryRow("SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &handle)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
|
||||
|
@ -717,6 +725,8 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
u.Handle = handle.String
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
|
|
25
app.go
25
app.go
|
@ -56,7 +56,7 @@ var (
|
|||
debugging bool
|
||||
|
||||
// Software version can be set from git env using -ldflags
|
||||
softwareVer = "0.11.2"
|
||||
softwareVer = "0.12.0"
|
||||
|
||||
// DEPRECATED VARS
|
||||
isSingleUser bool
|
||||
|
@ -221,6 +221,10 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
return handleViewPad(app, w, r)
|
||||
}
|
||||
|
||||
if app.cfg.App.Private {
|
||||
return viewLogin(app, w, r)
|
||||
}
|
||||
|
||||
if land := app.cfg.App.LandingPath(); land != "/" {
|
||||
return impart.HTTPError{http.StatusFound, land}
|
||||
}
|
||||
|
@ -234,6 +238,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
p := struct {
|
||||
page.StaticPage
|
||||
*OAuthButtons
|
||||
Flashes []template.HTML
|
||||
Banner template.HTML
|
||||
Content template.HTML
|
||||
|
@ -241,6 +246,7 @@ func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error {
|
|||
ForcedLanding bool
|
||||
}{
|
||||
StaticPage: pageForReq(app, r),
|
||||
OAuthButtons: NewOAuthButtons(app.Config()),
|
||||
ForcedLanding: forceLanding,
|
||||
}
|
||||
|
||||
|
@ -409,6 +415,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 == "" {
|
||||
|
@ -744,7 +755,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 {
|
||||
|
@ -881,3 +892,13 @@ func adminInitDatabase(app *App) error {
|
|||
log.Info("Done.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerUserAgent returns a User-Agent string to use in external requests. The
|
||||
// hostName parameter may be left empty.
|
||||
func ServerUserAgent(hostName string) string {
|
||||
hostUAStr := ""
|
||||
if hostName != "" {
|
||||
hostUAStr = "; +" + hostName
|
||||
}
|
||||
return "Go (" + serverSoftware + "/" + softwareVer + hostUAStr + ")"
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ password = changeme
|
|||
database = writefreely
|
||||
host = db
|
||||
port = 3306
|
||||
tls = false
|
||||
|
||||
[app]
|
||||
site_name = WriteFreely Example Blog!
|
||||
|
|
|
@ -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 {
|
||||
|
@ -69,6 +72,24 @@ type (
|
|||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
}
|
||||
|
||||
GitlabOauthCfg 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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
SlackOauthCfg struct {
|
||||
ClientID string `ini:"client_id"`
|
||||
ClientSecret string `ini:"client_secret"`
|
||||
|
@ -77,6 +98,19 @@ type (
|
|||
CallbackProxyAPI string `ini:"callback_proxy_api"`
|
||||
}
|
||||
|
||||
GenericOauthCfg 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"`
|
||||
TokenEndpoint string `ini:"token_endpoint"`
|
||||
InspectEndpoint string `ini:"inspect_endpoint"`
|
||||
AuthEndpoint string `ini:"auth_endpoint"`
|
||||
AllowDisconnect bool `ini:"allow_disconnect"`
|
||||
}
|
||||
|
||||
// AppCfg holds values that affect how the application functions
|
||||
AppCfg struct {
|
||||
SiteName string `ini:"site_name"`
|
||||
|
@ -119,6 +153,9 @@ type (
|
|||
|
||||
// Check for Updates
|
||||
UpdateChecks bool `ini:"update_checks"`
|
||||
|
||||
// Disable password authentication if use only Oauth
|
||||
DisablePasswordAuth bool `ini:"disable_password_auth"`
|
||||
}
|
||||
|
||||
// Config holds the complete configuration for running a writefreely instance
|
||||
|
@ -128,6 +165,9 @@ type (
|
|||
App AppCfg `ini:"app"`
|
||||
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
|
||||
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
|
||||
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
|
||||
GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"`
|
||||
GenericOauth GenericOauthCfg `ini:"oauth.generic"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -183,6 +223,16 @@ func (ac *AppCfg) LandingPath() string {
|
|||
return ac.Landing
|
||||
}
|
||||
|
||||
func (ac AppCfg) SignupPath() string {
|
||||
if !ac.OpenRegistration {
|
||||
return ""
|
||||
}
|
||||
if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") {
|
||||
return "/signup"
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Load reads the given configuration file, then parses and returns it as a Config.
|
||||
func Load(fname string) (*Config, error) {
|
||||
if fname == "" {
|
||||
|
|
|
@ -356,7 +356,7 @@ func Configure(fname string, configSections string) (*SetupData, error) {
|
|||
if data.Config.App.Federation {
|
||||
selPrompt = promptui.Select{
|
||||
Templates: selTmpls,
|
||||
Label: "Federation usage stats",
|
||||
Label: "Usage stats (active users, posts)",
|
||||
Items: []string{"Public", "Private"},
|
||||
}
|
||||
_, fedStatsType, err := selPrompt.Run()
|
||||
|
|
|
@ -22,3 +22,7 @@ func (db *datastore) isDuplicateKeyErr(err error) bool {
|
|||
func (db *datastore) isIgnorableError(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *datastore) isHighLoadError(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -40,3 +40,13 @@ func (db *datastore) isIgnorableError(err error) bool {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *datastore) isHighLoadError(err error) bool {
|
||||
if db.driverName == driverMySQL {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// +build sqlite,!wflib
|
||||
|
||||
/*
|
||||
* Copyright © 2019 A Bunch Tell LLC.
|
||||
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -60,3 +60,13 @@ func (db *datastore) isIgnorableError(err error) bool {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *datastore) isHighLoadError(err error) bool {
|
||||
if db.driverName == driverMySQL {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
131
database.go
131
database.go
|
@ -14,6 +14,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/writeas/web-core/silobridge"
|
||||
wf_db "github.com/writeas/writefreely/db"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -39,6 +40,8 @@ import (
|
|||
const (
|
||||
mySQLErrDuplicateKey = 1062
|
||||
mySQLErrCollationMix = 1267
|
||||
mySQLErrTooManyConns = 1040
|
||||
mySQLErrMaxUserConns = 1203
|
||||
|
||||
driverMySQL = "mysql"
|
||||
driverSQLite = "sqlite3"
|
||||
|
@ -130,8 +133,10 @@ type writestore interface {
|
|||
|
||||
GetIDForRemoteUser(context.Context, string, string, string) (int64, error)
|
||||
RecordRemoteUserID(context.Context, int64, string, string, string, string) error
|
||||
ValidateOAuthState(context.Context, string) (string, string, error)
|
||||
GenerateOAuthState(context.Context, string, string) (string, error)
|
||||
ValidateOAuthState(context.Context, string) (string, string, int64, string, error)
|
||||
GenerateOAuthState(context.Context, string, string, int64, string) (string, error)
|
||||
GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error)
|
||||
RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error
|
||||
|
||||
DatabaseInitialized() bool
|
||||
}
|
||||
|
@ -174,6 +179,7 @@ func (db *datastore) dateSub(l int, unit string) string {
|
|||
return fmt.Sprintf("DATE_SUB(NOW(), INTERVAL %d %s)", l, unit)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user in the database from the given User, UPDATING it in the process with the user's ID.
|
||||
func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string) error {
|
||||
if db.PostIDExists(u.Username) {
|
||||
return impart.HTTPError{http.StatusConflict, "Invalid collection name."}
|
||||
|
@ -786,19 +792,22 @@ 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."}
|
||||
case db.isHighLoadError(err):
|
||||
return nil, ErrUnavailable
|
||||
case err != nil:
|
||||
log.Error("Failed selecting from collections: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
c.StyleSheet = styleSheet.String
|
||||
c.Script = script.String
|
||||
c.Signature = signature.String
|
||||
c.Format = format.String
|
||||
c.Public = c.IsPublic()
|
||||
|
||||
|
@ -842,7 +851,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}
|
||||
|
@ -1143,6 +1153,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())
|
||||
|
@ -1207,6 +1218,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())
|
||||
|
@ -1583,6 +1595,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[
|
|||
break
|
||||
}
|
||||
p.extractData()
|
||||
p.augmentContent(&coll.Collection)
|
||||
|
||||
pp := p.processPost()
|
||||
pp.Collection = coll
|
||||
|
@ -1633,6 +1646,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{}
|
||||
|
||||
|
@ -2016,7 +2063,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
|
||||
|
@ -2510,20 +2557,26 @@ func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
|
|||
return &t, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GenerateOAuthState(ctx context.Context, provider, clientID string) (string, error) {
|
||||
func (db *datastore) GenerateOAuthState(ctx context.Context, provider string, clientID string, attachUser int64, inviteCode string) (string, error) {
|
||||
state := store.Generate62RandomString(24)
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at) VALUES (?, ?, ?, FALSE, NOW())", state, provider, clientID)
|
||||
attachUserVal := sql.NullInt64{Valid: attachUser > 0, Int64: attachUser}
|
||||
inviteCodeVal := sql.NullString{Valid: inviteCode != "", String: inviteCode}
|
||||
_, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at, attach_user_id, invite_code) VALUES (?, ?, ?, FALSE, "+db.now()+", ?, ?)", state, provider, clientID, attachUserVal, inviteCodeVal)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to record oauth client state: %w", err)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (db *datastore) ValidateOAuthState(ctx context.Context, state string) (string, string, error) {
|
||||
func (db *datastore) ValidateOAuthState(ctx context.Context, state string) (string, string, int64, string, error) {
|
||||
var provider string
|
||||
var clientID string
|
||||
var attachUserID sql.NullInt64
|
||||
var inviteCode sql.NullString
|
||||
err := wf_db.RunTransactionWithOptions(ctx, db.DB, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error {
|
||||
err := tx.QueryRow("SELECT provider, client_id FROM oauth_client_states WHERE state = ? AND used = FALSE", state).Scan(&provider, &clientID)
|
||||
err := tx.
|
||||
QueryRowContext(ctx, "SELECT provider, client_id, attach_user_id, invite_code FROM oauth_client_states WHERE state = ? AND used = FALSE", state).
|
||||
Scan(&provider, &clientID, &attachUserID, &inviteCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -2542,9 +2595,9 @@ func (db *datastore) ValidateOAuthState(ctx context.Context, state string) (stri
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", nil
|
||||
return "", "", 0, "", nil
|
||||
}
|
||||
return provider, clientID, nil
|
||||
return provider, clientID, attachUserID.Int64, inviteCode.String, nil
|
||||
}
|
||||
|
||||
func (db *datastore) RecordRemoteUserID(ctx context.Context, localUserID int64, remoteUserID, provider, clientID, accessToken string) error {
|
||||
|
@ -2573,6 +2626,35 @@ func (db *datastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provi
|
|||
return userID, nil
|
||||
}
|
||||
|
||||
type oauthAccountInfo struct {
|
||||
Provider string
|
||||
ClientID string
|
||||
RemoteUserID string
|
||||
DisplayName string
|
||||
AllowDisconnect bool
|
||||
}
|
||||
|
||||
func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) {
|
||||
rows, err := db.QueryContext(ctx, "SELECT provider, client_id, remote_user_id FROM oauth_users WHERE user_id = ? ", userID)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from oauth_users: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user oauth accounts."}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []oauthAccountInfo
|
||||
for rows.Next() {
|
||||
info := oauthAccountInfo{}
|
||||
err = rows.Scan(&info.Provider, &info.ClientID, &info.RemoteUserID)
|
||||
if err != nil {
|
||||
log.Error("Failed scanning GetAllUsers() row: %v", err)
|
||||
break
|
||||
}
|
||||
records = append(records, info)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// DatabaseInitialized returns whether or not the current datastore has been
|
||||
// initialized with the correct schema.
|
||||
// Currently, it checks to see if the `users` table exists.
|
||||
|
@ -2595,6 +2677,11 @@ func (db *datastore) DatabaseInitialized() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (db *datastore) RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error {
|
||||
_, err := db.ExecContext(ctx, `DELETE FROM oauth_users WHERE user_id = ? AND provider = ? AND client_id = ? AND remote_user_id = ?`, userID, provider, clientID, remoteUserID)
|
||||
return err
|
||||
}
|
||||
|
||||
func stringLogln(log *string, s string, v ...interface{}) {
|
||||
*log += fmt.Sprintf(s+"\n", v...)
|
||||
}
|
||||
|
@ -2605,7 +2692,19 @@ 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
|
||||
|
@ -2617,21 +2716,21 @@ func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string,
|
|||
if errRemoteUser == nil {
|
||||
_, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI)
|
||||
if err != nil {
|
||||
log.Error("Can't update handle (" + handle + ") in database for user " + actorIRI)
|
||||
log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI)
|
||||
}
|
||||
} else {
|
||||
// this probably means we don't have the user in the table so let's try to insert it
|
||||
// here we need to ask the server for the inboxes
|
||||
remoteActor, err := activityserve.NewRemoteActor(actorIRI)
|
||||
if err != nil {
|
||||
log.Error("Couldn't fetch remote actor", err)
|
||||
log.Error("Couldn't fetch remote actor: %v", err)
|
||||
}
|
||||
if debugging {
|
||||
log.Info("%s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle)
|
||||
}
|
||||
_, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, handle) VALUES(?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle)
|
||||
if err != nil {
|
||||
log.Error("Can't insert remote user in database", err)
|
||||
log.Error("Couldn't insert remote user: %v", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ func TestOAuthDatastore(t *testing.T) {
|
|||
driverName: "",
|
||||
}
|
||||
|
||||
state, err := ds.GenerateOAuthState(ctx, "test", "development")
|
||||
state, err := ds.GenerateOAuthState(ctx, "test", "development", 0, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, state, 24)
|
||||
|
||||
countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = false", state)
|
||||
|
||||
_, _, err = ds.ValidateOAuthState(ctx, state)
|
||||
_, _, _, _, err = ds.ValidateOAuthState(ctx, state)
|
||||
assert.NoError(t, err)
|
||||
|
||||
countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = true", state)
|
||||
|
|
26
db/create.go
26
db/create.go
|
@ -1,3 +1,13 @@
|
|||
/*
|
||||
* Copyright © 2019-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 db
|
||||
|
||||
import (
|
||||
|
@ -139,6 +149,15 @@ func (c *Column) SetDefault(value string) *Column {
|
|||
return c
|
||||
}
|
||||
|
||||
func (c *Column) SetDefaultCurrentTimestamp() *Column {
|
||||
def := "NOW()"
|
||||
if c.Dialect == DialectSQLite {
|
||||
def = "CURRENT_TIMESTAMP"
|
||||
}
|
||||
c.Default = OptionalString{Set: true, Value: def}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Column) SetType(t ColumnType) *Column {
|
||||
c.Type = t
|
||||
return c
|
||||
|
@ -168,7 +187,11 @@ func (c *Column) String() (string, error) {
|
|||
|
||||
if c.Default.Set {
|
||||
str.WriteString(" DEFAULT ")
|
||||
str.WriteString(c.Default.Value)
|
||||
val := c.Default.Value
|
||||
if val == "" {
|
||||
val = "''"
|
||||
}
|
||||
str.WriteString(val)
|
||||
}
|
||||
|
||||
if c.PrimaryKey {
|
||||
|
@ -241,4 +264,3 @@ func (b *CreateTableSqlBuilder) ToSQL() (string, error) {
|
|||
|
||||
return str.String(), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,8 @@ var (
|
|||
ErrInternalGeneral = impart.HTTPError{http.StatusInternalServerError, "The humans messed something up. They've been notified."}
|
||||
ErrInternalCookieSession = impart.HTTPError{http.StatusInternalServerError, "Could not get cookie session."}
|
||||
|
||||
ErrUnavailable = impart.HTTPError{http.StatusServiceUnavailable, "Service temporarily unavailable due to high load."}
|
||||
|
||||
ErrCollectionNotFound = impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."}
|
||||
ErrCollectionGone = impart.HTTPError{http.StatusGone, "This blog was unpublished."}
|
||||
ErrCollectionPageNotFound = impart.HTTPError{http.StatusNotFound, "Collection page doesn't exist."}
|
||||
|
@ -50,6 +52,8 @@ var (
|
|||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||
|
||||
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||
|
||||
ErrDisabledPasswordAuth = impart.HTTPError{http.StatusForbidden, "Password authentication is disabled."}
|
||||
)
|
||||
|
||||
// Post operation errors
|
||||
|
|
48
go.mod
48
go.mod
|
@ -3,60 +3,58 @@ module github.com/writeas/writefreely
|
|||
require (
|
||||
github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
|
||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d // indirect
|
||||
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/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/gorilla/feeds v1.1.0
|
||||
github.com/gorilla/mux v1.7.0
|
||||
github.com/gorilla/schema v1.0.2
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gorilla/schema v1.2.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/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/manifoldco/promptui v0.7.0
|
||||
github.com/mattn/go-sqlite3 v1.14.2
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
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-20191115095800-dd6d19cc8b89
|
||||
github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2
|
||||
github.com/writeas/go-webfinger v1.1.0
|
||||
github.com/writeas/httpsig v1.0.0
|
||||
github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d
|
||||
github.com/writeas/import v0.2.0
|
||||
github.com/writeas/impart v1.1.1
|
||||
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.41.0
|
||||
gopkg.in/ini.v1 v1.57.0
|
||||
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect
|
||||
)
|
||||
|
||||
|
|
107
go.sum
107
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-20180620154129-376d45073b49 h1:jWNY1NDg6a/c8RSXkai7IX6UOhir0LD39I4Dukg+4Ks=
|
||||
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ=
|
||||
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=
|
||||
|
@ -27,52 +33,70 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
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=
|
||||
github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d h1:+uoOvOnNDgsYbWtAij4xP6Rgir3eJGjocFPxBJETU/U=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8=
|
||||
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=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo=
|
||||
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 h1:WD8iJ37bRNwvETMfVTusVSAi0WdXTpfNVGY2aHycNKY=
|
||||
github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
|
||||
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg=
|
||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
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/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
|
||||
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
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=
|
||||