mirror of
https://github.com/writefreely/writefreely
synced 2024-11-28 03:20:17 +00:00
Merge branch 'develop' into acme-v2
This commit is contained in:
commit
42467fc9c1
52 changed files with 1092 additions and 289 deletions
22
Makefile
22
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
|
||||
|
|
42
account.go
42
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)
|
||||
|
@ -1040,16 +1040,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
|||
|
||||
obj := struct {
|
||||
*UserPage
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Suspended bool
|
||||
Email string
|
||||
HasPass bool
|
||||
IsLogOut bool
|
||||
Silenced bool
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Suspended: fullUser.IsSilenced(),
|
||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||
Email: fullUser.EmailClear(app.keys),
|
||||
HasPass: passIsSet,
|
||||
IsLogOut: r.FormValue("logout") == "1",
|
||||
Silenced: fullUser.IsSilenced(),
|
||||
}
|
||||
|
||||
showUserPage(w, "settings", obj)
|
||||
|
|
195
account_import.go
Normal file
195
account_import.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/writeas/impart"
|
||||
wfimport "github.com/writeas/import"
|
||||
"github.com/writeas/web-core/log"
|
||||
)
|
||||
|
||||
func viewImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
// Fetch extra user data
|
||||
p := NewUserPage(app, r, u, "Import Posts", nil)
|
||||
|
||||
c, err := app.db.GetCollections(u, app.Config().App.Host)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("unable to fetch collections: %v", err)}
|
||||
}
|
||||
|
||||
d := struct {
|
||||
*UserPage
|
||||
Collections *[]Collection
|
||||
Flashes []template.HTML
|
||||
Message string
|
||||
InfoMsg bool
|
||||
}{
|
||||
UserPage: p,
|
||||
Collections: c,
|
||||
Flashes: []template.HTML{},
|
||||
}
|
||||
|
||||
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||
for _, flash := range flashes {
|
||||
if strings.HasPrefix(flash, "SUCCESS: ") {
|
||||
d.Message = strings.TrimPrefix(flash, "SUCCESS: ")
|
||||
} else if strings.HasPrefix(flash, "INFO: ") {
|
||||
d.Message = strings.TrimPrefix(flash, "INFO: ")
|
||||
d.InfoMsg = true
|
||||
} else {
|
||||
d.Flashes = append(d.Flashes, template.HTML(flash))
|
||||
}
|
||||
}
|
||||
|
||||
showUserPage(w, "import", d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
// limit 10MB per submission
|
||||
r.ParseMultipartForm(10 << 20)
|
||||
|
||||
collAlias := r.PostFormValue("collection")
|
||||
coll := &Collection{
|
||||
ID: 0,
|
||||
}
|
||||
var err error
|
||||
if collAlias != "" {
|
||||
coll, err = app.db.GetCollection(collAlias)
|
||||
if err != nil {
|
||||
log.Error("Unable to get collection for import: %s", err)
|
||||
return err
|
||||
}
|
||||
// Only allow uploading to collection if current user is owner
|
||||
if coll.OwnerID != u.ID {
|
||||
err := ErrUnauthorizedGeneral
|
||||
_ = addSessionFlash(app, w, r, err.Message, nil)
|
||||
return err
|
||||
}
|
||||
coll.hostName = app.cfg.App.Host
|
||||
}
|
||||
|
||||
fileDates := make(map[string]int64)
|
||||
err = json.Unmarshal([]byte(r.FormValue("fileDates")), &fileDates)
|
||||
if err != nil {
|
||||
log.Error("invalid form data for file dates: %v", err)
|
||||
return impart.HTTPError{http.StatusBadRequest, "form data for file dates was invalid"}
|
||||
}
|
||||
files := r.MultipartForm.File["files"]
|
||||
var fileErrs []error
|
||||
filesSubmitted := len(files)
|
||||
var filesImported int
|
||||
for _, formFile := range files {
|
||||
fname := ""
|
||||
ok := func() bool {
|
||||
file, err := formFile.Open()
|
||||
if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("Unable to read file %s", formFile.Filename))
|
||||
log.Error("import file: open from form: %v", err)
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tempFile, err := ioutil.TempFile("", "post-upload-*.txt")
|
||||
if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename))
|
||||
log.Error("import file: create temp file %s: %v", formFile.Filename, err)
|
||||
return false
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
_, err = io.Copy(tempFile, file)
|
||||
if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename))
|
||||
log.Error("import file: copy to temp location %s: %v", formFile.Filename, err)
|
||||
return false
|
||||
}
|
||||
|
||||
info, err := tempFile.Stat()
|
||||
if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename))
|
||||
log.Error("import file: stat temp file %s: %v", formFile.Filename, err)
|
||||
return false
|
||||
}
|
||||
fname = info.Name()
|
||||
return true
|
||||
}()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
post, err := wfimport.FromFile(filepath.Join(os.TempDir(), fname))
|
||||
if err == wfimport.ErrEmptyFile {
|
||||
// not a real error so don't log
|
||||
_ = addSessionFlash(app, w, r, fmt.Sprintf("%s was empty, import skipped", formFile.Filename), nil)
|
||||
continue
|
||||
} else if err == wfimport.ErrInvalidContentType {
|
||||
// same as above
|
||||
_ = addSessionFlash(app, w, r, fmt.Sprintf("%s is not a supported post file", formFile.Filename), nil)
|
||||
continue
|
||||
} else if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("failed to read copy of %s", formFile.Filename))
|
||||
log.Error("import textfile: file to post: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if collAlias != "" {
|
||||
post.Collection = collAlias
|
||||
}
|
||||
dateTime := time.Unix(fileDates[formFile.Filename], 0)
|
||||
post.Created = &dateTime
|
||||
created := post.Created.Format("2006-01-02T15:04:05Z")
|
||||
submittedPost := SubmittedPost{
|
||||
Title: &post.Title,
|
||||
Content: &post.Content,
|
||||
Font: "norm",
|
||||
Created: &created,
|
||||
}
|
||||
rp, err := app.db.CreatePost(u.ID, coll.ID, &submittedPost)
|
||||
if err != nil {
|
||||
fileErrs = append(fileErrs, fmt.Errorf("failed to create post from %s", formFile.Filename))
|
||||
log.Error("import textfile: create db post: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Federate post, if necessary
|
||||
if app.cfg.App.Federation && coll.ID > 0 {
|
||||
go federatePost(
|
||||
app,
|
||||
&PublicPost{
|
||||
Post: rp,
|
||||
Collection: &CollectionObj{
|
||||
Collection: *coll,
|
||||
},
|
||||
},
|
||||
coll.ID,
|
||||
false,
|
||||
)
|
||||
}
|
||||
filesImported++
|
||||
}
|
||||
if len(fileErrs) != 0 {
|
||||
_ = addSessionFlash(app, w, r, multierror.ListFormatFunc(fileErrs), nil)
|
||||
}
|
||||
|
||||
if filesImported == filesSubmitted {
|
||||
verb := "posts"
|
||||
if filesSubmitted == 1 {
|
||||
verb = "post"
|
||||
}
|
||||
_ = addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: Import complete, %d %s imported.", filesImported, verb), nil)
|
||||
} else if filesImported > 0 {
|
||||
_ = addSessionFlash(app, w, r, fmt.Sprintf("INFO: %d of %d posts imported, see details below.", filesImported, filesSubmitted), nil)
|
||||
}
|
||||
return impart.HTTPError{http.StatusFound, "/me/import"}
|
||||
}
|
109
activitypub.go
109
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()))
|
||||
}
|
||||
|
|
8
admin.go
8
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)}
|
||||
|
|
48
app.go
48
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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
191
database.go
191
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
|
||||
|
||||
|
@ -317,18 +319,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
|
||||
|
@ -2113,22 +2115,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{}
|
||||
|
@ -2136,103 +2129,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) {
|
||||
|
@ -2281,7 +2329,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)
|
||||
|
@ -2555,3 +2603,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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
4
feed.go
4
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
|
||||
|
|
9
go.mod
9
go.mod
|
@ -6,17 +6,21 @@ 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/v2 v2.0.2
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec
|
||||
|
@ -35,10 +39,12 @@ require (
|
|||
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
|
||||
github.com/writeas/impart v1.1.1-0.20191230230525-d3c45ced010d
|
||||
github.com/writeas/import v0.2.0
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219
|
||||
github.com/writeas/nerds v1.0.0
|
||||
github.com/writeas/saturday v1.7.1
|
||||
|
@ -52,6 +58,7 @@ require (
|
|||
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
|
||||
|
|
33
go.sum
33
go.sum
|
@ -1,3 +1,5 @@
|
|||
code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs=
|
||||
code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk=
|
||||
|
@ -23,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=
|
||||
|
@ -38,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=
|
||||
|
@ -54,10 +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/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=
|
||||
|
@ -115,16 +126,28 @@ 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=
|
||||
github.com/writeas/go-webfinger v0.0.0-20190106002315-85cf805c86d2/go.mod h1:w2VxyRO/J5vfNjJHYVubsjUGHd3RLDoVciz0DE3ApOc=
|
||||
github.com/writeas/go-writeas v1.1.0 h1:WHGm6wriBkxYAOGbvriXH8DlMUGOi6jhSZLUZKQ+4mQ=
|
||||
github.com/writeas/go-writeas v1.1.0/go.mod h1:oh9U1rWaiE0p3kzdKwwvOpNXgp0P0IELI7OLOwV4fkA=
|
||||
github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk=
|
||||
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
|
||||
github.com/writeas/httpsig v1.0.0 h1:peIAoIA3DmlP8IG8tMNZqI4YD1uEnWBmkcC9OFPjt3A=
|
||||
github.com/writeas/httpsig v1.0.0/go.mod h1:7ClMGSrSVXJbmiLa17bZ1LrG1oibGZmUMlh3402flPY=
|
||||
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.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=
|
||||
github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ=
|
||||
github.com/writeas/nerds v1.0.0 h1:ZzRcCN+Sr3MWID7o/x1cr1ZbLvdpej9Y1/Ho+JKlqxo=
|
||||
|
@ -177,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=
|
||||
|
|
11
invites.go
11
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
|
||||
|
|
|
@ -1318,6 +1318,24 @@ form {
|
|||
font-size: 0.86em;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
&.prominent {
|
||||
margin: 1em 0;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
input, select {
|
||||
width: 100%;
|
||||
}
|
||||
select {
|
||||
font-size: 1em;
|
||||
padding: 0.5rem;
|
||||
display: block;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
div.row {
|
||||
display: flex;
|
||||
|
|
|
@ -56,11 +56,12 @@ 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 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)
|
||||
}
|
||||
|
||||
// CurrentVer returns the current migration version the application is on
|
||||
|
|
29
migrations/v6.go
Normal file
29
migrations/v6.go
Normal file
|
@ -0,0 +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
|
||||
|
||||
func supportActivityPubMentions(db *datastore) error {
|
||||
t, err := db.Begin()
|
||||
|
||||
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` DEFAULT '' NOT NULL`)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.Commit()
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
5
oauth.go
5
oauth.go
|
@ -224,6 +224,11 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http
|
|||
return nil
|
||||
}
|
||||
|
||||
displayName := tokenInfo.DisplayName
|
||||
if len(displayName) == 0 {
|
||||
displayName = tokenInfo.Username
|
||||
}
|
||||
|
||||
tp := &oauthSignupPageParams{
|
||||
AccessToken: tokenResponse.AccessToken,
|
||||
TokenUsername: tokenInfo.Username,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
18
pad.go
18
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
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ form dd {
|
|||
</ul>{{end}}
|
||||
|
||||
<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="token_username" value="{{ .TokenUsername }}" />
|
||||
<input type="hidden" name="token_alias" value="{{ .TokenAlias }}" />
|
||||
|
@ -77,15 +77,15 @@ form dd {
|
|||
|
||||
<dl class="billing">
|
||||
<label>
|
||||
<dt>Blog Title</dt>
|
||||
<dt>Display Name</dt>
|
||||
<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>
|
||||
</label>
|
||||
<label>
|
||||
<dt>Username</dt>
|
||||
<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}}
|
||||
</dd>
|
||||
</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 }} />
|
||||
</dd>
|
||||
</label>
|
||||
<label>
|
||||
<dt>Password</dt>
|
||||
<dd>
|
||||
<input type="password" name="password" style="width: 100%; box-sizing: border-box;" placeholder="Password" /><br />
|
||||
</dd>
|
||||
</label>
|
||||
<dt>
|
||||
<input type="submit" id="btn-login" value="Login" />
|
||||
</dt>
|
||||
|
@ -108,11 +102,73 @@ form dd {
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="/js/h.js"></script>
|
||||
<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() {
|
||||
// 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");
|
||||
$btn.value = "Logging in...";
|
||||
$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>
|
||||
{{end}}
|
||||
|
|
|
@ -38,6 +38,7 @@ var (
|
|||
titleElementReg = regexp.MustCompile("</?h[1-6]>")
|
||||
hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`)
|
||||
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) {
|
||||
|
@ -86,6 +87,8 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *c
|
|||
tagPrefix = "/read/t/"
|
||||
}
|
||||
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
|
||||
policy := getSanitizationPolicy()
|
||||
|
|
91
posts.go
91
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)
|
||||
|
|
15
routes.go
15
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())
|
||||
|
||||
|
@ -97,6 +100,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
|
||||
me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET")
|
||||
me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET")
|
||||
me.HandleFunc("/import", handler.User(viewImport)).Methods("GET")
|
||||
me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET")
|
||||
me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET")
|
||||
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET")
|
||||
|
@ -109,6 +113,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
|
||||
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
|
||||
apiMe.HandleFunc("/invites", handler.User(handleCreateUserInvite)).Methods("POST")
|
||||
apiMe.HandleFunc("/import", handler.User(handleImport)).Methods("POST")
|
||||
|
||||
// Sign up validation
|
||||
write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST")
|
||||
|
@ -159,7 +164,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())
|
||||
|
@ -167,14 +172,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())
|
||||
|
|
|
@ -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."
|
||||
|
|
16
static/js/localdate.js
Normal file
16
static/js/localdate.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
function toLocalDate(dateEl, displayEl) {
|
||||
var d = new Date(dateEl.getAttribute("datetime"));
|
||||
displayEl.textContent = d.toLocaleDateString(navigator.language || "en-US", { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
|
||||
// Adjust dates on individual post pages, and on posts in a list *with* an explicit title
|
||||
var $dates = document.querySelectorAll("article > time");
|
||||
for (var i=0; i < $dates.length; i++) {
|
||||
toLocalDate($dates[i], $dates[i]);
|
||||
}
|
||||
|
||||
// Adjust dates on posts in a list without an explicit title, where they act as the header
|
||||
$dates = document.querySelectorAll("h2.post-title > time");
|
||||
for (i=0; i < $dates.length; i++) {
|
||||
toLocalDate($dates[i], $dates[i].querySelector('a'));
|
||||
}
|
|
@ -64,7 +64,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"
|
||||
|
@ -88,7 +88,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"),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,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"),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -55,10 +55,10 @@ body#post header {
|
|||
|
||||
{{template "user-navigation" .}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{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 }}
|
||||
<footer dir="ltr">
|
||||
|
@ -83,6 +83,7 @@ body#post header {
|
|||
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
|
||||
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script src="/js/localdate.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var pinning = false;
|
||||
|
|
|
@ -61,8 +61,8 @@ body#collection header nav.tabs a:first-child {
|
|||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||
{{template "user-navigation" .}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<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>
|
||||
|
@ -115,6 +115,7 @@ body#collection header nav.tabs a:first-child {
|
|||
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/localdate.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script type="text/javascript">
|
||||
var deleting = false;
|
||||
|
|
|
@ -59,10 +59,10 @@
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{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 }}
|
||||
<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}}
|
||||
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script src="/js/localdate.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var pinning = false;
|
||||
|
|
|
@ -53,8 +53,8 @@
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
|
||||
<h1>{{.Tag}}</h1>
|
||||
|
@ -75,6 +75,7 @@
|
|||
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}}
|
||||
{{if .Collection.Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}}
|
||||
{{end}}
|
||||
<script src="/js/localdate.js"></script>
|
||||
{{if .IsOwner}}
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
|
|
|
@ -62,8 +62,8 @@
|
|||
</ul></nav>{{end}}
|
||||
|
||||
<header>
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{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>
|
||||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
||||
|
@ -116,6 +116,7 @@
|
|||
{{end}}
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script src="/js/localdate.js"></script>
|
||||
<script type="text/javascript">
|
||||
var deleting = false;
|
||||
function delPost(e, id, owned) {
|
||||
|
|
|
@ -269,7 +269,7 @@
|
|||
<script src="/js/h.js"></script>
|
||||
<script>
|
||||
function updateMeta() {
|
||||
if ({{.Suspended}}) {
|
||||
if ({{.Silenced}}) {
|
||||
alert("Your account is silenced, so you can't edit posts.");
|
||||
return
|
||||
}
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
</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}}
|
||||
<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 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>
|
||||
|
|
|
@ -131,9 +131,9 @@
|
|||
{{else}}var canPublish = true;{{end}}
|
||||
var publishing = false;
|
||||
var justPublished = false;
|
||||
var suspended = {{.Suspended}};
|
||||
var silenced = {{.Silenced}};
|
||||
var publish = function(content, font) {
|
||||
if (suspended === true) {
|
||||
if (silenced === true) {
|
||||
alert("Your account is silenced, so you can't publish or update posts.");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -49,8 +49,8 @@
|
|||
</nav>
|
||||
</header>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{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>
|
||||
|
|
|
@ -88,9 +88,9 @@
|
|||
<section itemscope itemtype="http://schema.org/Blog">
|
||||
{{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>
|
||||
<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}}
|
||||
<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}}
|
||||
<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>
|
||||
|
@ -112,7 +112,7 @@
|
|||
</nav>{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/localdate.js">
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var $articles = document.querySelectorAll('article');
|
||||
|
|
|
@ -7,21 +7,21 @@ table.classy th {
|
|||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
td.active-suspend {
|
||||
td.active-silence {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
td.active-suspend > input[type="submit"] {
|
||||
td.active-silence > input[type="submit"] {
|
||||
margin-left: auto;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
td.active-suspend {
|
||||
td.active-silence {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
td.active-suspend > input[type="submit"] {
|
||||
td.active-silence > input[type="submit"] {
|
||||
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}}>
|
||||
<a id="status"/>
|
||||
<th>Status</th>
|
||||
<td class="active-suspend">
|
||||
<td class="active-silence">
|
||||
{{if .User.IsSilenced}}
|
||||
<p>Silenced</p>
|
||||
<input type="submit" value="Unsilence"/>
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
{{if .Flashes}}<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>{{end}}
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
|
||||
<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">
|
||||
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
|
||||
<h4>
|
||||
|
@ -34,10 +37,11 @@
|
|||
{{end}}
|
||||
{{ end }}
|
||||
</h4>
|
||||
{{if .Summary}}<p>{{.Summary}}</p>{{end}}
|
||||
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{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 }}
|
||||
|
||||
<div id="moving"></div>
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<div class="content-container snug">
|
||||
<div id="overlay"></div>
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<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}}
|
||||
</ul>{{end}}
|
||||
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<h2>blogs</h2>
|
||||
<ul class="atoms collections">
|
||||
|
|
64
templates/user/import.tmpl
Normal file
64
templates/user/import.tmpl
Normal file
|
@ -0,0 +1,64 @@
|
|||
{{define "import"}}
|
||||
{{template "header" .}}
|
||||
<style>
|
||||
input[type=file] {
|
||||
padding: 0;
|
||||
font-size: 0.86em;
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="snug content-container">
|
||||
<h1 id="import-header">Import posts</h1>
|
||||
{{if .Message}}
|
||||
<div class="alert {{if .InfoMsg}}info{{else}}success{{end}}">
|
||||
<p>{{.Message}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Flashes}}
|
||||
<ul class="errors">
|
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
<p>Publish plain text or Markdown files to your account by uploading them below.</p>
|
||||
<div class="formContainer">
|
||||
<form id="importPosts" class="prominent" enctype="multipart/form-data" action="/api/me/import" method="POST">
|
||||
<label>Select some files to import:
|
||||
<input id="fileInput" class="fileInput" name="files" type="file" multiple accept="text/markdown, text/plain"/>
|
||||
</label>
|
||||
<input id="fileDates" name="fileDates" hidden/>
|
||||
<label>
|
||||
Import these posts to:
|
||||
<select name="collection">
|
||||
{{range $i, $el := .Collections}}
|
||||
<option value="{{.Alias}}" {{if eq $i 0}}selected{{end}}>{{.DisplayTitle}}</option>
|
||||
{{end}}
|
||||
<option value="">Drafts</option>
|
||||
</select>
|
||||
</label>
|
||||
<script>
|
||||
// timezone offset in seconds
|
||||
const tzOffsetSec = new Date().getTimezoneOffset() * 60;
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileDates = document.getElementById('fileDates');
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const files = e.target.files;
|
||||
let dateMap = {};
|
||||
for (let file of files) {
|
||||
// convert from milliseconds to seconds and adjust for tz
|
||||
dateMap[file.name] = Math.round(file.lastModified / 1000) + tzOffsetSec;
|
||||
}
|
||||
fileDates.value = JSON.stringify(dateMap);
|
||||
})
|
||||
</script>
|
||||
<input type="submit" value="Import" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
|
@ -10,6 +10,7 @@
|
|||
<li class="separator"><hr /></li>
|
||||
{{if .IsAdmin}}<li><a href="/admin">Admin</a></li>{{end}}
|
||||
<li><a href="/me/settings">Settings</a></li>
|
||||
<li><a href="/me/import">Import posts</a></li>
|
||||
<li><a href="/me/export">Export</a></li>
|
||||
<li class="separator"><hr /></li>
|
||||
<li><a href="/me/logout">Log out</a></li>
|
||||
|
@ -32,6 +33,7 @@
|
|||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||
<li><a href="/me/settings">Account settings</a></li>
|
||||
<li><a href="/me/import">Import posts</a></li>
|
||||
<li><a href="/me/export">Export</a></li>
|
||||
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}}
|
||||
<li class="separator"><hr /></li>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{define "user-suspended"}}
|
||||
{{define "user-silenced"}}
|
||||
<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>
|
||||
</div>
|
|
@ -8,18 +8,7 @@
|
|||
margin-left: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
select {
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
display: block;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
input, table.classy {
|
||||
table.classy {
|
||||
width: 100%;
|
||||
}
|
||||
table.classy.export a {
|
||||
|
@ -31,14 +20,17 @@ table td {
|
|||
</style>
|
||||
|
||||
<div class="snug content-container">
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<h1>Invite people</h1>
|
||||
<p>Invite others to join <em>{{.SiteName}}</em> by generating and sharing invite links below.</p>
|
||||
|
||||
<form style="margin: 2em 0" action="/api/me/invites" method="post">
|
||||
<form style="margin: 2em 0" class="prominent" action="/api/me/invites" method="post">
|
||||
<div class="row">
|
||||
<div class="half">
|
||||
<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="1">1 use</option>
|
||||
<option value="5">5 uses</option>
|
||||
|
@ -50,7 +42,7 @@ table td {
|
|||
</div>
|
||||
<div class="half">
|
||||
<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="30">30 minutes</option>
|
||||
<option value="60">1 hour</option>
|
||||
|
@ -63,7 +55,7 @@ table td {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ h3 { font-weight: normal; }
|
|||
.section > *:not(input) { font-size: 0.86em; }
|
||||
</style>
|
||||
<div class="content-container snug regular">
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<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">
|
||||
|
|
|
@ -17,8 +17,8 @@ td.none {
|
|||
</style>
|
||||
|
||||
<div class="content-container snug">
|
||||
{{if .Suspended}}
|
||||
{{template "user-suspended"}}
|
||||
{{if .Silenced}}
|
||||
{{template "user-silenced"}}
|
||||
{{end}}
|
||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
||||
|
||||
|
|
57
webfinger.go
57
webfinger.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright © 2018 A Bunch Tell LLC.
|
||||
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||
*
|
||||
* This file is part of WriteFreely.
|
||||
*
|
||||
|
@ -11,7 +11,10 @@
|
|||
package writefreely
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/writeas/go-webfinger"
|
||||
"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)
|
||||
return nil, err
|
||||
}
|
||||
suspended, err := wfr.db.IsUserSuspended(c.OwnerID)
|
||||
silenced, err := wfr.db.IsUserSilenced(c.OwnerID)
|
||||
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
|
||||
}
|
||||
if suspended {
|
||||
if silenced {
|
||||
return nil, wfUserNotFoundErr
|
||||
}
|
||||
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 {
|
||||
return err == wfUserNotFoundErr
|
||||
}
|
||||
|
||||
// RemoteLookup looks up a user by handle at a remote server
|
||||
// and returns the actor URL
|
||||
func RemoteLookup(handle string) string {
|
||||
handle = strings.TrimLeft(handle, "@")
|
||||
// let's take the server part of the handle
|
||||
parts := strings.Split(handle, "@")
|
||||
resp, err := http.Get("https://" + parts[1] + "/.well-known/webfinger?resource=acct:" + handle)
|
||||
if err != nil {
|
||||
log.Error("Error performing webfinger request", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error("Error reading webfinger response", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var result webfinger.Resource
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
log.Error("Unsupported webfinger response received: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var href string
|
||||
// iterate over webfinger links and find the one with
|
||||
// a self "rel"
|
||||
for _, link := range result.Links {
|
||||
if link.Rel == "self" {
|
||||
href = link.HRef
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find it with the above then
|
||||
// try using aliases
|
||||
if href == "" {
|
||||
// take the last alias because mastodon has the
|
||||
// https://instance.tld/@user first which
|
||||
// doesn't work as an href
|
||||
href = result.Aliases[len(result.Aliases)-1]
|
||||
}
|
||||
|
||||
return href
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue