Browse Source

Merge branch 'develop' into T713-oauth-account-management

pull/243/head
Matt Baer 4 years ago
parent
commit
f846cada4b
49 changed files with 830 additions and 324 deletions
  1. +16
    -6
      Makefile
  2. +14
    -14
      account.go
  3. +87
    -22
      activitypub.go
  4. +6
    -2
      admin.go
  5. +47
    -1
      app.go
  6. +2
    -1
      author/author.go
  7. +11
    -2
      cmd/writefreely/main.go
  8. +28
    -14
      collections.go
  9. +13
    -1
      database-no-sqlite.go
  10. +12
    -0
      database-sqlite.go
  11. +138
    -53
      database.go
  12. +5
    -4
      errors.go
  13. +2
    -2
      feed.go
  14. +9
    -8
      go.mod
  15. +26
    -20
      go.sum
  16. +9
    -2
      invites.go
  17. +7
    -6
      migrations/migrations.go
  18. +23
    -30
      migrations/v6.go
  19. +36
    -0
      migrations/v7.go
  20. +33
    -21
      oauth_signup.go
  21. +11
    -1
      oauth_slack.go
  22. +9
    -9
      pad.go
  23. +66
    -10
      pages/signup-oauth.tmpl
  24. +3
    -0
      postrender.go
  25. +59
    -32
      posts.go
  26. +8
    -5
      routes.go
  27. +17
    -5
      scripts/upgrade-server.sh
  28. +16
    -0
      static/js/localdate.js
  29. +3
    -3
      templates.go
  30. +4
    -3
      templates/chorus-collection-post.tmpl
  31. +3
    -2
      templates/chorus-collection.tmpl
  32. +4
    -3
      templates/collection-post.tmpl
  33. +3
    -2
      templates/collection-tags.tmpl
  34. +3
    -2
      templates/collection.tmpl
  35. +1
    -1
      templates/edit-meta.tmpl
  36. +2
    -2
      templates/include/posts.tmpl
  37. +2
    -2
      templates/pad.tmpl
  38. +2
    -2
      templates/post.tmpl
  39. +3
    -3
      templates/read.tmpl
  40. +5
    -5
      templates/user/admin/view-user.tmpl
  41. +10
    -6
      templates/user/articles.tmpl
  42. +2
    -2
      templates/user/collection.tmpl
  43. +2
    -2
      templates/user/collections.tmpl
  44. +4
    -1
      templates/user/import.tmpl
  45. +1
    -1
      templates/user/include/silenced.tmpl
  46. +6
    -3
      templates/user/invite.tmpl
  47. +2
    -2
      templates/user/settings.tmpl
  48. +2
    -2
      templates/user/stats.tmpl
  49. +53
    -4
      webfinger.go

+ 16
- 6
Makefile View File

@@ -25,31 +25,37 @@ build-no-sqlite: assets-no-sqlite deps-no-sqlite


build-linux: deps build-linux: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/karalabe/xgo; \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely


build-windows: deps build-windows: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/karalabe/xgo; \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely


build-darwin: deps build-darwin: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/karalabe/xgo; \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely


build-arm6: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi
xgo --targets=linux/arm-6, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely

build-arm7: deps build-arm7: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/karalabe/xgo; \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely


build-arm64: deps build-arm64: deps
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GOGET) -u github.com/karalabe/xgo; \
$(GOGET) -u src.techknowlogick.com/xgo; \
fi fi
xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely xgo --targets=linux/arm64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely


@@ -85,6 +91,10 @@ release : clean ui assets
mv build/$(BINARY_NAME)-linux-amd64 $(BUILDPATH)/$(BINARY_NAME) mv build/$(BINARY_NAME)-linux-amd64 $(BUILDPATH)/$(BINARY_NAME)
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(BINARY_NAME) tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(BINARY_NAME)
rm $(BUILDPATH)/$(BINARY_NAME) rm $(BUILDPATH)/$(BINARY_NAME)
$(MAKE) build-arm6
mv build/$(BINARY_NAME)-linux-arm-6 $(BUILDPATH)/$(BINARY_NAME)
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm6.tar.gz -C build $(BINARY_NAME)
rm $(BUILDPATH)/$(BINARY_NAME)
$(MAKE) build-arm7 $(MAKE) build-arm7
mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(BINARY_NAME) mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(BINARY_NAME)
tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz -C build $(BINARY_NAME) tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_arm7.tar.gz -C build $(BINARY_NAME)
@@ -145,7 +155,7 @@ $(TMPBIN)/go-bindata: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/go-bindata github.com/jteeuwen/go-bindata/go-bindata $(GOBUILD) -o $(TMPBIN)/go-bindata github.com/jteeuwen/go-bindata/go-bindata


$(TMPBIN)/xgo: deps $(TMPBIN) $(TMPBIN)/xgo: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo
$(GOBUILD) -o $(TMPBIN)/xgo src.techknowlogick.com/xgo


ci-assets : $(TMPBIN)/go-bindata ci-assets : $(TMPBIN)/go-bindata
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql $(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql


+ 14
- 14
account.go View File

@@ -746,7 +746,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
log.Error("unable to fetch collections: %v", err) log.Error("unable to fetch collections: %v", err)
} }


suspended, err := app.db.IsUserSuspended(u.ID)
silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
log.Error("view articles: %v", err) log.Error("view articles: %v", err)
} }
@@ -754,12 +754,12 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
*UserPage *UserPage
AnonymousPosts *[]PublicPost AnonymousPosts *[]PublicPost
Collections *[]Collection Collections *[]Collection
Suspended bool
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
AnonymousPosts: p, AnonymousPosts: p,
Collections: c, Collections: c,
Suspended: suspended,
Silenced: silenced,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
@@ -781,7 +781,7 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
uc, _ := app.db.GetUserCollectionCount(u.ID) uc, _ := app.db.GetUserCollectionCount(u.ID)
// TODO: handle any errors // TODO: handle any errors


suspended, err := app.db.IsUserSuspended(u.ID)
silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
log.Error("view collections %v", err) log.Error("view collections %v", err)
return fmt.Errorf("view collections: %v", err) return fmt.Errorf("view collections: %v", err)
@@ -793,13 +793,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
UsedCollections, TotalCollections int UsedCollections, TotalCollections int


NewBlogsDisabled bool NewBlogsDisabled bool
Suspended bool
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
Collections: c, Collections: c,
UsedCollections: int(uc), UsedCollections: int(uc),
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
Suspended: suspended,
Silenced: silenced,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
showUserPage(w, "collections", d) showUserPage(w, "collections", d)
@@ -817,7 +817,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
return ErrCollectionNotFound return ErrCollectionNotFound
} }


suspended, err := app.db.IsUserSuspended(u.ID)
silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
log.Error("view edit collection %v", err) log.Error("view edit collection %v", err)
return fmt.Errorf("view edit collection: %v", err) return fmt.Errorf("view edit collection: %v", err)
@@ -826,11 +826,11 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
obj := struct { obj := struct {
*UserPage *UserPage
*Collection *Collection
Suspended bool
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
Collection: c, Collection: c,
Suspended: suspended,
Silenced: silenced,
} }


showUserPage(w, "collection", obj) showUserPage(w, "collection", obj)
@@ -992,7 +992,7 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
titleStats = c.DisplayTitle() + " " titleStats = c.DisplayTitle() + " "
} }


suspended, err := app.db.IsUserSuspended(u.ID)
silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
log.Error("view stats: %v", err) log.Error("view stats: %v", err)
return err return err
@@ -1003,13 +1003,13 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
Collection *Collection Collection *Collection
TopPosts *[]PublicPost TopPosts *[]PublicPost
APFollowers int APFollowers int
Suspended bool
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
VisitsBlog: alias, VisitsBlog: alias,
Collection: c, Collection: c,
TopPosts: topPosts, TopPosts: topPosts,
Suspended: suspended,
Silenced: silenced,
} }
if app.cfg.App.Federation { if app.cfg.App.Federation {
folls, err := app.db.GetAPFollowers(c) folls, err := app.db.GetAPFollowers(c)
@@ -1062,7 +1062,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
Email string Email string
HasPass bool HasPass bool
IsLogOut bool IsLogOut bool
Suspended bool
Silenced bool
OauthSection bool OauthSection bool
OauthAccounts []oauthAccountInfo OauthAccounts []oauthAccountInfo
OauthSlack bool OauthSlack bool
@@ -1072,7 +1072,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
Email: fullUser.EmailClear(app.keys), Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet, HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1", IsLogOut: r.FormValue("logout") == "1",
Suspended: fullUser.IsSilenced(),
Silenced: fullUser.IsSilenced(),
OauthSection: displayOauthSection, OauthSection: displayOauthSection,
OauthAccounts: oauthAccounts, OauthAccounts: oauthAccounts,
OauthSlack: enableOauthSlack, OauthSlack: enableOauthSlack,


+ 87
- 22
activitypub.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -37,6 +37,8 @@ import (
const ( const (
// TODO: delete. don't use this! // TODO: delete. don't use this!
apCustomHandleDefault = "blog" apCustomHandleDefault = "blog"

apCacheTime = time.Minute
) )


type RemoteUser struct { type RemoteUser struct {
@@ -44,6 +46,7 @@ type RemoteUser struct {
ActorID string ActorID string
Inbox string Inbox string
SharedInbox string SharedInbox string
Handle string
} }


func (ru *RemoteUser) AsPerson() *activitystreams.Person { func (ru *RemoteUser) AsPerson() *activitystreams.Person {
@@ -62,6 +65,12 @@ func (ru *RemoteUser) AsPerson() *activitystreams.Person {
} }
} }


func activityPubClient() *http.Client {
return &http.Client{
Timeout: 15 * time.Second,
}
}

func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)


@@ -80,18 +89,19 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection activities: %v", err) log.Error("fetch collection activities: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


p := c.PersonObject() p := c.PersonObject()


setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, p, http.StatusOK) return impart.RenderActivityJSON(w, p, http.StatusOK)
} }


@@ -113,12 +123,12 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection outbox: %v", err) log.Error("fetch collection outbox: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host
@@ -148,11 +158,12 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false) posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false)
for _, pp := range *posts { for _, pp := range *posts {
pp.Collection = res pp.Collection = res
o := pp.ActivityObject(app.cfg)
o := pp.ActivityObject(app)
a := activitystreams.NewCreateActivity(o) a := activitystreams.NewCreateActivity(o)
ocp.OrderedItems = append(ocp.OrderedItems, *a) ocp.OrderedItems = append(ocp.OrderedItems, *a)
} }


setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }


@@ -174,12 +185,12 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection followers: %v", err) log.Error("fetch collection followers: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host
@@ -207,6 +218,7 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID) ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID)
} }
*/ */
setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }


@@ -228,12 +240,12 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection following: %v", err) log.Error("fetch collection following: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host
@@ -251,6 +263,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
// Return outbox page // Return outbox page
ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p) ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p)
ocp.OrderedItems = []interface{}{} ocp.OrderedItems = []interface{}{}
setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }


@@ -270,12 +283,12 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
// TODO: return Reject? // TODO: return Reject?
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: %v", err) log.Error("fetch collection inbox: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host
@@ -382,6 +395,11 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
} }


go func() { go func() {
if to == nil {
log.Error("No to! %v", err)
return
}

time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
am, err := a.Serialize() am, err := a.Serialize()
if err != nil { if err != nil {
@@ -390,10 +408,6 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
} }
am["@context"] = []string{activitystreams.Namespace} am["@context"] = []string{activitystreams.Namespace}


if to == nil {
log.Error("No to! %v", err)
return
}
err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am) err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am)
if err != nil { if err != nil {
log.Error("Unable to make activity POST: %v", err) log.Error("Unable to make activity POST: %v", err)
@@ -502,7 +516,7 @@ func makeActivityPost(hostName string, p *activitystreams.Person, url string, m
} }
} }


resp, err := http.DefaultClient.Do(r)
resp, err := activityPubClient().Do(r)
if err != nil { if err != nil {
return err return err
} }
@@ -538,7 +552,7 @@ func resolveIRI(hostName, url string) ([]byte, error) {
} }
} }


resp, err := http.DefaultClient.Do(r)
resp, err := activityPubClient().Do(r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -564,7 +578,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
} }
p.Collection.hostName = app.cfg.App.Host p.Collection.hostName = app.cfg.App.Host
actor := p.Collection.PersonObject(collID) actor := p.Collection.PersonObject(collID)
na := p.ActivityObject(app.cfg)
na := p.ActivityObject(app)


// Add followers // Add followers
p.Collection.ID = collID p.Collection.ID = collID
@@ -610,7 +624,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
} }
} }
actor := p.Collection.PersonObject(collID) actor := p.Collection.PersonObject(collID)
na := p.ActivityObject(app.cfg)
na := p.ActivityObject(app)


// Add followers // Add followers
p.Collection.ID = collID p.Collection.ID = collID
@@ -628,18 +642,25 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
inbox = f.Inbox inbox = f.Inbox
} }
if _, ok := inboxes[inbox]; ok { if _, ok := inboxes[inbox]; ok {
// check if we're already sending to this shared inbox
inboxes[inbox] = append(inboxes[inbox], f.ActorID) inboxes[inbox] = append(inboxes[inbox], f.ActorID)
} else { } else {
// add the new shared inbox to the list
inboxes[inbox] = []string{f.ActorID} inboxes[inbox] = []string{f.ActorID}
} }
} }


var activity *activitystreams.Activity
// for each one of the shared inboxes
for si, instFolls := range inboxes { for si, instFolls := range inboxes {
// add all followers from that instance
// to the CC field
na.CC = []string{} na.CC = []string{}
for _, f := range instFolls { for _, f := range instFolls {
na.CC = append(na.CC, f) na.CC = append(na.CC, f)
} }
var activity *activitystreams.Activity
// create a new "Create" activity
// with our article as object
if isUpdate { if isUpdate {
activity = activitystreams.NewUpdateActivity(na) activity = activitystreams.NewUpdateActivity(na)
} else { } else {
@@ -647,17 +668,42 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
activity.To = na.To activity.To = na.To
activity.CC = na.CC activity.CC = na.CC
} }
// and post it to that sharedInbox
err = makeActivityPost(app.cfg.App.Host, actor, si, activity) err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
if err != nil { if err != nil {
log.Error("Couldn't post! %v", err) log.Error("Couldn't post! %v", err)
} }
} }

// re-create the object so that the CC list gets reset and has
// the mentioned users. This might seem wasteful but the code is
// cleaner than adding the mentioned users to CC here instead of
// in p.ActivityObject()
na = p.ActivityObject(app)
for _, tag := range na.Tag {
if tag.Type == "Mention" {
activity = activitystreams.NewCreateActivity(na)
activity.To = na.To
activity.CC = na.CC
// This here might be redundant in some cases as we might have already
// sent this to the sharedInbox of this instance above, but we need too
// much logic to catch this at the expense of the odd extra request.
// 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)
err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity)
if err != nil {
log.Error("Couldn't post! %v", err)
}
}
}

return nil return nil
} }


func getRemoteUser(app *App, actorID string) (*RemoteUser, error) { func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
u := RemoteUser{ActorID: actorID} u := RemoteUser{ActorID: actorID}
err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
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)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."} return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."}
@@ -669,6 +715,21 @@ func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
return &u, nil return &u, nil
} }


// getRemoteUserFromHandle retrieves the profile page of a remote user
// from the @user@server.tld handle
func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) {
u := RemoteUser{Handle: handle}
err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox)
switch {
case err == sql.ErrNoRows:
return nil, ErrRemoteUserNotFound
case err != nil:
log.Error("Couldn't get remote user %s: %v", handle, err)
return nil, err
}
return &u, nil
}

func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) { func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
log.Info("Fetching actor %s locally", actorIRI) log.Info("Fetching actor %s locally", actorIRI)
actor := &activitystreams.Person{} actor := &activitystreams.Person{}
@@ -743,3 +804,7 @@ func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error {


return nil return nil
} }

func setCacheControl(w http.ResponseWriter, ttl time.Duration) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", ttl.Seconds()))
}

+ 6
- 2
admin.go View File

@@ -187,7 +187,11 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
var err error var err error
p.User, err = app.db.GetUserForAuth(username) p.User, err = app.db.GetUserForAuth(username)
if err != nil { if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
if err == ErrUserNotFound {
return err
}
log.Error("Could not get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
} }


flashes, _ := getSessionFlashes(app, w, r, nil) flashes, _ := getSessionFlashes(app, w, r, nil)
@@ -259,7 +263,7 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht
err = app.db.SetUserStatus(user.ID, UserSilenced) err = app.db.SetUserStatus(user.ID, UserSilenced)
} }
if err != nil { if err != nil {
log.Error("toggle user suspended: %v", err)
log.Error("toggle user silenced: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v", err)} return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v", err)}
} }
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}


+ 47
- 1
app.go View File

@@ -30,7 +30,7 @@ import (
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/writeas/go-strip-markdown"
stripmd "github.com/writeas/go-strip-markdown"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
@@ -689,6 +689,52 @@ func ResetPassword(apper Apper, username string) error {
return nil return nil
} }


// DoDeleteAccount runs the confirmation and account delete process.
func DoDeleteAccount(apper Apper, username string) error {
// Connect to the database
apper.LoadConfig()
connectToDatabase(apper.App())
defer shutdown(apper.App())

// check user exists
u, err := apper.App().db.GetUserForAuth(username)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
userID := u.ID

// do not delete the admin account
// TODO: check for other admins and skip?
if u.IsAdmin() {
log.Error("Can not delete admin account")
os.Exit(1)
}

// confirm deletion, w/ w/out posts
prompt := promptui.Prompt{
Templates: &promptui.PromptTemplates{
Success: "{{ . | bold | faint }}: ",
},
Label: fmt.Sprintf("Really delete user : %s", username),
IsConfirm: true,
}
_, err = prompt.Run()
if err != nil {
log.Info("Aborted...")
os.Exit(0)
}

log.Info("Deleting...")
err = apper.App().db.DeleteAccount(userID)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
log.Info("Success.")
return nil
}

func connectToDatabase(app *App) { func connectToDatabase(app *App) {
log.Info("Connecting to %s database...", app.cfg.Database.Type) log.Info("Connecting to %s database...", app.cfg.Database.Type)




+ 2
- 1
author/author.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -65,6 +65,7 @@ var reservedUsernames = map[string]bool{
"metadata": true, "metadata": true,
"new": true, "new": true,
"news": true, "news": true,
"oauth": true,
"post": true, "post": true,
"posts": true, "posts": true,
"privacy": true, "privacy": true,


+ 11
- 2
cmd/writefreely/main.go View File

@@ -13,11 +13,12 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"strings"

"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely" "github.com/writeas/writefreely"
"os"
"strings"
) )


func main() { func main() {
@@ -38,6 +39,7 @@ func main() {
// Admin actions // Admin actions
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
createUser := flag.String("create-user", "", "Create a regular user with the given username:password") createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
deleteUsername := flag.String("delete-user", "", "Delete a user with the given username")
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
outputVersion := flag.Bool("v", false, "Output the current version") outputVersion := flag.Bool("v", false, "Output the current version")
flag.Parse() flag.Parse()
@@ -102,6 +104,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
os.Exit(0) os.Exit(0)
} else if *deleteUsername != "" {
err := writefreely.DoDeleteAccount(app, *deleteUsername)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *migrate { } else if *migrate {
err := writefreely.Migrate(app) err := writefreely.Migrate(app)
if err != nil { if err != nil {


+ 28
- 14
collections.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -71,7 +71,7 @@ type (
IsTopLevel bool IsTopLevel bool
CurrentPage int CurrentPage int
TotalPages int TotalPages int
Suspended bool
Silenced bool
} }
SubmittedCollection struct { SubmittedCollection struct {
// Data used for updating a given collection // Data used for updating a given collection
@@ -397,13 +397,13 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
} }
userID = u.ID userID = u.ID
} }
suspended, err := app.db.IsUserSuspended(userID)
silenced, err := app.db.IsUserSilenced(userID)
if err != nil { if err != nil {
log.Error("new collection: %v", err) log.Error("new collection: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


if !author.IsValidUsername(app.cfg, c.Alias) { if !author.IsValidUsername(app.cfg, c.Alias) {
@@ -487,7 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
res.Owner = u res.Owner = u
} }
} }
// TODO: check suspended
// TODO: check status for silenced
app.db.GetPostsCount(res, isCollOwner) app.db.GetPostsCount(res, isCollOwner)
// Strip non-public information // Strip non-public information
res.Collection.ForPublic() res.Collection.ForPublic()
@@ -656,7 +656,7 @@ func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.R
} }


// TODO: move this to all permission checks? // TODO: move this to all permission checks?
suspended, err := app.db.IsUserSuspended(c.OwnerID)
suspended, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("process protected collection permissions: %v", err) log.Error("process protected collection permissions: %v", err)
return nil, err return nil, err
@@ -754,7 +754,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("view collection: %v", err) log.Error("view collection: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
@@ -764,6 +764,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
ac := c.PersonObject() ac := c.PersonObject()
ac.Context = []interface{}{activitystreams.Namespace} ac.Context = []interface{}{activitystreams.Namespace}
setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ac, http.StatusOK) return impart.RenderActivityJSON(w, ac, http.StatusOK)
} }


@@ -816,10 +817,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
log.Error("Error getting user for collection: %v", err) log.Error("Error getting user for collection: %v", err)
} }
} }
if !isOwner && suspended {
if !isOwner && silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
displayPage.Suspended = isOwner && suspended
displayPage.Silenced = isOwner && silenced
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner


@@ -856,6 +857,19 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
return err return err
} }


func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
handle := vars["handle"]

remoteUser, err := app.db.GetProfilePageFromHandle(app, handle)
if err != nil || remoteUser == "" {
log.Error("Couldn't find user %s: %v", handle, err)
return ErrRemoteUserNotFound
}

return impart.HTTPError{Status: http.StatusFound, Message: remoteUser}
}

func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error { func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
tag := vars["tag"] tag := vars["tag"]
@@ -925,7 +939,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
return ErrCollectionNotFound return ErrCollectionNotFound
} }
} }
displayPage.Suspended = owner != nil && owner.IsSilenced()
displayPage.Silenced = owner != nil && owner.IsSilenced()
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner
// Add more data // Add more data
@@ -979,14 +993,14 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
} }
} }


suspended, err := app.db.IsUserSuspended(u.ID)
silenced, err := app.db.IsUserSilenced(u.ID)
if err != nil { if err != nil {
log.Error("existing collection: %v", err) log.Error("existing collection: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }


if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


if r.Method == "DELETE" { if r.Method == "DELETE" {


+ 13
- 1
database-no-sqlite.go View File

@@ -1,7 +1,7 @@
// +build !sqlite,!wflib // +build !sqlite,!wflib


/* /*
* Copyright © 2019 A Bunch Tell LLC.
* Copyright © 2019-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -28,3 +28,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool {


return false return false
} }

func (db *datastore) isIgnorableError(err error) bool {
if db.driverName == driverMySQL {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
return mysqlErr.Number == mySQLErrCollationMix
}
} else {
log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName)
}

return false
}

+ 12
- 0
database-sqlite.go View File

@@ -48,3 +48,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool {


return false return false
} }

func (db *datastore) isIgnorableError(err error) bool {
if db.driverName == driverMySQL {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
return mysqlErr.Number == mySQLErrCollationMix
}
} else {
log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName)
}

return false
}

+ 138
- 53
database.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -22,6 +22,7 @@ import (
"github.com/guregu/null" "github.com/guregu/null"
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
uuid "github.com/nu7hatch/gouuid" uuid "github.com/nu7hatch/gouuid"
"github.com/writeas/activityserve"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/nerds/store" "github.com/writeas/nerds/store"
"github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitypub"
@@ -37,6 +38,7 @@ import (


const ( const (
mySQLErrDuplicateKey = 1062 mySQLErrDuplicateKey = 1062
mySQLErrCollationMix = 1267


driverMySQL = "mysql" driverMySQL = "mysql"
driverSQLite = "sqlite3" driverSQLite = "sqlite3"
@@ -63,7 +65,7 @@ type writestore interface {
GetAccessToken(userID int64) (string, error) GetAccessToken(userID int64) (string, error)
GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (string, error)
GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error)
DeleteAccount(userID int64) (l *string, err error)
DeleteAccount(userID int64) error
ChangeSettings(app *App, u *User, s *userSettings) error ChangeSettings(app *App, u *User, s *userSettings) error
ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error


@@ -319,18 +321,18 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
return u, nil return u, nil
} }


// IsUserSuspended returns true if the user account associated with id is
// currently suspended.
func (db *datastore) IsUserSuspended(id int64) (bool, error) {
// IsUserSilenced returns true if the user account associated with id is
// currently silenced.
func (db *datastore) IsUserSilenced(id int64) (bool, error) {
u := &User{ID: id} u := &User{ID: id}


err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound)
return false, fmt.Errorf("is user silenced: %v", ErrUserNotFound)
case err != nil: case err != nil:
log.Error("Couldn't SELECT user password: %v", err)
return false, fmt.Errorf("is user suspended: %v", err)
log.Error("Couldn't SELECT user status: %v", err)
return false, fmt.Errorf("is user silenced: %v", err)
} }


return u.IsSilenced(), nil return u.IsSilenced(), nil
@@ -2115,22 +2117,13 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool {
return true return true
} }


func (db *datastore) DeleteAccount(userID int64) (l *string, err error) {
debug := ""
l = &debug

t, err := db.Begin()
if err != nil {
stringLogln(l, "Unable to begin: %v", err)
return
}

// DeleteAccount will delete the entire account for userID
func (db *datastore) DeleteAccount(userID int64) error {
// Get all collections // Get all collections
rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID)
if err != nil { if err != nil {
t.Rollback()
stringLogln(l, "Unable to get collections: %v", err)
return
log.Error("Unable to get collections: %v", err)
return err
} }
defer rows.Close() defer rows.Close()
colls := []Collection{} colls := []Collection{}
@@ -2138,103 +2131,158 @@ func (db *datastore) DeleteAccount(userID int64) (l *string, err error) {
for rows.Next() { for rows.Next() {
err = rows.Scan(&c.ID, &c.Alias) err = rows.Scan(&c.ID, &c.Alias)
if err != nil { if err != nil {
t.Rollback()
stringLogln(l, "Unable to scan collection cols: %v", err)
return
log.Error("Unable to scan collection cols: %v", err)
return err
} }
colls = append(colls, c) colls = append(colls, c)
} }


// Start transaction
t, err := db.Begin()
if err != nil {
log.Error("Unable to begin: %v", err)
return err
}

// Clean up all collection related information
var res sql.Result var res sql.Result
for _, c := range colls { for _, c := range colls {
// TODO: user deleteCollection() func
// Delete tokens // Delete tokens
res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete attributes on %s: %v", c.Alias, err)
return
log.Error("Unable to delete attributes on %s: %v", c.Alias, err)
return err
} }
rs, _ := res.RowsAffected() rs, _ := res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionattributes", rs, c.Alias)
log.Info("Deleted %d for %s from collectionattributes", rs, c.Alias)


// Remove any optional collection password // Remove any optional collection password
res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete passwords on %s: %v", c.Alias, err)
return
log.Error("Unable to delete passwords on %s: %v", c.Alias, err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionpasswords", rs, c.Alias)
log.Info("Deleted %d for %s from collectionpasswords", rs, c.Alias)


// Remove redirects to this collection // Remove redirects to this collection
res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete redirects on %s: %v", c.Alias, err)
return
log.Error("Unable to delete redirects on %s: %v", c.Alias, err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionredirects", rs, c.Alias)
log.Info("Deleted %d for %s from collectionredirects", rs, c.Alias)

// Remove any collection keys
res, err = t.Exec("DELETE FROM collectionkeys WHERE collection_id = ?", c.ID)
if err != nil {
t.Rollback()
log.Error("Unable to delete keys on %s: %v", c.Alias, err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d for %s from collectionkeys", rs, c.Alias)

// TODO: federate delete collection

// Remove remote follows
res, err = t.Exec("DELETE FROM remotefollows WHERE collection_id = ?", c.ID)
if err != nil {
t.Rollback()
log.Error("Unable to delete remote follows on %s: %v", c.Alias, err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d for %s from remotefollows", rs, c.Alias)
} }


// Delete collections // Delete collections
res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete collections: %v", err)
return
log.Error("Unable to delete collections: %v", err)
return err
} }
rs, _ := res.RowsAffected() rs, _ := res.RowsAffected()
stringLogln(l, "Deleted %d from collections", rs)
log.Info("Deleted %d from collections", rs)


// Delete tokens // Delete tokens
res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete access tokens: %v", err)
return
log.Error("Unable to delete access tokens: %v", err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from accesstokens", rs)
log.Info("Deleted %d from accesstokens", rs)

// Delete user attributes
res, err = t.Exec("DELETE FROM oauth_users WHERE user_id = ?", userID)
if err != nil {
t.Rollback()
log.Error("Unable to delete oauth_users: %v", err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d from oauth_users", rs)


// Delete posts // Delete posts
// TODO: should maybe get each row so we can federate a delete
// if so needs to be outside of transaction like collections
res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID) res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete posts: %v", err)
return
log.Error("Unable to delete posts: %v", err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from posts", rs)
log.Info("Deleted %d from posts", rs)


// Delete user attributes
res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID) res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete attributes: %v", err)
return
log.Error("Unable to delete attributes: %v", err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from userattributes", rs)
log.Info("Deleted %d from userattributes", rs)


// Delete user invites
res, err = t.Exec("DELETE FROM userinvites WHERE owner_id = ?", userID)
if err != nil {
t.Rollback()
log.Error("Unable to delete invites: %v", err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d from userinvites", rs)

// Delete the user
res, err = t.Exec("DELETE FROM users WHERE id = ?", userID) res, err = t.Exec("DELETE FROM users WHERE id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete user: %v", err)
return
log.Error("Unable to delete user: %v", err)
return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from users", rs)
log.Info("Deleted %d from users", rs)


// Commit all changes to the database
err = t.Commit() err = t.Commit()
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to commit: %v", err)
return
log.Error("Unable to commit: %v", err)
return err
} }


return
// TODO: federate delete actor

return nil
} }


func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) { func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) {
@@ -2283,7 +2331,7 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) {
var i Invite var i Invite
err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive) err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive)
switch { switch {
case err == sql.ErrNoRows:
case err == sql.ErrNoRows, db.isIgnorableError(err):
return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."} return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."}
case err != nil: case err != nil:
log.Error("Failed selecting invite: %v", err) log.Error("Failed selecting invite: %v", err)
@@ -2592,3 +2640,40 @@ func handleFailedPostInsert(err error) error {
log.Error("Couldn't insert into posts: %v", err) log.Error("Couldn't insert into posts: %v", err)
return err return err
} }

func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) {
actorIRI := ""
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
// handle from a previous version
// TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all
actorIRI = RemoteLookup(handle)
_, errRemoteUser := getRemoteUser(app, actorIRI)
// if it exists then we need to update the handle
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)
}
} 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)
}
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)
return "", err
}
}
} else {
actorIRI = remoteUser.ActorID
}
return actorIRI, nil
}

+ 5
- 4
errors.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -45,10 +45,11 @@ var (
ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."} ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."}
ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."} ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."}


ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
ErrRemoteUserNotFound = impart.HTTPError{http.StatusNotFound, "Remote user not found."}
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}


ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
ErrUserSilenced = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
) )


// Post operation errors // Post operation errors


+ 2
- 2
feed.go View File

@@ -36,12 +36,12 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
return nil return nil
} }


suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("view feed: get user: %v", err) log.Error("view feed: get user: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended {
if silenced {
return ErrCollectionNotFound return ErrCollectionNotFound
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


+ 9
- 8
go.mod View File

@@ -6,22 +6,23 @@ require (
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect
github.com/clbanning/mxj v1.8.4 // 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/dustin/go-humanize v1.0.0
github.com/fatih/color v1.7.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/go-sql-driver/mysql v1.4.1
github.com/go-test/deep v1.0.1 // indirect github.com/go-test/deep v1.0.1 // indirect
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // 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/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/feeds v1.1.0 github.com/gorilla/feeds v1.1.0
github.com/gorilla/mux v1.7.0 github.com/gorilla/mux v1.7.0
github.com/gorilla/schema v1.0.2 github.com/gorilla/schema v1.0.2
github.com/gorilla/sessions v1.1.3
github.com/gorilla/sessions v1.2.0
github.com/guregu/null v3.4.0+incompatible github.com/guregu/null v3.4.0+incompatible
github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-multierror v1.0.0
github.com/ikeikeikeike/go-sitemap-generator v1.0.1
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2
github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kr/pretty v0.1.0
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/manifoldco/promptui v0.3.2 github.com/manifoldco/promptui v0.3.2
@@ -32,12 +33,13 @@ require (
github.com/nicksnyder/go-i18n v1.10.0 // indirect github.com/nicksnyder/go-i18n v1.10.0 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/pelletier/go-toml v1.2.0 // indirect github.com/pelletier/go-toml v1.2.0 // indirect
github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.8.1 // indirect
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/writeas/activity v0.1.2 github.com/writeas/activity v0.1.2
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89
github.com/writeas/go-strip-markdown v2.0.1+incompatible 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 v0.0.0-20190106002315-85cf805c86d2
github.com/writeas/httpsig v1.0.0 github.com/writeas/httpsig v1.0.0
@@ -49,15 +51,14 @@ require (
github.com/writeas/slug v1.2.0 github.com/writeas/slug v1.2.0
github.com/writeas/web-core v1.2.0 github.com/writeas/web-core v1.2.0
github.com/writefreely/go-nodeinfo v1.2.0 github.com/writefreely/go-nodeinfo v1.2.0
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect
google.golang.org/appengine v1.4.0 // indirect google.golang.org/appengine v1.4.0 // indirect
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // 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.41.0
gopkg.in/yaml.v2 v2.2.2 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect
) )


go 1.13 go 1.13

+ 26
- 20
go.sum View File

@@ -25,13 +25,18 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 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/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 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 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.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-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 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.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
@@ -40,14 +45,14 @@ github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200j
github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo= 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/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/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/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= 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/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 h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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 h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/feeds v1.1.0 h1:pcgLJhbdYgaUESnj3AmXPcB7cS3vy63+jC/TI14AGXk= 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.0/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
@@ -56,16 +61,14 @@ 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.0.2/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 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
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 h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM=
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 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/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 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/ikeikeikeike/go-sitemap-generator v1.0.1 h1:49Fn8gro/B12vCY8pf5/+/Jpr3kwB9TvP0MSymo69SY=
github.com/ikeikeikeike/go-sitemap-generator v1.0.1/go.mod h1:QI+zWsz6yQyxkG9LWNcnu0f7aiAE5tPdsZOsICgmd1c=
github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= 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/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
@@ -123,6 +126,12 @@ github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTG
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY= 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/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0=
github.com/writeas/activityserve v0.0.0-20191008122325-5fc3b48e70c5 h1:nG84xWpxBM8YU/FJchezJqg7yZH8ImSRow6NoYtbSII=
github.com/writeas/activityserve v0.0.0-20191008122325-5fc3b48e70c5/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
github.com/writeas/activityserve v0.0.0-20191011072627-3a81f7784d5b h1:rd2wX/bTqD55hxtBjAhwLcUgaQE36c70KX3NzpDAwVI=
github.com/writeas/activityserve v0.0.0-20191011072627-3a81f7784d5b/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89 h1:NJhzq9aTccL3SSSZMrcnYhkD6sObdY9otNZ1X6/ZKNE=
github.com/writeas/activityserve v0.0.0-20191115095800-dd6d19cc8b89/go.mod h1:Kz62mzYsCnrFTSTSFLXFj3fGYBQOntmBWTDDq57b46A=
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 h1:DUsp4OhdfI+e6iUqcPQlwx8QYXuUDsToTz/x82D3Zuo= github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2 h1:DUsp4OhdfI+e6iUqcPQlwx8QYXuUDsToTz/x82D3Zuo=
@@ -137,12 +146,6 @@ github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE=
github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d h1:PK7DOj3JE6MGf647esPrKzXEHFjGWX2hl22uX79ixaE= github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d h1:PK7DOj3JE6MGf647esPrKzXEHFjGWX2hl22uX79ixaE=
github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y=
github.com/writeas/import v0.0.0-20190815214647-baae8acd8d06 h1:S6oKKP8GhSoyZUvVuhO9UiQ9f+U1aR/x5B4MP7YQHaU=
github.com/writeas/import v0.0.0-20190815214647-baae8acd8d06/go.mod h1:f3K8z7YnJwKnPIT4h7980n9C6cQb4DIB2QcxVCTB7lE=
github.com/writeas/import v0.0.0-20190815235139-628d10daaa9e h1:31PkvDTWkjzC1nGzWw9uAE92ZfcVyFX/K9L9ejQjnEs=
github.com/writeas/import v0.0.0-20190815235139-628d10daaa9e/go.mod h1:f3K8z7YnJwKnPIT4h7980n9C6cQb4DIB2QcxVCTB7lE=
github.com/writeas/import v0.1.1 h1:SbYltT+nxrJBUe0xQWJqeKMHaupbxV0a6K3RtwcE4yY=
github.com/writeas/import v0.1.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM=
github.com/writeas/import v0.2.0 h1:Ov23JW9Rnjxk06rki1Spar45bNX647HhwhAZj3flJiY= 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.0/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 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c=
@@ -156,8 +159,6 @@ github.com/writeas/saturday v1.7.1 h1:lYo1EH6CYyrFObQoA9RNWHVlpZA5iYL5Opxo7PYAnZ
github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ=
github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= 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/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0=
github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE=
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0= 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.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
@@ -165,20 +166,23 @@ github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHio
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f h1:ETU2VEl7TnT5bl7IvuKEzTDpplg5wzGYsOCAPhdoEIg=
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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 h1:rJm0LuqUjoDhSk2zO9ISMSToQxGz7Os2jRiOL8AWu4c=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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 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-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/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/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 h1:bPP/rGuN1LUM0eaEwo6vnP6OfIWJzJBulzGUiKLjjSY= golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 h1:bPP/rGuN1LUM0eaEwo6vnP6OfIWJzJBulzGUiKLjjSY=
@@ -196,3 +200,5 @@ gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+p
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b h1:rPAdjgXks4ToezTjygsnKZroxKVnA1L35DSpsJXPtfc=
src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=

+ 9
- 2
invites.go View File

@@ -56,12 +56,19 @@ func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Req


p := struct { p := struct {
*UserPage *UserPage
Invites *[]Invite
Invites *[]Invite
Silenced bool
}{ }{
UserPage: NewUserPage(app, r, u, "Invite People", f), UserPage: NewUserPage(app, r, u, "Invite People", f),
} }


var err error var err error

p.Silenced, err = app.db.IsUserSilenced(u.ID)
if err != nil {
log.Error("view invites: %v", err)
}

p.Invites, err = app.db.GetUserInvites(u.ID) p.Invites, err = app.db.GetUserInvites(u.ID)
if err != nil { if err != nil {
return err return err
@@ -79,7 +86,7 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
expVal := r.FormValue("expires") expVal := r.FormValue("expires")


if u.IsSilenced() { if u.IsSilenced() {
return ErrUserSuspended
return ErrUserSilenced
} }


var err error var err error


+ 7
- 6
migrations/migrations.go View File

@@ -56,12 +56,13 @@ func (m *migration) Migrate(db *datastore) error {
} }


var migrations = []Migration{ var migrations = []Migration{
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
New("support oauth", oauth), // V3 -> V4
New("support slack oauth", oauthSlack), // V4 -> v5
New("support oauth attach", oauthAttach), // V5 -> V6
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
New("support oauth", oauth), // V3 -> V4
New("support slack oauth", oauthSlack), // V4 -> v5
New("support ActivityPub mentions", supportActivityPubMentions), // V5 -> V6 (v0.12.0)
New("support oauth attach", oauthAttach), // V6 -> V7
} }


// CurrentVer returns the current migration version the application is on // CurrentVer returns the current migration version the application is on


+ 23
- 30
migrations/v6.go View File

@@ -1,36 +1,29 @@
/*
* Copyright © 2019 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 package migrations


import (
"context"
"database/sql"
func supportActivityPubMentions(db *datastore) error {
t, err := db.Begin()


wf_db "github.com/writeas/writefreely/db"
)
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` DEFAULT '' NOT NULL`)
if err != nil {
t.Rollback()
return err
}


func oauthAttach(db *datastore) error {
dialect := wf_db.DialectMySQL
if db.driverName == driverSQLite {
dialect = wf_db.DialectSQLite
err = t.Commit()
if err != nil {
t.Rollback()
return err
} }
return wf_db.RunTransactionWithOptions(context.Background(), db.DB, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error {
builders := []wf_db.SQLBuilder{
dialect.
AlterTable("oauth_client_states").
AddColumn(dialect.
Column(
"attach_user_id",
wf_db.ColumnTypeInteger,
wf_db.OptionalInt{Set: true, Value: 24,}).SetNullable(false).SetDefault("0")),
}
for _, builder := range builders {
query, err := builder.ToSQL()
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, query); err != nil {
return err
}
}
return nil
})

return nil
} }

+ 36
- 0
migrations/v7.go View File

@@ -0,0 +1,36 @@
package migrations

import (
"context"
"database/sql"

wf_db "github.com/writeas/writefreely/db"
)

func oauthAttach(db *datastore) error {
dialect := wf_db.DialectMySQL
if db.driverName == driverSQLite {
dialect = wf_db.DialectSQLite
}
return wf_db.RunTransactionWithOptions(context.Background(), db.DB, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error {
builders := []wf_db.SQLBuilder{
dialect.
AlterTable("oauth_client_states").
AddColumn(dialect.
Column(
"attach_user_id",
wf_db.ColumnTypeInteger,
wf_db.OptionalInt{Set: true, Value: 24}).SetNullable(false).SetDefault("0")),
}
for _, builder := range builders {
query, err := builder.ToSQL()
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, query); err != nil {
return err
}
}
return nil
})
}

+ 33
- 21
oauth_signup.go View File

@@ -1,3 +1,13 @@
/*
* 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 package writefreely


import ( import (
@@ -22,16 +32,16 @@ type viewOauthSignupVars struct {


AccessToken string AccessToken string
TokenUsername string TokenUsername string
TokenAlias string
TokenAlias string // TODO: rename this to match the data it represents: the collection title
TokenEmail string TokenEmail string
TokenRemoteUser string TokenRemoteUser string
Provider string Provider string
ClientID string ClientID string
TokenHash string TokenHash string


Username string
Alias string
Email string
LoginUsername string
Alias string // TODO: rename this to match the data it represents: the collection title
Email string
} }


const ( const (
@@ -52,7 +62,7 @@ const (
type oauthSignupPageParams struct { type oauthSignupPageParams struct {
AccessToken string AccessToken string
TokenUsername string TokenUsername string
TokenAlias string
TokenAlias string // TODO: rename this to match the data it represents: the collection title
TokenEmail string TokenEmail string
TokenRemoteUser string TokenRemoteUser string
ClientID string ClientID string
@@ -91,14 +101,20 @@ func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.R
return h.showOauthSignupPage(app, w, r, tp, err) return h.showOauthSignupPage(app, w, r, tp, err)
} }


hashedPass, err := auth.HashPass([]byte(r.FormValue(oauthParamPassword)))
if err != nil {
return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password"))
var err error
hashedPass := []byte{}
clearPass := r.FormValue(oauthParamPassword)
hasPass := clearPass != ""
if hasPass {
hashedPass, err = auth.HashPass([]byte(clearPass))
if err != nil {
return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password"))
}
} }
newUser := &User{ newUser := &User{
Username: r.FormValue(oauthParamUsername), Username: r.FormValue(oauthParamUsername),
HashedPass: hashedPass, HashedPass: hashedPass,
HasPass: true,
HasPass: hasPass,
Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey), Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey),
Created: time.Now().Truncate(time.Second).UTC(), Created: time.Now().Truncate(time.Second).UTC(),
} }
@@ -131,13 +147,9 @@ func (h oauthHandler) validateOauthSignup(r *http.Request) error {
if len(username) > 100 { if len(username) > 100 {
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."} return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."}
} }
alias := r.FormValue(oauthParamAlias)
if len(alias) == 0 {
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too short."}
}
password := r.FormValue("password")
if len(password) == 0 {
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Password is too short."}
collTitle := r.FormValue(oauthParamAlias)
if len(collTitle) == 0 {
collTitle = username
} }
email := r.FormValue(oauthParamEmail) email := r.FormValue(oauthParamEmail)
if len(email) > 0 { if len(email) > 0 {
@@ -151,7 +163,7 @@ func (h oauthHandler) validateOauthSignup(r *http.Request) error {


func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error { func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error {
username := tp.TokenUsername username := tp.TokenUsername
alias := tp.TokenAlias
collTitle := tp.TokenAlias
email := tp.TokenEmail email := tp.TokenEmail


session, err := app.sessionStore.Get(r, cookieName) session, err := app.sessionStore.Get(r, cookieName)
@@ -164,7 +176,7 @@ func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *ht
username = tmpValue username = tmpValue
} }
if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 { if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 {
alias = tmpValue
collTitle = tmpValue
} }
if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 { if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 {
email = tmpValue email = tmpValue
@@ -184,9 +196,9 @@ func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *ht
ClientID: tp.ClientID, ClientID: tp.ClientID,
TokenHash: tp.TokenHash, TokenHash: tp.TokenHash,


Username: username,
Alias: alias,
Email: email,
LoginUsername: username,
Alias: collTitle,
Email: email,
} }


// Display any error messages // Display any error messages


+ 11
- 1
oauth_slack.go View File

@@ -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 writefreely package writefreely


import ( import (
@@ -157,7 +167,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse { func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
return &InspectResponse{ return &InspectResponse{
UserID: resp.User.ID, UserID: resp.User.ID,
Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.Generate62RandomString(5)),
Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.GenerateRandomString("0123456789bcdfghjklmnpqrstvwxyz", 5)),
DisplayName: resp.User.Name, DisplayName: resp.User.Name,
Email: resp.User.Email, Email: resp.User.Email,
} }


+ 9
- 9
pad.go View File

@@ -35,10 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
} }
appData := &struct { appData := &struct {
page.StaticPage page.StaticPage
Post *RawPost
User *User
Blogs *[]Collection
Suspended bool
Post *RawPost
User *User
Blogs *[]Collection
Silenced bool


Editing bool // True if we're modifying an existing post Editing bool // True if we're modifying an existing post
EditCollection *Collection // Collection of the post we're editing, if any EditCollection *Collection // Collection of the post we're editing, if any
@@ -53,9 +53,9 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
log.Error("Unable to get user's blogs for Pad: %v", err) log.Error("Unable to get user's blogs for Pad: %v", err)
} }
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID)
if err != nil { if err != nil {
log.Error("Unable to get users suspension status for Pad: %v", err)
log.Error("Unable to get user status for Pad: %v", err)
} }
} }


@@ -127,16 +127,16 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
EditCollection *Collection // Collection of the post we're editing, if any EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string Flashes []string
NeedsToken bool NeedsToken bool
Suspended bool
Silenced bool
}{ }{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"}, Post: &RawPost{Font: "norm"},
User: getUserSession(app, r), User: getUserSession(app, r),
} }
var err error var err error
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID)
if err != nil { if err != nil {
log.Error("view meta: get user suspended status: %v", err)
log.Error("view meta: get user status: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }




+ 66
- 10
pages/signup-oauth.tmpl View File

@@ -65,7 +65,7 @@ form dd {
</ul>{{end}} </ul>{{end}}


<div id="billing"> <div id="billing">
<form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
<form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="return disableSubmit()">
<input type="hidden" name="access_token" value="{{ .AccessToken }}" /> <input type="hidden" name="access_token" value="{{ .AccessToken }}" />
<input type="hidden" name="token_username" value="{{ .TokenUsername }}" /> <input type="hidden" name="token_username" value="{{ .TokenUsername }}" />
<input type="hidden" name="token_alias" value="{{ .TokenAlias }}" /> <input type="hidden" name="token_alias" value="{{ .TokenAlias }}" />
@@ -77,15 +77,15 @@ form dd {


<dl class="billing"> <dl class="billing">
<label> <label>
<dt>Blog Title</dt>
<dt>Display Name</dt>
<dd> <dd>
<input type="text" style="width: 100%; box-sizing: border-box;" name="alias" placeholder="Alias"{{ if .Alias }} value="{{.Alias}}"{{ end }} />
<input type="text" style="width: 100%; box-sizing: border-box;" name="alias" placeholder="Name"{{ if .Alias }} value="{{.Alias}}"{{ end }} />
</dd> </dd>
</label> </label>
<label> <label>
<dt>Username</dt> <dt>Username</dt>
<dd> <dd>
<input type="text" name="username" style="width: 100%; box-sizing: border-box;" placeholder="Username" value="{{.Username}}" /><br />
<input type="text" id="username" name="username" style="width: 100%; box-sizing: border-box;" placeholder="Username" value="{{.LoginUsername}}" /><br />
{{if .Federation}}<p id="alias-site" class="demo">@<strong>your-username</strong>@{{.FriendlyHost}}</p>{{else}}<p id="alias-site" class="demo">{{.FriendlyHost}}/<strong>your-username</strong></p>{{end}} {{if .Federation}}<p id="alias-site" class="demo">@<strong>your-username</strong>@{{.FriendlyHost}}</p>{{else}}<p id="alias-site" class="demo">{{.FriendlyHost}}/<strong>your-username</strong></p>{{end}}
</dd> </dd>
</label> </label>
@@ -95,12 +95,6 @@ form dd {
<input type="text" name="email" style="width: 100%; box-sizing: border-box;" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} /> <input type="text" name="email" style="width: 100%; box-sizing: border-box;" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} />
</dd> </dd>
</label> </label>
<label>
<dt>Password</dt>
<dd>
<input type="password" name="password" style="width: 100%; box-sizing: border-box;" placeholder="Password" /><br />
</dd>
</label>
<dt> <dt>
<input type="submit" id="btn-login" value="Login" /> <input type="submit" id="btn-login" value="Login" />
</dt> </dt>
@@ -108,11 +102,73 @@ form dd {
</form> </form>
</div> </div>


<script type="text/javascript" src="/js/h.js"></script>
<script type="text/javascript"> <script type="text/javascript">
// Copied from signup.tmpl
// NOTE: this element is named "alias" on signup.tmpl and "username" here
var $alias = H.getEl('username');

function disableSubmit() { function disableSubmit() {
// Validate input
if (!aliasOK) {
var $a = $alias;
$a.el.className = 'error';
$a.el.focus();
$a.el.scrollIntoView();
return false;
}

var $btn = document.getElementById("btn-login"); var $btn = document.getElementById("btn-login");
$btn.value = "Logging in..."; $btn.value = "Logging in...";
$btn.disabled = true; $btn.disabled = true;
return true;
} }

// Copied from signup.tmpl
var $aliasSite = document.getElementById('alias-site');
var aliasOK = true;
var typingTimer;
var doneTypingInterval = 750;
var doneTyping = function() {
// Check on username
var alias = $alias.el.value;
if (alias != "") {
var params = {
username: alias
};
var http = new XMLHttpRequest();
http.open("POST", '/api/alias', true);

// Send the proper header information along with the request
http.setRequestHeader("Content-type", "application/json");

http.onreadystatechange = function() {
if (http.readyState == 4) {
data = JSON.parse(http.responseText);
if (http.status == 200) {
aliasOK = true;
$alias.removeClass('error');
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)demo(?!\S)/g, '');
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)error(?!\S)/g, '');
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>' + data.data + '</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>' + data.data + '</strong>/{{ end }}';
} else {
aliasOK = false;
$alias.setClass('error');
$aliasSite.className = 'error';
$aliasSite.textContent = data.error_msg;
}
}
}
http.send(JSON.stringify(params));
} else {
$aliasSite.className += ' demo';
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>your-username</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>your-username</strong>/{{ end }}';
}
};
$alias.on('keyup input', function() {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
});
doneTyping();
</script> </script>
{{end}} {{end}}

+ 3
- 0
postrender.go View File

@@ -38,6 +38,7 @@ var (
titleElementReg = regexp.MustCompile("</?h[1-6]>") titleElementReg = regexp.MustCompile("</?h[1-6]>")
hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`) hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`)
markeddownReg = regexp.MustCompile("<p>(.+)</p>") markeddownReg = regexp.MustCompile("<p>(.+)</p>")
mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`)
) )


func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) { func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) {
@@ -86,6 +87,8 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
tagPrefix = "/read/t/" tagPrefix = "/read/t/"
} }
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>"))) md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>")))
handlePrefix := cfg.App.Host + "/@/"
md = []byte(mentionReg.ReplaceAll(md, []byte("<a href=\""+handlePrefix+"$1$2\" class=\"u-url mention\">@<span>$1$2</span></a>")))
} }
// Strip out bad HTML // Strip out bad HTML
policy := getSanitizationPolicy() policy := getSanitizationPolicy()


+ 59
- 32
posts.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018-2019 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -35,7 +35,6 @@ import (
"github.com/writeas/web-core/i18n" "github.com/writeas/web-core/i18n"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/web-core/tags" "github.com/writeas/web-core/tags"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
"github.com/writeas/writefreely/parse" "github.com/writeas/writefreely/parse"
) )
@@ -229,6 +228,10 @@ func (p Post) Summary() string {
return shortPostDescription(p.Content) return shortPostDescription(p.Content)
} }


func (p Post) SummaryHTML() template.HTML {
return template.HTML(p.Summary())
}

// Excerpt shows any text that comes before a (more) tag. // Excerpt shows any text that comes before a (more) tag.
// TODO: use HTMLExcerpt in templates instead of this method // TODO: use HTMLExcerpt in templates instead of this method
func (p *Post) Excerpt() template.HTML { func (p *Post) Excerpt() template.HTML {
@@ -381,9 +384,9 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
} }


var suspended bool
var silenced bool
if found { if found {
suspended, err = app.db.IsUserSuspended(ownerID.Int64)
silenced, err = app.db.IsUserSilenced(ownerID.Int64)
if err != nil { if err != nil {
log.Error("view post: %v", err) log.Error("view post: %v", err)
} }
@@ -436,10 +439,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page := struct { page := struct {
*AnonymousPost *AnonymousPost
page.StaticPage page.StaticPage
Username string
IsOwner bool
SiteURL string
Suspended bool
Username string
IsOwner bool
SiteURL string
Silenced bool
}{ }{
AnonymousPost: post, AnonymousPost: post,
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
@@ -450,10 +453,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
} }


if !page.IsOwner && suspended {
if !page.IsOwner && silenced {
return ErrPostNotFound return ErrPostNotFound
} }
page.Suspended = suspended
page.Silenced = silenced
err = templates["post"].ExecuteTemplate(w, "post", page) err = templates["post"].ExecuteTemplate(w, "post", page)
if err != nil { if err != nil {
log.Error("Post template execute error: %v", err) log.Error("Post template execute error: %v", err)
@@ -510,12 +513,12 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
} else { } else {
userID = app.db.GetUserID(accessToken) userID = app.db.GetUserID(accessToken)
} }
suspended, err := app.db.IsUserSuspended(userID)
silenced, err := app.db.IsUserSilenced(userID)
if err != nil { if err != nil {
log.Error("new post: %v", err) log.Error("new post: %v", err)
} }
if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


if userID == -1 { if userID == -1 {
@@ -683,12 +686,12 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
} }


suspended, err := app.db.IsUserSuspended(userID)
silenced, err := app.db.IsUserSilenced(userID)
if err != nil { if err != nil {
log.Error("existing post: %v", err) log.Error("existing post: %v", err)
} }
if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


// Modify post struct // Modify post struct
@@ -885,12 +888,12 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
ownerID = u.ID ownerID = u.ID
} }


suspended, err := app.db.IsUserSuspended(ownerID)
silenced, err := app.db.IsUserSilenced(ownerID)
if err != nil { if err != nil {
log.Error("add post: %v", err) log.Error("add post: %v", err)
} }
if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


// Parse claimed posts in format: // Parse claimed posts in format:
@@ -987,12 +990,12 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
userID = u.ID userID = u.ID
} }


suspended, err := app.db.IsUserSuspended(userID)
silenced, err := app.db.IsUserSilenced(userID)
if err != nil { if err != nil {
log.Error("pin post: %v", err) log.Error("pin post: %v", err)
} }
if suspended {
return ErrUserSuspended
if silenced {
return ErrUserSilenced
} }


// Parse request // Parse request
@@ -1068,11 +1071,11 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
} }


suspended, err := app.db.IsUserSuspended(p.OwnerID.Int64)
silenced, err := app.db.IsUserSilenced(p.OwnerID.Int64)
if err != nil { if err != nil {
log.Error("fetch post: %v", err) log.Error("fetch post: %v", err)
} }
if suspended {
if silenced {
return ErrPostNotFound return ErrPostNotFound
} }


@@ -1087,8 +1090,9 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }


p.Collection = &CollectionObj{Collection: *coll} p.Collection = &CollectionObj{Collection: *coll}
po := p.ActivityObject(app.cfg)
po := p.ActivityObject(app)
po.Context = []interface{}{activitystreams.Namespace} po.Context = []interface{}{activitystreams.Namespace}
setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, po, http.StatusOK) return impart.RenderActivityJSON(w, po, http.StatusOK)
} }


@@ -1122,7 +1126,8 @@ func (p *PublicPost) CanonicalURL(hostName string) string {
return p.Collection.CanonicalURL() + p.Slug.String return p.Collection.CanonicalURL() + p.Slug.String
} }


func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object {
func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object {
cfg := app.cfg
o := activitystreams.NewArticleObject() o := activitystreams.NewArticleObject()
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
o.Published = p.Created o.Published = p.Created
@@ -1162,6 +1167,27 @@ func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object
}) })
} }
} }
// Find mentioned users
mentionedUsers := make(map[string]string)

stripper := bluemonday.StrictPolicy()
content := stripper.Sanitize(p.Content)
mentionRegex := regexp.MustCompile(`@[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\b`)
mentions := mentionRegex.FindAllString(content, -1)

for _, handle := range mentions {
actorIRI, err := app.db.GetProfilePageFromHandle(app, handle)
if err != nil {
log.Info("Can't find this user either in the database nor in the remote instance")
return nil
}
mentionedUsers[handle] = actorIRI
}

for handle, iri := range mentionedUsers {
o.CC = append(o.CC, iri)
o.Tag = append(o.Tag, activitystreams.Tag{Type: "Mention", HRef: iri, Name: handle})
}
return o return o
} }


@@ -1329,7 +1355,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


suspended, err := app.db.IsUserSuspended(c.OwnerID)
silenced, err := app.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("view collection post: %v", err) log.Error("view collection post: %v", err)
} }
@@ -1339,7 +1365,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
return ErrPostNotFound return ErrPostNotFound
} }
if c.IsProtected() && (u == nil || u.ID != c.OwnerID) { if c.IsProtected() && (u == nil || u.ID != c.OwnerID) {
if suspended {
if silenced {
return ErrPostNotFound return ErrPostNotFound
} else if !isAuthorizedForCollection(app, c.Alias, r) { } else if !isAuthorizedForCollection(app, c.Alias, r) {
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/?g=" + slug} return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/?g=" + slug}
@@ -1394,7 +1420,7 @@ Are you sure it was ever here?`,
p.Collection = coll p.Collection = coll
p.IsTopLevel = app.cfg.App.SingleUser p.IsTopLevel = app.cfg.App.SingleUser


if !p.IsOwner && suspended {
if !p.IsOwner && silenced {
return ErrPostNotFound return ErrPostNotFound
} }
// Check if post has been unpublished // Check if post has been unpublished
@@ -1428,8 +1454,9 @@ Are you sure it was ever here?`,
return ErrCollectionPageNotFound return ErrCollectionPageNotFound
} }
p.extractData() p.extractData()
ap := p.ActivityObject(app.cfg)
ap := p.ActivityObject(app)
ap.Context = []interface{}{activitystreams.Namespace} ap.Context = []interface{}{activitystreams.Namespace}
setCacheControl(w, apCacheTime)
return impart.RenderActivityJSON(w, ap, http.StatusOK) return impart.RenderActivityJSON(w, ap, http.StatusOK)
} else { } else {
p.extractData() p.extractData()
@@ -1446,14 +1473,14 @@ Are you sure it was ever here?`,
IsFound bool IsFound bool
IsAdmin bool IsAdmin bool
CanInvite bool CanInvite bool
Suspended bool
Silenced bool
}{ }{
PublicPost: p, PublicPost: p,
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner, IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain, IsCustomDomain: cr.isCustomDomain,
IsFound: postFound, IsFound: postFound,
Suspended: suspended,
Silenced: silenced,
} }
tp.IsAdmin = u != nil && u.IsAdmin() tp.IsAdmin = u != nil && u.IsAdmin()
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)


+ 8
- 5
routes.go View File

@@ -70,6 +70,9 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))


// handle mentions
write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader))

configureSlackOauth(handler, write, apper.App()) configureSlackOauth(handler, write, apper.App())
configureWriteAsOauth(handler, write, apper.App()) configureWriteAsOauth(handler, write, apper.App())


@@ -162,7 +165,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Handle special pages first // Handle special pages first
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired)) write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
write.HandleFunc("/invite/{code:[a-zA-Z0-9]+}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
// TODO: show a reader-specific 404 page if the function is disabled // TODO: show a reader-specific 404 page if the function is disabled
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter()) RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
@@ -170,14 +173,14 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
draftEditPrefix := "" draftEditPrefix := ""
if apper.App().cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
draftEditPrefix = "/d" draftEditPrefix = "/d"
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
} else { } else {
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
} }


// All the existing stuff // All the existing stuff
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET")
// Collections // Collections
if apper.App().cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter()) RouteCollections(handler, write.PathPrefix("/").Subrouter())


+ 17
- 5
scripts/upgrade-server.sh View File

@@ -11,7 +11,7 @@
## have not installed the binary `writefreely` in another location. ## ## have not installed the binary `writefreely` in another location. ##
############################################################################### ###############################################################################
# #
# Copyright © 2019 A Bunch Tell LLC.
# Copyright © 2019-2020 A Bunch Tell LLC.
# #
# This file is part of WriteFreely. # This file is part of WriteFreely.
# #
@@ -31,7 +31,7 @@ fi
# go ahead and check for the latest release on linux # go ahead and check for the latest release on linux
echo "Checking for updates..." echo "Checking for updates..."


url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep linux | cut -d\" -f4`
url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep 'linux' | grep 'amd64' | cut -d\" -f4`


# check current version # check current version


@@ -82,13 +82,25 @@ filename=${parts[-1]}
echo "Extracting files..." echo "Extracting files..."
tar -zxf $tempdir/$filename -C $tempdir tar -zxf $tempdir/$filename -C $tempdir


# stop service
echo "Stopping writefreely systemd service..."
if `systemctl start writefreely`; then
echo "Success, service stopped."
else
echo "Upgrade failed to stop the systemd service, exiting early."
exit 1
fi

# copy files # copy files
echo "Copying files..." echo "Copying files..."
cp -r $tempdir/{pages,static,templates,writefreely} .
cp -r $tempdir/writefreely/{pages,static,templates,writefreely} .

# migrate db
./writefreely -migrate


# restart service # restart service
echo "Restarting writefreely systemd service..."
if `systemctl restart writefreely`; then
echo "Starting writefreely systemd service..."
if `systemctl start writefreely`; then
echo "Success, version has been upgraded to $latest." echo "Success, version has been upgraded to $latest."
else else
echo "Upgrade complete, but failed to restart service." echo "Upgrade complete, but failed to restart service."


+ 16
- 0
static/js/localdate.js View File

@@ -0,0 +1,16 @@
function toLocalDate(dateEl, displayEl) {
var d = new Date(dateEl.getAttribute("datetime"));
displayEl.textContent = d.toLocaleDateString(navigator.language || "en-US", { year: 'numeric', month: 'long', day: 'numeric' });
}

// Adjust dates on individual post pages, and on posts in a list *with* an explicit title
var $dates = document.querySelectorAll("article > time");
for (var i=0; i < $dates.length; i++) {
toLocalDate($dates[i], $dates[i]);
}

// Adjust dates on posts in a list without an explicit title, where they act as the header
$dates = document.querySelectorAll("h2.post-title > time");
for (i=0; i < $dates.length; i++) {
toLocalDate($dates[i], $dates[i].querySelector('a'));
}

+ 3
- 3
templates.go View File

@@ -65,7 +65,7 @@ func initTemplate(parentDir, name string) {
filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, name+".tmpl"),
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
} }
if name == "collection" || name == "collection-tags" || name == "chorus-collection" { if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
@@ -89,7 +89,7 @@ func initPage(parentDir, path, key string) {
path, path,
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
)) ))
} }


@@ -102,7 +102,7 @@ func initUserPage(parentDir, path, key string) {
path, path,
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"),
)) ))
} }




+ 4
- 3
templates/chorus-collection-post.tmpl View File

@@ -55,10 +55,10 @@ body#post header {


{{template "user-navigation" .}} {{template "user-navigation" .}}
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if $.Collection.Format.ShowDates}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if $.Collection.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>


{{ if .Collection.ShowFooterBranding }} {{ if .Collection.ShowFooterBranding }}
<footer dir="ltr"> <footer dir="ltr">
@@ -83,6 +83,7 @@ body#post header {
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} {{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}} {{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
{{end}} {{end}}
<script src="/js/localdate.js"></script>
<script type="text/javascript"> <script type="text/javascript">


var pinning = false; var pinning = false;


+ 3
- 2
templates/chorus-collection.tmpl View File

@@ -61,8 +61,8 @@ body#collection header nav.tabs a:first-child {
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> <body id="collection" itemscope itemtype="http://schema.org/WebPage">
{{template "user-navigation" .}} {{template "user-navigation" .}}
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<header> <header>
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> <h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
@@ -115,6 +115,7 @@ body#collection header nav.tabs a:first-child {
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}} {{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
{{end}} {{end}}
<script src="/js/h.js"></script> <script src="/js/h.js"></script>
<script src="/js/localdate.js"></script>
<script src="/js/postactions.js"></script> <script src="/js/postactions.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var deleting = false; var deleting = false;


+ 4
- 3
templates/collection-post.tmpl View File

@@ -59,10 +59,10 @@
</nav> </nav>
</header> </header>
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if $.Collection.Format.ShowDates}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name{{if $.Collection.Format.ShowDates}} dated{{end}}">{{.FormattedDisplayTitle}}</h2>{{end}}{{if $.Collection.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>


{{ if .Collection.ShowFooterBranding }} {{ if .Collection.ShowFooterBranding }}
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav></footer> <footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav></footer>
@@ -73,6 +73,7 @@
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} {{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}} {{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
{{end}} {{end}}
<script src="/js/localdate.js"></script>
<script type="text/javascript"> <script type="text/javascript">


var pinning = false; var pinning = false;


+ 3
- 2
templates/collection-tags.tmpl View File

@@ -53,8 +53,8 @@
</nav> </nav>
</header> </header>
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}} {{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
<h1>{{.Tag}}</h1> <h1>{{.Tag}}</h1>
@@ -75,6 +75,7 @@
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} {{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
{{if .Collection.Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}} {{if .Collection.Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
{{end}} {{end}}
<script src="/js/localdate.js"></script>
{{if .IsOwner}} {{if .IsOwner}}
<script src="/js/h.js"></script> <script src="/js/h.js"></script>
<script src="/js/postactions.js"></script> <script src="/js/postactions.js"></script>


+ 3
- 2
templates/collection.tmpl View File

@@ -62,8 +62,8 @@
</ul></nav>{{end}} </ul></nav>{{end}}
<header> <header>
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> <h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} {{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
@@ -116,6 +116,7 @@
{{end}} {{end}}
<script src="/js/h.js"></script> <script src="/js/h.js"></script>
<script src="/js/postactions.js"></script> <script src="/js/postactions.js"></script>
<script src="/js/localdate.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var deleting = false; var deleting = false;
function delPost(e, id, owned) { function delPost(e, id, owned) {


+ 1
- 1
templates/edit-meta.tmpl View File

@@ -269,7 +269,7 @@
<script src="/js/h.js"></script> <script src="/js/h.js"></script>
<script> <script>
function updateMeta() { function updateMeta() {
if ({{.Suspended}}) {
if ({{.Silenced}}) {
alert("Your account is silenced, so you can't edit posts."); alert("Your account is silenced, so you can't edit posts.");
return return
} }


+ 2
- 2
templates/include/posts.tmpl View File

@@ -21,10 +21,10 @@
{{end}} {{end}}
{{end}} {{end}}
</h2> </h2>
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{$.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>{{end}}
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{$.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>{{end}}
{{else}} {{else}}
<h2 class="post-title" itemprop="name"> <h2 class="post-title" itemprop="name">
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time>{{end}}
{{if $.Format.ShowDates}}<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time>{{end}}
{{if $.IsOwner}} {{if $.IsOwner}}
{{if not $.Format.ShowDates}}<a class="user hidden action" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view</a>{{end}} {{if not $.Format.ShowDates}}<a class="user hidden action" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{$.CanonicalURL}}{{.Slug.String}}{{end}}">view</a>{{end}}
<a class="user hidden action" href="/{{if not $.SingleUser}}{{$.Alias}}/{{end}}{{.Slug.String}}/edit">edit</a> <a class="user hidden action" href="/{{if not $.SingleUser}}{{$.Alias}}/{{end}}{{.Slug.String}}/edit">edit</a>


+ 2
- 2
templates/pad.tmpl View File

@@ -131,9 +131,9 @@
{{else}}var canPublish = true;{{end}} {{else}}var canPublish = true;{{end}}
var publishing = false; var publishing = false;
var justPublished = false; var justPublished = false;
var suspended = {{.Suspended}};
var silenced = {{.Silenced}};
var publish = function(content, font) { var publish = function(content, font) {
if (suspended === true) {
if (silenced === true) {
alert("Your account is silenced, so you can't publish or update posts."); alert("Your account is silenced, so you can't publish or update posts.");
return; return;
} }


+ 2
- 2
templates/post.tmpl View File

@@ -49,8 +49,8 @@
</nav> </nav>
</header> </header>


{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article> <article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article>


+ 3
- 3
templates/read.tmpl View File

@@ -88,9 +88,9 @@
<section itemscope itemtype="http://schema.org/Blog"> <section itemscope itemtype="http://schema.org/Blog">
{{range .Posts}}<article class="{{.Font}} h-entry" itemscope itemtype="http://schema.org/BlogPosting"> {{range .Posts}}<article class="{{.Font}} h-entry" itemscope itemtype="http://schema.org/BlogPosting">
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name"><a href="{{if .Slug.String}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2> {{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name"><a href="{{if .Slug.String}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2>
<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{.Collection.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>
<time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{.Collection.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>
{{else}} {{else}}
<h2 class="post-title" itemprop="name"><time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time></h2>
<h2 class="post-title" itemprop="name"><time class="dt-published" datetime="{{.Created8601}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time></h2>
{{end}} {{end}}
<p class="source">{{if .Collection}}from <a href="{{.Collection.CanonicalURL}}">{{.Collection.DisplayTitle}}</a>{{else}}<em>Anonymous</em>{{end}}</p> <p class="source">{{if .Collection}}from <a href="{{.Collection.CanonicalURL}}">{{.Collection.DisplayTitle}}</a>{{else}}<em>Anonymous</em>{{end}}</p>
{{if .Excerpt}}<div class="p-summary" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{.Excerpt}}</div> {{if .Excerpt}}<div class="p-summary" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{.Excerpt}}</div>
@@ -112,7 +112,7 @@
</nav>{{end}} </nav>{{end}}


</div> </div>
<script src="/js/localdate.js">
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
var $articles = document.querySelectorAll('article'); var $articles = document.querySelectorAll('article');


+ 5
- 5
templates/user/admin/view-user.tmpl View File

@@ -7,21 +7,21 @@ table.classy th {
h3 { h3 {
font-weight: normal; font-weight: normal;
} }
td.active-suspend {
td.active-silence {
display: flex; display: flex;
align-items: center; align-items: center;
} }


td.active-suspend > input[type="submit"] {
td.active-silence > input[type="submit"] {
margin-left: auto; margin-left: auto;
margin-right: 5%; margin-right: 5%;
} }


@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
td.active-suspend {
td.active-silence {
flex-wrap: wrap; flex-wrap: wrap;
} }
td.active-suspend > input[type="submit"] {
td.active-silence > input[type="submit"] {
margin: auto; margin: auto;
} }
} }
@@ -73,7 +73,7 @@ input.copy-text {
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}> <form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}>
<a id="status"/> <a id="status"/>
<th>Status</th> <th>Status</th>
<td class="active-suspend">
<td class="active-silence">
{{if .User.IsSilenced}} {{if .User.IsSilenced}}
<p>Silenced</p> <p>Silenced</p>
<input type="submit" value="Unsilence"/> <input type="submit" value="Unsilence"/>


+ 10
- 6
templates/user/articles.tmpl View File

@@ -6,13 +6,16 @@
{{if .Flashes}}<ul class="errors"> {{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}} </ul>{{end}}
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}


<h2 id="posts-header">drafts</h2> <h2 id="posts-header">drafts</h2>


{{ if .AnonymousPosts }}<div class="atoms posts">
{{ if .AnonymousPosts }}
<p>These are your draft posts. You can share them individually (without a blog) or move them to your blog when you're ready.</p>

<div class="atoms posts">
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post"> {{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3> <h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
<h4> <h4>
@@ -34,10 +37,11 @@
{{end}} {{end}}
{{ end }} {{ end }}
</h4> </h4>
{{if .Summary}}<p>{{.Summary}}</p>{{end}}
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{end}}
</div>{{end}} </div>{{end}}
</div>{{ else }}<div id="no-posts-published"><p>You haven't saved any drafts yet.</p>
<p>They'll show up here once you do. {{if not .SingleUser}}Find your blog posts from the <a href="/me/c/">Blogs</a> page.{{end}}</p>
</div>{{ else }}<div id="no-posts-published">
<p>Your anonymous and draft posts will show up here once you've published some. You'll be able to share them individually (without a blog) or move them to a blog when you're ready.</p>
{{if not .SingleUser}}<p>Alternatively, see your blogs and their posts on your <a href="/me/c/">Blogs</a> page.</p>{{end}}
<p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }} <p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }}


<div id="moving"></div> <div id="moving"></div>


+ 2
- 2
templates/user/collection.tmpl View File

@@ -8,8 +8,8 @@
<div class="content-container snug"> <div class="content-container snug">
<div id="overlay"></div> <div id="overlay"></div>


{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2> <h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>




+ 2
- 2
templates/user/collections.tmpl View File

@@ -7,8 +7,8 @@
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}} </ul>{{end}}


{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<h2>blogs</h2> <h2>blogs</h2>
<ul class="atoms collections"> <ul class="atoms collections">


+ 4
- 1
templates/user/import.tmpl View File

@@ -42,13 +42,16 @@
</select> </select>
</label> </label>
<script> <script>
// timezone offset in seconds
const tzOffsetSec = new Date().getTimezoneOffset() * 60;
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const fileDates = document.getElementById('fileDates'); const fileDates = document.getElementById('fileDates');
fileInput.addEventListener('change', (e) => { fileInput.addEventListener('change', (e) => {
const files = e.target.files; const files = e.target.files;
let dateMap = {}; let dateMap = {};
for (let file of files) { for (let file of files) {
dateMap[file.name] = file.lastModified / 1000;
// convert from milliseconds to seconds and adjust for tz
dateMap[file.name] = Math.round(file.lastModified / 1000) + tzOffsetSec;
} }
fileDates.value = JSON.stringify(dateMap); fileDates.value = JSON.stringify(dateMap);
}) })


templates/user/include/suspended.tmpl → templates/user/include/silenced.tmpl View File

@@ -1,4 +1,4 @@
{{define "user-suspended"}}
{{define "user-silenced"}}
<div class="alert info"> <div class="alert info">
<p><strong>Your account has been silenced.</strong> You can still access all of your posts and blogs, but no one else can currently see them.</p> <p><strong>Your account has been silenced.</strong> You can still access all of your posts and blogs, but no one else can currently see them.</p>
</div> </div>

+ 6
- 3
templates/user/invite.tmpl View File

@@ -20,6 +20,9 @@ table td {
</style> </style>


<div class="snug content-container"> <div class="snug content-container">
{{if .Silenced}}
{{template "user-silenced"}}
{{end}}
<h1>Invite people</h1> <h1>Invite people</h1>
<p>Invite others to join <em>{{.SiteName}}</em> by generating and sharing invite links below.</p> <p>Invite others to join <em>{{.SiteName}}</em> by generating and sharing invite links below.</p>


@@ -27,7 +30,7 @@ table td {
<div class="row"> <div class="row">
<div class="half"> <div class="half">
<label for="uses">Maximum number of uses:</label> <label for="uses">Maximum number of uses:</label>
<select id="uses" name="uses">
<select id="uses" name="uses" {{if .Silenced}}disabled{{end}}>
<option value="0">No limit</option> <option value="0">No limit</option>
<option value="1">1 use</option> <option value="1">1 use</option>
<option value="5">5 uses</option> <option value="5">5 uses</option>
@@ -39,7 +42,7 @@ table td {
</div> </div>
<div class="half"> <div class="half">
<label for="expires">Expire after:</label> <label for="expires">Expire after:</label>
<select id="expires" name="expires">
<select id="expires" name="expires" {{if .Silenced}}disabled{{end}}>
<option value="0">Never</option> <option value="0">Never</option>
<option value="30">30 minutes</option> <option value="30">30 minutes</option>
<option value="60">1 hour</option> <option value="60">1 hour</option>
@@ -52,7 +55,7 @@ table td {
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<input type="submit" value="Generate" />
<input type="submit" value="Generate" {{if .Silenced}}disabled title="You cannot generate invites while your account is silenced."{{end}} />
</div> </div>
</form> </form>




+ 2
- 2
templates/user/settings.tmpl View File

@@ -7,8 +7,8 @@ h3 { font-weight: normal; }
.section > *:not(input) { font-size: 0.86em; } .section > *:not(input) { font-size: 0.86em; }
</style> </style>
<div class="content-container snug regular"> <div class="content-container snug regular">
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2> <h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2>
{{if .Flashes}}<ul class="errors"> {{if .Flashes}}<ul class="errors">


+ 2
- 2
templates/user/stats.tmpl View File

@@ -17,8 +17,8 @@ td.none {
</style> </style>


<div class="content-container snug"> <div class="content-container snug">
{{if .Suspended}}
{{template "user-suspended"}}
{{if .Silenced}}
{{template "user-silenced"}}
{{end}} {{end}}
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2> <h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>




+ 53
- 4
webfinger.go View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC.
* Copyright © 2018-2020 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@@ -11,7 +11,10 @@
package writefreely package writefreely


import ( import (
"encoding/json"
"io/ioutil"
"net/http" "net/http"
"strings"


"github.com/writeas/go-webfinger" "github.com/writeas/go-webfinger"
"github.com/writeas/impart" "github.com/writeas/impart"
@@ -38,12 +41,12 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
log.Error("Unable to get blog: %v", err) log.Error("Unable to get blog: %v", err)
return nil, err return nil, err
} }
suspended, err := wfr.db.IsUserSuspended(c.OwnerID)
silenced, err := wfr.db.IsUserSilenced(c.OwnerID)
if err != nil { if err != nil {
log.Error("webfinger find user: check is suspended: %v", err)
log.Error("webfinger find user: check is silenced: %v", err)
return nil, err return nil, err
} }
if suspended {
if silenced {
return nil, wfUserNotFoundErr return nil, wfUserNotFoundErr
} }
c.hostName = wfr.cfg.App.Host c.hostName = wfr.cfg.App.Host
@@ -89,3 +92,49 @@ func (wfr wfResolver) DummyUser(username string, hostname string, r []webfinger.
func (wfr wfResolver) IsNotFoundError(err error) bool { func (wfr wfResolver) IsNotFoundError(err error) bool {
return err == wfUserNotFoundErr return err == wfUserNotFoundErr
} }

// RemoteLookup looks up a user by handle at a remote server
// and returns the actor URL
func RemoteLookup(handle string) string {
handle = strings.TrimLeft(handle, "@")
// let's take the server part of the handle
parts := strings.Split(handle, "@")
resp, err := http.Get("https://" + parts[1] + "/.well-known/webfinger?resource=acct:" + handle)
if err != nil {
log.Error("Error performing webfinger request", err)
return ""
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("Error reading webfinger response", err)
return ""
}

var result webfinger.Resource
err = json.Unmarshal(body, &result)
if err != nil {
log.Error("Unsupported webfinger response received: %v", err)
return ""
}

var href string
// iterate over webfinger links and find the one with
// a self "rel"
for _, link := range result.Links {
if link.Rel == "self" {
href = link.HRef
}
}

// if we didn't find it with the above then
// try using aliases
if href == "" {
// take the last alias because mastodon has the
// https://instance.tld/@user first which
// doesn't work as an href
href = result.Aliases[len(result.Aliases)-1]
}

return href
}

Loading…
Cancel
Save