@@ -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 | ||||
@@ -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, | ||||
@@ -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())) | |||||
} |
@@ -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)} | ||||
@@ -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) | ||||
@@ -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, | ||||
@@ -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 { | ||||
@@ -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" { | ||||
@@ -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 | |||||
} |
@@ -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 | |||||
} |
@@ -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 | |||||
} |
@@ -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 | ||||
@@ -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 | ||||
@@ -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 |
@@ -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= |
@@ -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 | ||||
@@ -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 | ||||
@@ -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 | |||||
} | } |
@@ -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 | |||||
}) | |||||
} |
@@ -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 | ||||
@@ -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, | ||||
} | } | ||||
@@ -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 | ||||
} | } | ||||
@@ -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}} |
@@ -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() | ||||
@@ -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) | ||||
@@ -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()) | ||||
@@ -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." | ||||
@@ -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')); | |||||
} |
@@ -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"), | |||||
)) | )) | ||||
} | } | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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> | ||||
@@ -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) { | ||||
@@ -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 | ||||
} | } | ||||
@@ -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> | ||||
@@ -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; | ||||
} | } | ||||
@@ -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> | ||||
@@ -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'); | ||||
@@ -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"/> | ||||
@@ -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> | ||||
@@ -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> | ||||
@@ -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"> | ||||
@@ -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); | ||||
}) | }) | ||||
@@ -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> |
@@ -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> | ||||
@@ -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"> | ||||
@@ -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> | ||||
@@ -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 | |||||
} |