Scheduled
{{end}}{{if .Title.String}}diff --git a/Makefile b/Makefile index 782e680..85f02d3 100644 --- a/Makefile +++ b/Makefile @@ -25,31 +25,37 @@ build-no-sqlite: assets-no-sqlite deps-no-sqlite build-linux: deps @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GOGET) -u github.com/karalabe/xgo; \ + $(GOGET) -u src.techknowlogick.com/xgo; \ fi xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely build-windows: deps @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GOGET) -u github.com/karalabe/xgo; \ + $(GOGET) -u src.techknowlogick.com/xgo; \ fi xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely build-darwin: deps @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GOGET) -u github.com/karalabe/xgo; \ + $(GOGET) -u src.techknowlogick.com/xgo; \ fi 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 @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GOGET) -u github.com/karalabe/xgo; \ + $(GOGET) -u src.techknowlogick.com/xgo; \ fi xgo --targets=linux/arm-7, -dest build/ $(LDFLAGS) -tags='sqlite' -out writefreely ./cmd/writefreely build-arm64: deps @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GOGET) -u github.com/karalabe/xgo; \ + $(GOGET) -u src.techknowlogick.com/xgo; \ fi 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) tar -cvzf $(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz -C build $(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 mv build/$(BINARY_NAME)-linux-arm-7 $(BUILDPATH)/$(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 $(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 $(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql diff --git a/account.go b/account.go index ae3ef87..c4274dc 100644 --- a/account.go +++ b/account.go @@ -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) } - suspended, err := app.db.IsUserSuspended(u.ID) + silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { log.Error("view articles: %v", err) } @@ -754,12 +754,12 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err *UserPage AnonymousPosts *[]PublicPost Collections *[]Collection - Suspended bool + Silenced bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), AnonymousPosts: p, Collections: c, - Suspended: suspended, + Silenced: silenced, } d.UserPage.SetMessaging(u) 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) // TODO: handle any errors - suspended, err := app.db.IsUserSuspended(u.ID) + silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { log.Error("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 NewBlogsDisabled bool - Suspended bool + Silenced bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), Collections: c, UsedCollections: int(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), - Suspended: suspended, + Silenced: silenced, } d.UserPage.SetMessaging(u) showUserPage(w, "collections", d) @@ -817,7 +817,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques return ErrCollectionNotFound } - suspended, err := app.db.IsUserSuspended(u.ID) + silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { log.Error("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 { *UserPage *Collection - Suspended bool + Silenced bool }{ UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), Collection: c, - Suspended: suspended, + Silenced: silenced, } showUserPage(w, "collection", obj) @@ -992,7 +992,7 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error titleStats = c.DisplayTitle() + " " } - suspended, err := app.db.IsUserSuspended(u.ID) + silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { log.Error("view stats: %v", err) return err @@ -1003,13 +1003,13 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error Collection *Collection TopPosts *[]PublicPost APFollowers int - Suspended bool + Silenced bool }{ UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), VisitsBlog: alias, Collection: c, TopPosts: topPosts, - Suspended: suspended, + Silenced: silenced, } if app.cfg.App.Federation { 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 HasPass bool IsLogOut bool - Suspended bool + Silenced bool OauthSection bool OauthAccounts []oauthAccountInfo OauthSlack bool @@ -1072,7 +1072,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err Email: fullUser.EmailClear(app.keys), HasPass: passIsSet, IsLogOut: r.FormValue("logout") == "1", - Suspended: fullUser.IsSilenced(), + Silenced: fullUser.IsSilenced(), OauthSection: displayOauthSection, OauthAccounts: oauthAccounts, OauthSlack: enableOauthSlack, diff --git a/activitypub.go b/activitypub.go index a18a636..f15773f 100644 --- a/activitypub.go +++ b/activitypub.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -37,6 +37,8 @@ import ( const ( // TODO: delete. don't use this! apCustomHandleDefault = "blog" + + apCacheTime = time.Minute ) type RemoteUser struct { @@ -44,6 +46,7 @@ type RemoteUser struct { ActorID string Inbox string SharedInbox string + Handle string } 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 { w.Header().Set("Server", serverSoftware) @@ -80,18 +89,19 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re if err != nil { return err } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection activities: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } c.hostName = app.cfg.App.Host p := c.PersonObject() + setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, p, http.StatusOK) } @@ -113,12 +123,12 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques if err != nil { return err } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection outbox: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } 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) for _, pp := range *posts { pp.Collection = res - o := pp.ActivityObject(app.cfg) + o := pp.ActivityObject(app) a := activitystreams.NewCreateActivity(o) ocp.OrderedItems = append(ocp.OrderedItems, *a) } + setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } @@ -174,12 +185,12 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection followers: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } 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) } */ + setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } @@ -228,12 +240,12 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection following: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } c.hostName = app.cfg.App.Host @@ -251,6 +263,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req // Return outbox page ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p) ocp.OrderedItems = []interface{}{} + setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } @@ -270,12 +283,12 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // TODO: return Reject? return err } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection inbox: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } c.hostName = app.cfg.App.Host @@ -382,6 +395,11 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request } go func() { + if to == nil { + log.Error("No to! %v", err) + return + } + time.Sleep(2 * time.Second) am, err := a.Serialize() if err != nil { @@ -390,10 +408,6 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request } 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) if err != nil { 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 { 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 { return nil, err } @@ -564,7 +578,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { } p.Collection.hostName = app.cfg.App.Host actor := p.Collection.PersonObject(collID) - na := p.ActivityObject(app.cfg) + na := p.ActivityObject(app) // Add followers p.Collection.ID = collID @@ -610,7 +624,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { } } actor := p.Collection.PersonObject(collID) - na := p.ActivityObject(app.cfg) + na := p.ActivityObject(app) // Add followers p.Collection.ID = collID @@ -628,18 +642,25 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { inbox = f.Inbox } if _, ok := inboxes[inbox]; ok { + // check if we're already sending to this shared inbox inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { + // add the new shared inbox to the list inboxes[inbox] = []string{f.ActorID} } } + var activity *activitystreams.Activity + // for each one of the shared inboxes for si, instFolls := range inboxes { + // add all followers from that instance + // to the CC field na.CC = []string{} for _, f := range instFolls { na.CC = append(na.CC, f) } - var activity *activitystreams.Activity + // create a new "Create" activity + // with our article as object if isUpdate { activity = activitystreams.NewUpdateActivity(na) } else { @@ -647,17 +668,42 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { activity.To = na.To activity.CC = na.CC } + // and post it to that sharedInbox err = makeActivityPost(app.cfg.App.Host, actor, si, activity) if err != nil { 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 } func getRemoteUser(app *App, actorID string) (*RemoteUser, error) { 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 { case err == sql.ErrNoRows: 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 } +// 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) { log.Info("Fetching actor %s locally", actorIRI) actor := &activitystreams.Person{} @@ -743,3 +804,7 @@ func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error { return nil } + +func setCacheControl(w http.ResponseWriter, ttl time.Duration) { + w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", ttl.Seconds())) +} diff --git a/admin.go b/admin.go index 0a73a11..5f7d244 100644 --- a/admin.go +++ b/admin.go @@ -187,7 +187,11 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque var err error p.User, err = app.db.GetUserForAuth(username) 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) @@ -259,7 +263,7 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht err = app.db.SetUserStatus(user.ID, UserSilenced) } 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.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} diff --git a/app.go b/app.go index d465a3e..170c321 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/manifoldco/promptui" - "github.com/writeas/go-strip-markdown" + stripmd "github.com/writeas/go-strip-markdown" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/converter" @@ -689,6 +689,52 @@ func ResetPassword(apper Apper, username string) error { 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) { log.Info("Connecting to %s database...", app.cfg.Database.Type) diff --git a/author/author.go b/author/author.go index bf3bfe1..0114905 100644 --- a/author/author.go +++ b/author/author.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -65,6 +65,7 @@ var reservedUsernames = map[string]bool{ "metadata": true, "new": true, "news": true, + "oauth": true, "post": true, "posts": true, "privacy": true, diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 48993c7..7fc2342 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -13,11 +13,12 @@ package main import ( "flag" "fmt" + "os" + "strings" + "github.com/gorilla/mux" "github.com/writeas/web-core/log" "github.com/writeas/writefreely" - "os" - "strings" ) func main() { @@ -38,6 +39,7 @@ func main() { // Admin actions 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") + deleteUsername := flag.String("delete-user", "", "Delete a user with the given username") resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") outputVersion := flag.Bool("v", false, "Output the current version") flag.Parse() @@ -102,6 +104,13 @@ func main() { os.Exit(1) } 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 { err := writefreely.Migrate(app) if err != nil { diff --git a/collections.go b/collections.go index 5c9a1b7..9688ad9 100644 --- a/collections.go +++ b/collections.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -71,7 +71,7 @@ type ( IsTopLevel bool CurrentPage int TotalPages int - Suspended bool + Silenced bool } SubmittedCollection struct { // 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 } - suspended, err := app.db.IsUserSuspended(userID) + silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("new collection: %v", err) return ErrInternalGeneral } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } 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 } } - // TODO: check suspended + // TODO: check status for silenced app.db.GetPostsCount(res, isCollOwner) // Strip non-public information 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? - suspended, err := app.db.IsUserSuspended(c.OwnerID) + suspended, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("process protected collection permissions: %v", 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 - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("view collection: %v", err) 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") { ac := c.PersonObject() ac.Context = []interface{}{activitystreams.Namespace} + setCacheControl(w, apCacheTime) 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) } } - if !isOwner && suspended { + if !isOwner && silenced { return ErrCollectionNotFound } - displayPage.Suspended = isOwner && suspended + displayPage.Silenced = isOwner && silenced displayPage.Owner = owner coll.Owner = displayPage.Owner @@ -856,6 +857,19 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro 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 { vars := mux.Vars(r) tag := vars["tag"] @@ -925,7 +939,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return ErrCollectionNotFound } } - displayPage.Suspended = owner != nil && owner.IsSilenced() + displayPage.Silenced = owner != nil && owner.IsSilenced() displayPage.Owner = owner coll.Owner = displayPage.Owner // 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 { log.Error("existing collection: %v", err) return ErrInternalGeneral } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } if r.Method == "DELETE" { diff --git a/database-no-sqlite.go b/database-no-sqlite.go index a3d50fc..03d1a32 100644 --- a/database-no-sqlite.go +++ b/database-no-sqlite.go @@ -1,7 +1,7 @@ // +build !sqlite,!wflib /* - * Copyright © 2019 A Bunch Tell LLC. + * Copyright © 2019-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -28,3 +28,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool { 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 +} diff --git a/database-sqlite.go b/database-sqlite.go index 3741169..bd77e6a 100644 --- a/database-sqlite.go +++ b/database-sqlite.go @@ -48,3 +48,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool { 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 +} diff --git a/database.go b/database.go index cbda701..f36f519 100644 --- a/database.go +++ b/database.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -22,6 +22,7 @@ import ( "github.com/guregu/null" "github.com/guregu/null/zero" uuid "github.com/nu7hatch/gouuid" + "github.com/writeas/activityserve" "github.com/writeas/impart" "github.com/writeas/nerds/store" "github.com/writeas/web-core/activitypub" @@ -37,6 +38,7 @@ import ( const ( mySQLErrDuplicateKey = 1062 + mySQLErrCollationMix = 1267 driverMySQL = "mysql" driverSQLite = "sqlite3" @@ -63,7 +65,7 @@ type writestore interface { GetAccessToken(userID int64) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (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 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 } -// 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} err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) switch { 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: - 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 @@ -2115,22 +2117,13 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool { 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 rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) 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() colls := []Collection{} @@ -2138,103 +2131,158 @@ func (db *datastore) DeleteAccount(userID int64) (l *string, err error) { for rows.Next() { err = rows.Scan(&c.ID, &c.Alias) 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) } + // 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 for _, c := range colls { - // TODO: user deleteCollection() func // Delete tokens res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) if err != nil { 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() - 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 res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) if err != nil { 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() - 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 res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) if err != nil { 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() - 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 res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete collections: %v", err) - return + log.Error("Unable to delete collections: %v", err) + return err } rs, _ := res.RowsAffected() - stringLogln(l, "Deleted %d from collections", rs) + log.Info("Deleted %d from collections", rs) // Delete tokens res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) if err != nil { 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() - 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 + // 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) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete posts: %v", err) - return + log.Error("Unable to delete posts: %v", err) + return err } 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) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete attributes: %v", err) - return + log.Error("Unable to delete attributes: %v", err) + return err } 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) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete user: %v", err) - return + log.Error("Unable to delete user: %v", err) + return err } 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() if err != nil { 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) { @@ -2283,7 +2331,7 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) { 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) switch { - case err == sql.ErrNoRows: + case err == sql.ErrNoRows, db.isIgnorableError(err): return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."} case err != nil: 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) 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 +} diff --git a/errors.go b/errors.go index c0d435c..b62fc9e 100644 --- a/errors.go +++ b/errors.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -45,10 +45,11 @@ var ( 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."} - 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 diff --git a/feed.go b/feed.go index 44bb331..4e1f612 100644 --- a/feed.go +++ b/feed.go @@ -36,12 +36,12 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { return nil } - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("view feed: get user: %v", err) return ErrInternalGeneral } - if suspended { + if silenced { return ErrCollectionNotFound } c.hostName = app.cfg.App.Host diff --git a/go.mod b/go.mod index f6aa8b7..5da3da4 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,23 @@ require ( github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect github.com/clbanning/mxj v1.8.4 // indirect + github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.7.0 + github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d // indirect github.com/go-sql-driver/mysql v1.4.1 github.com/go-test/deep v1.0.1 // indirect github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect + github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gorilla/feeds v1.1.0 github.com/gorilla/mux v1.7.0 github.com/gorilla/schema v1.0.2 - github.com/gorilla/sessions v1.1.3 + github.com/gorilla/sessions v1.2.0 github.com/guregu/null v3.4.0+incompatible github.com/hashicorp/go-multierror v1.0.0 - github.com/ikeikeikeike/go-sitemap-generator v1.0.1 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 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/lunixbochs/vtclean v1.0.0 // indirect github.com/manifoldco/promptui v0.3.2 @@ -32,12 +33,13 @@ require ( github.com/nicksnyder/go-i18n v1.10.0 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/pelletier/go-toml v1.2.0 // indirect - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.8.1 // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/stretchr/testify v1.3.0 github.com/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-webfinger v0.0.0-20190106002315-85cf805c86d2 github.com/writeas/httpsig v1.0.0 @@ -49,15 +51,14 @@ require ( github.com/writeas/slug v1.2.0 github.com/writeas/web-core 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/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 gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect gopkg.in/ini.v1 v1.41.0 gopkg.in/yaml.v2 v2.2.2 // indirect + src.techknowlogick.com/xgo v0.0.0-20200129005940-d0fae26e014b // indirect ) go 1.13 diff --git a/go.sum b/go.sum index 5b8b88a..2d433ec 100644 --- a/go.sum +++ b/go.sum @@ -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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= +github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/go-fed/httpsig v0.1.0 h1:6F2OxRVnNTN4OPN+Mc2jxs2WEay9/qiHT/jphlvAwIY= github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= +github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d h1:+uoOvOnNDgsYbWtAij4xP6Rgir3eJGjocFPxBJETU/U= +github.com/go-fed/httpsig v0.1.1-0.20190924171022-f4c36041199d/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-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-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/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8 h1:WD8iJ37bRNwvETMfVTusVSAi0WdXTpfNVGY2aHycNKY= +github.com/gologme/log v0.0.0-20181207131047-4e5d8ccb38e8/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY= github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/gorilla/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/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 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/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.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/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/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/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw= 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/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY= github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0= +github.com/writeas/activityserve v0.0.0-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/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= 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.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/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/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM= 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/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= -github.com/writeas/web-core v1.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/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI= 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/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-20181217174547-8f45f776aaf1 h1:rJm0LuqUjoDhSk2zO9ISMSToQxGz7Os2jRiOL8AWu4c= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-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-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/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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= diff --git a/invites.go b/invites.go index 1dba7bd..d5d024a 100644 --- a/invites.go +++ b/invites.go @@ -56,12 +56,19 @@ func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Req p := struct { *UserPage - Invites *[]Invite + Invites *[]Invite + Silenced bool }{ UserPage: NewUserPage(app, r, u, "Invite People", f), } 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) if err != nil { return err @@ -79,7 +86,7 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re expVal := r.FormValue("expires") if u.IsSilenced() { - return ErrUserSuspended + return ErrUserSilenced } var err error diff --git a/migrations/migrations.go b/migrations/migrations.go index 1aa64cb..a5eea2d 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -56,12 +56,13 @@ func (m *migration) Migrate(db *datastore) error { } 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 diff --git a/migrations/v6.go b/migrations/v6.go index 9673bc4..c6f5012 100644 --- a/migrations/v6.go +++ b/migrations/v6.go @@ -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 -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 } diff --git a/migrations/v7.go b/migrations/v7.go new file mode 100644 index 0000000..a7ac567 --- /dev/null +++ b/migrations/v7.go @@ -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 + }) +} diff --git a/oauth_signup.go b/oauth_signup.go index cf90af6..220afbd 100644 --- a/oauth_signup.go +++ b/oauth_signup.go @@ -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 import ( @@ -22,16 +32,16 @@ type viewOauthSignupVars struct { AccessToken string TokenUsername string - TokenAlias string + TokenAlias string // TODO: rename this to match the data it represents: the collection title TokenEmail string TokenRemoteUser string Provider string ClientID 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 ( @@ -52,7 +62,7 @@ const ( type oauthSignupPageParams struct { AccessToken string TokenUsername string - TokenAlias string + TokenAlias string // TODO: rename this to match the data it represents: the collection title TokenEmail string TokenRemoteUser 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) } - 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{ Username: r.FormValue(oauthParamUsername), HashedPass: hashedPass, - HasPass: true, + HasPass: hasPass, Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey), Created: time.Now().Truncate(time.Second).UTC(), } @@ -131,13 +147,9 @@ func (h oauthHandler) validateOauthSignup(r *http.Request) error { if len(username) > 100 { 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) 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 { username := tp.TokenUsername - alias := tp.TokenAlias + collTitle := tp.TokenAlias email := tp.TokenEmail session, err := app.sessionStore.Get(r, cookieName) @@ -164,7 +176,7 @@ func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *ht username = tmpValue } if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 { - alias = tmpValue + collTitle = tmpValue } if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 { email = tmpValue @@ -184,9 +196,9 @@ func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *ht ClientID: tp.ClientID, TokenHash: tp.TokenHash, - Username: username, - Alias: alias, - Email: email, + LoginUsername: username, + Alias: collTitle, + Email: email, } // Display any error messages diff --git a/oauth_slack.go b/oauth_slack.go index 8cf4992..35db156 100644 --- a/oauth_slack.go +++ b/oauth_slack.go @@ -1,3 +1,13 @@ +/* + * Copyright © 2019-2020 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + package writefreely import ( @@ -157,7 +167,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse { return &InspectResponse{ 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, Email: resp.User.Email, } diff --git a/pad.go b/pad.go index 3b0f1c2..0354cd3 100644 --- a/pad.go +++ b/pad.go @@ -35,10 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } appData := &struct { 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 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 { 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 { - 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 Flashes []string NeedsToken bool - Suspended bool + Silenced bool }{ StaticPage: pageForReq(app, r), Post: &RawPost{Font: "norm"}, User: getUserSession(app, r), } var err error - appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) + appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID) if err != nil { - log.Error("view meta: get user suspended status: %v", err) + log.Error("view meta: get user status: %v", err) return ErrInternalGeneral } diff --git a/pages/signup-oauth.tmpl b/pages/signup-oauth.tmpl index 34081cf..ecf5db0 100644 --- a/pages/signup-oauth.tmpl +++ b/pages/signup-oauth.tmpl @@ -65,7 +65,7 @@ form dd { {{end}}
(.+)
") + 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) { @@ -86,6 +87,8 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c tagPrefix = "/read/t/" } md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) + handlePrefix := cfg.App.Host + "/@/" + md = []byte(mentionReg.ReplaceAll(md, []byte("@$1$2"))) } // Strip out bad HTML policy := getSanitizationPolicy() diff --git a/posts.go b/posts.go index d2fbcca..a0e4588 100644 --- a/posts.go +++ b/posts.go @@ -1,5 +1,5 @@ /* - * Copyright © 2018-2019 A Bunch Tell LLC. + * Copyright © 2018-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -35,7 +35,6 @@ import ( "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" "github.com/writeas/web-core/tags" - "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" "github.com/writeas/writefreely/parse" ) @@ -229,6 +228,10 @@ func (p Post) Summary() string { 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. // TODO: use HTMLExcerpt in templates instead of this method 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 { - suspended, err = app.db.IsUserSuspended(ownerID.Int64) + silenced, err = app.db.IsUserSilenced(ownerID.Int64) if err != nil { log.Error("view post: %v", err) } @@ -436,10 +439,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { page := struct { *AnonymousPost page.StaticPage - Username string - IsOwner bool - SiteURL string - Suspended bool + Username string + IsOwner bool + SiteURL string + Silenced bool }{ AnonymousPost: post, 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 } - if !page.IsOwner && suspended { + if !page.IsOwner && silenced { return ErrPostNotFound } - page.Suspended = suspended + page.Silenced = silenced err = templates["post"].ExecuteTemplate(w, "post", page) if err != nil { log.Error("Post template execute error: %v", err) @@ -510,12 +513,12 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { } else { userID = app.db.GetUserID(accessToken) } - suspended, err := app.db.IsUserSuspended(userID) + silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("new post: %v", err) } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } 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 { log.Error("existing post: %v", err) } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } // Modify post struct @@ -885,12 +888,12 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { ownerID = u.ID } - suspended, err := app.db.IsUserSuspended(ownerID) + silenced, err := app.db.IsUserSilenced(ownerID) if err != nil { log.Error("add post: %v", err) } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } // Parse claimed posts in format: @@ -987,12 +990,12 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { userID = u.ID } - suspended, err := app.db.IsUserSuspended(userID) + silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("pin post: %v", err) } - if suspended { - return ErrUserSuspended + if silenced { + return ErrUserSilenced } // 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 { log.Error("fetch post: %v", err) } - if suspended { + if silenced { return ErrPostNotFound } @@ -1087,8 +1090,9 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { } p.Collection = &CollectionObj{Collection: *coll} - po := p.ActivityObject(app.cfg) + po := p.ActivityObject(app) po.Context = []interface{}{activitystreams.Namespace} + setCacheControl(w, apCacheTime) 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 } -func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object { +func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object { + cfg := app.cfg o := activitystreams.NewArticleObject() o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID 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 } @@ -1329,7 +1355,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error } c.hostName = app.cfg.App.Host - suspended, err := app.db.IsUserSuspended(c.OwnerID) + silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("view collection post: %v", err) } @@ -1339,7 +1365,7 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error return ErrPostNotFound } if c.IsProtected() && (u == nil || u.ID != c.OwnerID) { - if suspended { + if silenced { return ErrPostNotFound } else if !isAuthorizedForCollection(app, c.Alias, r) { return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/?g=" + slug} @@ -1394,7 +1420,7 @@ Are you sure it was ever here?`, p.Collection = coll p.IsTopLevel = app.cfg.App.SingleUser - if !p.IsOwner && suspended { + if !p.IsOwner && silenced { return ErrPostNotFound } // Check if post has been unpublished @@ -1428,8 +1454,9 @@ Are you sure it was ever here?`, return ErrCollectionPageNotFound } p.extractData() - ap := p.ActivityObject(app.cfg) + ap := p.ActivityObject(app) ap.Context = []interface{}{activitystreams.Namespace} + setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ap, http.StatusOK) } else { p.extractData() @@ -1446,14 +1473,14 @@ Are you sure it was ever here?`, IsFound bool IsAdmin bool CanInvite bool - Suspended bool + Silenced bool }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, - Suspended: suspended, + Silenced: silenced, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) diff --git a/routes.go b/routes.go index cb6d327..54ee7fd 100644 --- a/routes.go +++ b/routes.go @@ -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(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) + // handle mentions + write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader)) + configureSlackOauth(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 write.HandleFunc("/login", handler.Web(viewLogin, 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 write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter()) @@ -170,14 +173,14 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { draftEditPrefix := "" if apper.App().cfg.App.SingleUser { draftEditPrefix = "/d" - write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET") } else { - write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET") } // 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 if apper.App().cfg.App.SingleUser { RouteCollections(handler, write.PathPrefix("/").Subrouter()) diff --git a/scripts/upgrade-server.sh b/scripts/upgrade-server.sh index c8e004a..581085d 100755 --- a/scripts/upgrade-server.sh +++ b/scripts/upgrade-server.sh @@ -11,7 +11,7 @@ ## 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. # @@ -31,7 +31,7 @@ fi # go ahead and check for the latest release on linux 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 @@ -82,13 +82,25 @@ filename=${parts[-1]} echo "Extracting files..." 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 echo "Copying files..." -cp -r $tempdir/{pages,static,templates,writefreely} . +cp -r $tempdir/writefreely/{pages,static,templates,writefreely} . + +# migrate db +./writefreely -migrate # 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." else echo "Upgrade complete, but failed to restart service." diff --git a/static/js/localdate.js b/static/js/localdate.js new file mode 100644 index 0000000..879ebe4 --- /dev/null +++ b/static/js/localdate.js @@ -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')); +} \ No newline at end of file diff --git a/templates.go b/templates.go index c1a6da8..5ee4bcf 100644 --- a/templates.go +++ b/templates.go @@ -65,7 +65,7 @@ func initTemplate(parentDir, name string) { filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.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" { // 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, filepath.Join(parentDir, templatesDir, "include", "footer.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, filepath.Join(parentDir, templatesDir, "user", "include", "header.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"), )) } diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index 0e1164b..0418767 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -55,10 +55,10 @@ body#post header { {{template "user-navigation" .}} - {{if .Suspended}} - {{template "user-suspended"}} + {{if .Silenced}} + {{template "user-silenced"}} {{end}} -Scheduled
{{end}}{{if .Title.String}}Scheduled
{{end}}{{if .Title.String}}